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:
parent
0db81b30f3
commit
99b8dc4759
2
.gitignore
vendored
2
.gitignore
vendored
@ -14,7 +14,7 @@ coverage/
|
||||
# YALC (for Erik)
|
||||
.yalc
|
||||
yalc.lock
|
||||
|
||||
*.dcm
|
||||
# Logging, System files, misc.
|
||||
.idea/
|
||||
.npm
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
41
extensions/cornerstone/src/ViewportLoadingIndicator.js
Normal file
41
extensions/cornerstone/src/ViewportLoadingIndicator.js
Normal file
@ -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;
|
||||
83
extensions/cornerstone/src/ViewportOverlay.js
Normal file
83
extensions/cornerstone/src/ViewportOverlay.js
Normal file
@ -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;
|
||||
36
extensions/cornerstone/src/_shared/getTools.js
Normal file
36
extensions/cornerstone/src/_shared/getTools.js
Normal file
@ -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: [],
|
||||
|
||||
3
extensions/cornerstone/src/id.js
Normal file
3
extensions/cornerstone/src/id.js
Normal file
@ -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,
|
||||
|
||||
245
extensions/default/src/DicomJSONDataSource/index.js
Normal file
245
extensions/default/src/DicomJSONDataSource/index.js
Normal file
@ -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,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
188
extensions/default/src/getHangingProtocolModule.js
Normal file
188
extensions/default/src/getHangingProtocolModule.js
Normal file
@ -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"
|
||||
|
||||
@ -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
|
||||
}),
|
||||
});
|
||||
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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,
|
||||
@ -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());
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -4,6 +4,7 @@ import { hotkeys } from '@ohif/core';
|
||||
const ohif = {
|
||||
layout: 'org.ohif.default.layoutTemplateModule.viewerLayout',
|
||||
sopClassHandler: 'org.ohif.default.sopClassHandlerModule.stack',
|
||||
hangingProtocols: 'org.ohif.default.hangingProtocolModule.default',
|
||||
};
|
||||
|
||||
const tracked = {
|
||||
@ -17,6 +18,7 @@ const dicomsr = {
|
||||
viewport: 'org.ohif.dicom-sr.viewportModule.dicom-sr',
|
||||
};
|
||||
|
||||
|
||||
export default function mode({ modeConfiguration }) {
|
||||
return {
|
||||
// TODO: We're using this as a route segment
|
||||
@ -40,6 +42,18 @@ export default function mode({ modeConfiguration }) {
|
||||
};
|
||||
|
||||
ToolBarService.recordInteraction(interaction);
|
||||
|
||||
ToolBarService.init(extensionManager);
|
||||
ToolBarService.addButtons(toolbarButtons);
|
||||
ToolBarService.createButtonSection('primary', [
|
||||
'MeasurementTools',
|
||||
'Zoom',
|
||||
'WindowLevel',
|
||||
'Pan',
|
||||
'Capture',
|
||||
'Layout',
|
||||
'MoreTools',
|
||||
]);
|
||||
},
|
||||
onModeExit: () => {},
|
||||
validationTags: {
|
||||
@ -53,21 +67,10 @@ export default function mode({ modeConfiguration }) {
|
||||
routes: [
|
||||
{
|
||||
path: 'longitudinal',
|
||||
init: ({ servicesManager, extensionManager }) => {
|
||||
const { ToolBarService } = servicesManager.services;
|
||||
ToolBarService.init(extensionManager);
|
||||
ToolBarService.addButtons(toolbarButtons);
|
||||
ToolBarService.createButtonSection('primary', [
|
||||
'MeasurementTools',
|
||||
'Zoom',
|
||||
'WindowLevel',
|
||||
'Pan',
|
||||
'Capture',
|
||||
'Layout',
|
||||
'MoreTools',
|
||||
]);
|
||||
},
|
||||
layoutTemplate: ({ routeProps }) => {
|
||||
/*init: ({ servicesManager, extensionManager }) => {
|
||||
//defaultViewerRouteInit
|
||||
},*/
|
||||
layoutTemplate: ({ location, servicesManager }) => {
|
||||
return {
|
||||
id: ohif.layout,
|
||||
props: {
|
||||
@ -95,6 +98,7 @@ export default function mode({ modeConfiguration }) {
|
||||
'org.ohif.measurement-tracking',
|
||||
'org.ohif.dicom-sr',
|
||||
],
|
||||
hangingProtocols: [ohif.hangingProtocols],
|
||||
sopClassHandlers: [ohif.sopClassHandler, dicomsr.sopClassHandler],
|
||||
hotkeys: [...hotkeys.defaults.hotkeyBindings],
|
||||
};
|
||||
|
||||
@ -74,7 +74,7 @@
|
||||
"cross-env": "^5.2.0",
|
||||
"css-loader": "^3.2.0",
|
||||
"dotenv": "^8.1.0",
|
||||
"eslint": "5.16.0",
|
||||
"eslint": "6.8.0",
|
||||
"eslint-config-prettier": "^6.4.0",
|
||||
"eslint-config-react-app": "^5.2.0",
|
||||
"eslint-plugin-flowtype": "2.x",
|
||||
@ -84,7 +84,7 @@
|
||||
"eslint-plugin-prettier": "^3.1.1",
|
||||
"eslint-plugin-promise": "^4.2.1",
|
||||
"eslint-plugin-react": "7.x",
|
||||
"eslint-plugin-react-hooks": "1.5.0",
|
||||
"eslint-plugin-react-hooks": "4.2.0",
|
||||
"extract-css-chunks-webpack-plugin": "^4.5.4",
|
||||
"html-webpack-plugin": "^3.2.0",
|
||||
"husky": "^3.0.0",
|
||||
|
||||
@ -45,7 +45,8 @@
|
||||
"mousetrap": "^1.6.3",
|
||||
"query-string": "^6.14.0",
|
||||
"object-hash": "2.1.1",
|
||||
"cornerstone-math": "0.1.9"
|
||||
"cornerstone-math": "0.1.9",
|
||||
"validate.js": "^0.12.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"webpack-merge": "5.7.3"
|
||||
|
||||
@ -17,9 +17,11 @@ function create({
|
||||
retrieve,
|
||||
store,
|
||||
reject,
|
||||
parseRouteParams,
|
||||
retrieveSeriesMetadata,
|
||||
deleteStudyMetadataPromise,
|
||||
getImageIdsForDisplaySet,
|
||||
getImageIdsForInstance,
|
||||
}) {
|
||||
const defaultQuery = {
|
||||
studies: {
|
||||
@ -56,15 +58,16 @@ function create({
|
||||
};
|
||||
|
||||
const defaultReject = {};
|
||||
|
||||
return {
|
||||
query: query || defaultQuery,
|
||||
retrieve: retrieve || defaultRetrieve,
|
||||
reject: reject || defaultReject,
|
||||
store: store || defaultStore,
|
||||
getImageIdsForDisplaySet,
|
||||
parseRouteParams,
|
||||
retrieveSeriesMetadata,
|
||||
deleteStudyMetadataPromise,
|
||||
getImageIdsForDisplaySet,
|
||||
getImageIdsForInstance,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -189,7 +189,7 @@ class MetadataProvider {
|
||||
metadata = {
|
||||
modality: instance.Modality,
|
||||
seriesInstanceUID: instance.SeriesInstanceUID,
|
||||
seriesNumber: instance.SeriesNumber,
|
||||
seriesNumber: getNumberValues(instance.SeriesNumber),
|
||||
studyInstanceUID: instance.StudyInstanceUID,
|
||||
seriesDate,
|
||||
seriesTime,
|
||||
@ -197,9 +197,9 @@ class MetadataProvider {
|
||||
break;
|
||||
case WADO_IMAGE_LOADER_TAGS.PATIENT_STUDY_MODULE:
|
||||
metadata = {
|
||||
patientAge: instance.PatientAge,
|
||||
patientSize: instance.PatientSize,
|
||||
patientWeight: instance.PatientWeight,
|
||||
patientAge: getNumberValues(instance.PatientAge),
|
||||
patientSize: getNumberValues(instance.PatientSize),
|
||||
patientWeight: getNumberValues(instance.PatientWeight),
|
||||
};
|
||||
break;
|
||||
case WADO_IMAGE_LOADER_TAGS.IMAGE_PLANE_MODULE:
|
||||
@ -228,51 +228,53 @@ class MetadataProvider {
|
||||
|
||||
metadata = {
|
||||
frameOfReferenceUID: instance.FrameOfReferenceUID,
|
||||
rows: instance.Rows,
|
||||
columns: instance.Columns,
|
||||
imageOrientationPatient: ImageOrientationPatient,
|
||||
rowCosines,
|
||||
columnCosines,
|
||||
imagePositionPatient: instance.ImagePositionPatient,
|
||||
sliceThickness: instance.SliceThickness,
|
||||
sliceLocation: instance.SliceLocation,
|
||||
pixelSpacing: PixelSpacing,
|
||||
rowPixelSpacing,
|
||||
columnPixelSpacing,
|
||||
rows: getNumberValues(instance.Rows),
|
||||
columns: getNumberValues(instance.Columns),
|
||||
imageOrientationPatient: getNumberValues(ImageOrientationPatient),
|
||||
rowCosines: getNumberValues(rowCosines),
|
||||
columnCosines: getNumberValues(columnCosines),
|
||||
imagePositionPatient: getNumberValues(instance.ImagePositionPatient),
|
||||
sliceThickness: getNumberValues(instance.SliceThickness),
|
||||
sliceLocation: getNumberValues(instance.SliceLocation),
|
||||
pixelSpacing: getNumberValues(PixelSpacing),
|
||||
rowPixelSpacing: getNumberValues(rowPixelSpacing),
|
||||
columnPixelSpacing: getNumberValues(columnPixelSpacing),
|
||||
};
|
||||
break;
|
||||
case WADO_IMAGE_LOADER_TAGS.IMAGE_PIXEL_MODULE:
|
||||
metadata = {
|
||||
samplesPerPixel: instance.SamplesPerPixel,
|
||||
photometricInterpretation: instance.PhotometricInterpretation,
|
||||
rows: instance.Rows,
|
||||
columns: instance.Columns,
|
||||
bitsAllocated: instance.BitsAllocated,
|
||||
bitsStored: instance.BitsStored,
|
||||
highBit: instance.HighBit,
|
||||
pixelRepresentation: instance.PixelRepresentation,
|
||||
planarConfiguration: instance.PlanarConfiguration,
|
||||
pixelAspectRatio: instance.PixelAspectRatio,
|
||||
smallestPixelValue: instance.SmallestPixelValue,
|
||||
largestPixelValue: instance.LargestPixelValue,
|
||||
samplesPerPixel: getNumberValues(instance.SamplesPerPixel),
|
||||
photometricInterpretation: getNumberValues(instance.PhotometricInterpretation),
|
||||
rows: getNumberValues(instance.Rows),
|
||||
columns: getNumberValues(instance.Columns),
|
||||
bitsAllocated: getNumberValues(instance.BitsAllocated),
|
||||
bitsStored: getNumberValues(instance.BitsStored),
|
||||
highBit: getNumberValues(instance.HighBit),
|
||||
pixelRepresentation: getNumberValues(instance.PixelRepresentation),
|
||||
planarConfiguration: getNumberValues(instance.PlanarConfiguration),
|
||||
pixelAspectRatio: getNumberValues(instance.PixelAspectRatio),
|
||||
smallestPixelValue: getNumberValues(instance.SmallestPixelValue),
|
||||
largestPixelValue: getNumberValues(instance.LargestPixelValue),
|
||||
redPaletteColorLookupTableDescriptor:
|
||||
instance.RedPaletteColorLookupTableDescriptor,
|
||||
getNumberValues(instance.RedPaletteColorLookupTableDescriptor),
|
||||
greenPaletteColorLookupTableDescriptor:
|
||||
instance.GreenPaletteColorLookupTableDescriptor,
|
||||
getNumberValues(instance.GreenPaletteColorLookupTableDescriptor),
|
||||
bluePaletteColorLookupTableDescriptor:
|
||||
instance.BluePaletteColorLookupTableDescriptor,
|
||||
getNumberValues(instance.BluePaletteColorLookupTableDescriptor),
|
||||
redPaletteColorLookupTableData:
|
||||
instance.RedPaletteColorLookupTableData,
|
||||
getNumberValues(instance.RedPaletteColorLookupTableData),
|
||||
greenPaletteColorLookupTableData:
|
||||
instance.GreenPaletteColorLookupTableData,
|
||||
getNumberValues(instance.GreenPaletteColorLookupTableData),
|
||||
bluePaletteColorLookupTableData:
|
||||
instance.BluePaletteColorLookupTableData,
|
||||
getNumberValues(instance.BluePaletteColorLookupTableData),
|
||||
};
|
||||
|
||||
break;
|
||||
case WADO_IMAGE_LOADER_TAGS.VOI_LUT_MODULE:
|
||||
const { WindowCenter, WindowWidth } = instance;
|
||||
|
||||
if (WindowCenter === undefined || WindowWidth === undefined) {
|
||||
return;
|
||||
}
|
||||
const windowCenter = Array.isArray(WindowCenter)
|
||||
? WindowCenter
|
||||
: [WindowCenter];
|
||||
@ -281,15 +283,20 @@ class MetadataProvider {
|
||||
: [WindowWidth];
|
||||
|
||||
metadata = {
|
||||
windowCenter,
|
||||
windowWidth,
|
||||
windowCenter: getNumberValues(windowCenter),
|
||||
windowWidth: getNumberValues(windowWidth),
|
||||
};
|
||||
|
||||
break;
|
||||
case WADO_IMAGE_LOADER_TAGS.MODALITY_LUT_MODULE:
|
||||
const { RescaleIntercept, RescaleSlope } = instance;
|
||||
if (RescaleIntercept === undefined || RescaleSlope === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
metadata = {
|
||||
rescaleIntercept: instance.RescaleIntercept,
|
||||
rescaleSlope: instance.RescaleSlope,
|
||||
rescaleIntercept: getNumberValues(instance.RescaleIntercept),
|
||||
rescaleSlope: getNumberValues(instance.RescaleSlope),
|
||||
rescaleType: instance.RescaleType,
|
||||
};
|
||||
break;
|
||||
@ -401,7 +408,7 @@ class MetadataProvider {
|
||||
case WADO_IMAGE_LOADER_TAGS.GENERAL_IMAGE_MODULE:
|
||||
metadata = {
|
||||
sopInstanceUid: instance.SOPInstanceUID,
|
||||
instanceNumber: instance.InstanceNumber,
|
||||
instanceNumber: getNumberValues(instance.InstanceNumber),
|
||||
lossyImageCompression: instance.LossyImageCompression,
|
||||
lossyImageCompressionRatio: instance.LossyImageCompressionRatio,
|
||||
lossyImageCompressionMethod: instance.LossyImageCompressionMethod,
|
||||
@ -457,6 +464,31 @@ class MetadataProvider {
|
||||
|
||||
const metadataProvider = new MetadataProvider();
|
||||
|
||||
|
||||
/**
|
||||
* Returns the values as an array of javascript numbers
|
||||
*
|
||||
* @param element - The javascript object for the specified element in the metadata
|
||||
* @returns {*}
|
||||
*/
|
||||
function getNumberValues(element) {
|
||||
if (!element) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (Array.isArray(element)) {
|
||||
const values = [];
|
||||
for (let i = 0; i < element.length; i++) {
|
||||
values.push(parseFloat(element[i]));
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
return parseFloat(element)
|
||||
}
|
||||
|
||||
|
||||
|
||||
export default metadataProvider;
|
||||
|
||||
const WADO_IMAGE_LOADER_TAGS = {
|
||||
@ -479,3 +511,4 @@ const WADO_IMAGE_LOADER_TAGS = {
|
||||
};
|
||||
|
||||
const INSTANCE = 'instance';
|
||||
const DICOMWEB = 'dicomweb';
|
||||
|
||||
@ -2,7 +2,12 @@ import MODULE_TYPES from './MODULE_TYPES.js';
|
||||
import log from './../log.js';
|
||||
|
||||
export default class ExtensionManager {
|
||||
constructor({ commandsManager, servicesManager, hotkeysManager, appConfig = {} }) {
|
||||
constructor({
|
||||
commandsManager,
|
||||
servicesManager,
|
||||
hotkeysManager,
|
||||
appConfig = {},
|
||||
}) {
|
||||
this.modules = {};
|
||||
this.registeredExtensionIds = [];
|
||||
this.moduleTypeNames = Object.values(MODULE_TYPES);
|
||||
@ -52,7 +57,7 @@ export default class ExtensionManager {
|
||||
onModeEnter({
|
||||
servicesManager: _servicesManager,
|
||||
commandsManager: _commandsManager,
|
||||
hotkeysManager: _hotkeysManager
|
||||
hotkeysManager: _hotkeysManager,
|
||||
});
|
||||
}
|
||||
});
|
||||
@ -115,9 +120,7 @@ export default class ExtensionManager {
|
||||
*/
|
||||
registerExtension = (extension, configuration = {}, dataSources = []) => {
|
||||
if (!extension) {
|
||||
throw new Error(
|
||||
'Attempting to register a null/undefined extension.'
|
||||
);
|
||||
throw new Error('Attempting to register a null/undefined extension.');
|
||||
}
|
||||
|
||||
let extensionId = extension.id;
|
||||
@ -191,6 +194,13 @@ export default class ExtensionManager {
|
||||
] = element;
|
||||
});
|
||||
break;
|
||||
case MODULE_TYPES.HANGING_PROTOCOL:
|
||||
extensionModule.forEach(element => {
|
||||
this.modulesMap[
|
||||
`${extensionId}.${moduleType}.${element.name}`
|
||||
] = element;
|
||||
});
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Module type invalid: ${moduleType}`);
|
||||
}
|
||||
@ -224,6 +234,10 @@ export default class ExtensionManager {
|
||||
return this.dataSourceMap[this.activeDataSource];
|
||||
};
|
||||
|
||||
getDataSource = () => {
|
||||
return this.dataSourceMap[this.activeDataSource];
|
||||
};
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {string} moduleType
|
||||
@ -288,6 +302,32 @@ export default class ExtensionManager {
|
||||
});
|
||||
}
|
||||
|
||||
_initHangingProtocolModule(extensionModule, extensionId) {
|
||||
extensionModule.forEach(element => {
|
||||
const namespace = `${extensionId}.${MODULE_TYPES.HANGING_PROTOCOL}.${element.name}`;
|
||||
|
||||
dataSources.forEach(dataSource => {
|
||||
if (dataSource.namespace === namespace) {
|
||||
const dataSourceInstance = element.createDataSource(
|
||||
dataSource.configuration
|
||||
);
|
||||
|
||||
if (this.dataSourceMap[dataSource.sourceName]) {
|
||||
this.dataSourceMap[dataSource.sourceName].push(dataSourceInstance);
|
||||
} else {
|
||||
this.dataSourceMap[dataSource.sourceName] = [dataSourceInstance];
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
extensionModule.forEach(element => {
|
||||
this.modulesMap[
|
||||
`${extensionId}.${MODULE_TYPES.DATA_SOURCE}.${element.name}`
|
||||
] = element;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @private
|
||||
|
||||
@ -215,6 +215,9 @@ describe('ExtensionManager.js', () => {
|
||||
getDataSourcesModule: () => {
|
||||
return [{}];
|
||||
},
|
||||
getHangingProtocolModule: () => {
|
||||
return [{}];
|
||||
},
|
||||
getContextModule: () => {
|
||||
return [{}];
|
||||
}
|
||||
|
||||
@ -7,4 +7,5 @@ export default {
|
||||
VIEWPORT: 'viewportModule',
|
||||
CONTEXT: 'contextModule',
|
||||
LAYOUT_TEMPLATE: 'layoutTemplateModule',
|
||||
HANGING_PROTOCOL: 'hangingProtocolModule',
|
||||
};
|
||||
|
||||
@ -25,6 +25,7 @@ import {
|
||||
MeasurementService,
|
||||
ViewportGridService,
|
||||
HangingProtocolService,
|
||||
pubSubServiceInterface,
|
||||
} from './services';
|
||||
|
||||
import IWebApiDataSource from './DataSources/IWebApiDataSource';
|
||||
@ -67,6 +68,7 @@ const OHIF = {
|
||||
HangingProtocolService,
|
||||
IWebApiDataSource,
|
||||
DicomMetadataStore,
|
||||
pubSubServiceInterface,
|
||||
};
|
||||
|
||||
export {
|
||||
@ -101,6 +103,7 @@ export {
|
||||
HangingProtocolService,
|
||||
IWebApiDataSource,
|
||||
DicomMetadataStore,
|
||||
pubSubServiceInterface,
|
||||
};
|
||||
|
||||
export { OHIF };
|
||||
|
||||
@ -10,21 +10,6 @@ describe('Top level exports', () => {
|
||||
'HotkeysManager',
|
||||
'ServicesManager',
|
||||
//
|
||||
'DicomMetadataStore',
|
||||
//
|
||||
'CineService',
|
||||
'DisplaySetService',
|
||||
'HangingProtocolService',
|
||||
'ToolBarService',
|
||||
'UINotificationService',
|
||||
'UIModalService',
|
||||
'UIDialogService',
|
||||
'UIViewportDialogService',
|
||||
'MeasurementService',
|
||||
'ViewportGridService',
|
||||
//
|
||||
'IWebApiDataSource',
|
||||
//
|
||||
'defaults',
|
||||
'utils',
|
||||
'hotkeys',
|
||||
@ -37,7 +22,21 @@ describe('Top level exports', () => {
|
||||
'log',
|
||||
'DICOMWeb',
|
||||
'DICOMSR',
|
||||
'OHIF', //
|
||||
'OHIF',
|
||||
//
|
||||
'CineService',
|
||||
'UIDialogService',
|
||||
'UIModalService',
|
||||
'UINotificationService',
|
||||
'UIViewportDialogService',
|
||||
'DisplaySetService',
|
||||
'MeasurementService',
|
||||
'ToolBarService',
|
||||
'ViewportGridService',
|
||||
'HangingProtocolService',
|
||||
'IWebApiDataSource',
|
||||
'DicomMetadataStore',
|
||||
'pubSubServiceInterface'
|
||||
].sort();
|
||||
|
||||
const exports = Object.keys(OHIF).sort();
|
||||
|
||||
24
platform/core/src/measurements/tools/polygonRoi.js
Normal file
24
platform/core/src/measurements/tools/polygonRoi.js
Normal file
@ -0,0 +1,24 @@
|
||||
const displayFunction = data => {
|
||||
let meanValue = '';
|
||||
const { cachedStats } = data;
|
||||
if (cachedStats && cachedStats.mean && !isNaN(cachedStats.mean)) {
|
||||
meanValue = cachedStats.mean.toFixed(2) + ' HU';
|
||||
}
|
||||
return meanValue;
|
||||
};
|
||||
|
||||
export const polygonRoi = {
|
||||
id: 'PolygonRoi',
|
||||
name: 'Polygon',
|
||||
toolGroup: 'allTools',
|
||||
cornerstoneToolType: 'FreehandRoiTool',
|
||||
options: {
|
||||
measurementTable: {
|
||||
displayFunction,
|
||||
},
|
||||
caseProgress: {
|
||||
include: true,
|
||||
evaluate: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
@ -54,6 +54,18 @@ function _getInstance(StudyInstanceUID, SeriesInstanceUID, SOPInstanceUID) {
|
||||
);
|
||||
}
|
||||
|
||||
function _getInstanceFromImageId(imageId) {
|
||||
for (let study of _model.studies) {
|
||||
for (let series of study.series) {
|
||||
for (let instance of series.instances) {
|
||||
if (instance.imageId === imageId) {
|
||||
return instance;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const BaseImplementation = {
|
||||
EVENTS,
|
||||
listeners: {},
|
||||
@ -82,7 +94,7 @@ const BaseImplementation = {
|
||||
madeInClient,
|
||||
});
|
||||
},
|
||||
addSeriesMetadata(seriesSummaryMetadata) {
|
||||
addSeriesMetadata(seriesSummaryMetadata, madeInClient = false) {
|
||||
const { StudyInstanceUID } = seriesSummaryMetadata[0];
|
||||
let study = _getStudy(StudyInstanceUID);
|
||||
if (!study) {
|
||||
@ -98,6 +110,7 @@ const BaseImplementation = {
|
||||
|
||||
this._broadcastEvent(EVENTS.SERIES_ADDED, {
|
||||
StudyInstanceUID,
|
||||
madeInClient,
|
||||
});
|
||||
},
|
||||
addStudy(study) {
|
||||
@ -124,6 +137,7 @@ const BaseImplementation = {
|
||||
getStudy: _getStudy,
|
||||
getSeries: _getSeries,
|
||||
getInstance: _getInstance,
|
||||
getInstanceFromImageId: _getInstanceFromImageId,
|
||||
};
|
||||
|
||||
const DicomMetadataStore = Object.assign(
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
const EVENTS = {
|
||||
STUDY_ADDED: 'event::dicomMetadataStore:studyAdded',
|
||||
INSTANCES_ADDED: 'event::dicomMetadataStore:instancesAdded',
|
||||
SERIES_ADDED: 'event::dicomMetadataStore:seriesAdded',
|
||||
};
|
||||
|
||||
@ -3,6 +3,7 @@ import createSeriesMetadata from './createSeriesMetadata';
|
||||
function createStudyMetadata(StudyInstanceUID) {
|
||||
return {
|
||||
StudyInstanceUID,
|
||||
isLoaded: false,
|
||||
series: [],
|
||||
/**
|
||||
*
|
||||
@ -24,7 +25,7 @@ function createStudyMetadata(StudyInstanceUID) {
|
||||
}
|
||||
},
|
||||
|
||||
setSeriesMetadata: function (SeriesInstanceUID, seriesMetadata) {
|
||||
setSeriesMetadata: function(SeriesInstanceUID, seriesMetadata) {
|
||||
let existingSeries = this.series.find(
|
||||
s => s.SeriesInstanceUID === SeriesInstanceUID
|
||||
);
|
||||
@ -34,7 +35,7 @@ function createStudyMetadata(StudyInstanceUID) {
|
||||
} else {
|
||||
this.series.push(Object.assign({ instances: [] }, seriesMetadata));
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -96,10 +96,13 @@ export default class DisplaySetService {
|
||||
/**
|
||||
*
|
||||
* @param {*} input
|
||||
* @param {*} param1
|
||||
* @param {*} param1: settings: initialViewportSettings by HP or callbacks after rendering
|
||||
* @returns {string[]} - added displaySetInstanceUIDs
|
||||
*/
|
||||
makeDisplaySets = (input, { batch = false, madeInClient = false } = {}) => {
|
||||
makeDisplaySets = (
|
||||
input,
|
||||
{ batch = false, madeInClient = false, settings = {} } = {}
|
||||
) => {
|
||||
if (!input || !input.length) {
|
||||
throw new Error('No instances were provided.');
|
||||
}
|
||||
@ -116,12 +119,15 @@ export default class DisplaySetService {
|
||||
if (batch) {
|
||||
for (let i = 0; i < input.length; i++) {
|
||||
const instances = input[i];
|
||||
const displaySets = this.makeDisplaySetForInstances(instances);
|
||||
const displaySets = this.makeDisplaySetForInstances(
|
||||
instances,
|
||||
settings
|
||||
);
|
||||
|
||||
displaySetsAdded = [...displaySetsAdded, displaySets];
|
||||
}
|
||||
} else {
|
||||
const displaySets = this.makeDisplaySetForInstances(input);
|
||||
const displaySets = this.makeDisplaySetForInstances(input, settings);
|
||||
|
||||
displaySetsAdded = displaySets;
|
||||
}
|
||||
@ -145,7 +151,7 @@ export default class DisplaySetService {
|
||||
}
|
||||
};
|
||||
|
||||
makeDisplaySetForInstances(instances) {
|
||||
makeDisplaySetForInstances(instances, settings) {
|
||||
const instance = instances[0];
|
||||
|
||||
const existingDisplaySets =
|
||||
@ -168,6 +174,13 @@ export default class DisplaySetService {
|
||||
} else {
|
||||
displaySets = handler.getDisplaySetsFromSeries(instances);
|
||||
|
||||
// applying hp-defined viewport settings to the displaysets
|
||||
displaySets.forEach(ds => {
|
||||
Object.keys(settings).forEach(key => {
|
||||
ds[key] = settings[key];
|
||||
});
|
||||
});
|
||||
|
||||
this._addDisplaySetsToCache(displaySets);
|
||||
this._addActiveDisplaySets(displaySets);
|
||||
}
|
||||
|
||||
@ -0,0 +1,96 @@
|
||||
import validate from './lib/validator';
|
||||
|
||||
|
||||
/**
|
||||
* Match a Metadata instance against rules using Validate.js for validation.
|
||||
* @param {InstanceMetadata} metadataInstance Metadata instance object
|
||||
* @param {Array} rules Array of MatchingRules instances (StudyMatchingRule|SeriesMatchingRule|ImageMatchingRule) for the match
|
||||
* @return {Object} Matching Object with score and details (which rule passed or failed)
|
||||
*/
|
||||
const match = (metadataInstance, rules, customAttributeRetrievalCallbacks) => {
|
||||
const options = {
|
||||
format: 'grouped',
|
||||
};
|
||||
|
||||
const details = {
|
||||
passed: [],
|
||||
failed: [],
|
||||
};
|
||||
|
||||
let requiredFailed = false;
|
||||
let score = 0;
|
||||
|
||||
rules.forEach(rule => {
|
||||
const attribute = rule.attribute;
|
||||
|
||||
// Do not use the custom attribute from the metadataInstance since it is subject to change
|
||||
if (customAttributeRetrievalCallbacks.hasOwnProperty(attribute)) {
|
||||
const customAttribute = customAttributeRetrievalCallbacks[attribute];
|
||||
metadataInstance[attribute] = customAttribute.callback(metadataInstance);
|
||||
}
|
||||
|
||||
// Format the constraint as required by Validate.js
|
||||
const testConstraint = {
|
||||
[attribute]: rule.constraint,
|
||||
};
|
||||
|
||||
// Create a single attribute object to be validated, since metadataInstance is an
|
||||
// instance of Metadata (StudyMetadata, SeriesMetadata or InstanceMetadata)
|
||||
const attributeValue = metadataInstance[attribute];
|
||||
const attributeMap = {
|
||||
[attribute]: attributeValue,
|
||||
};
|
||||
|
||||
// Use Validate.js to evaluate the constraints on the specified metadataInstance
|
||||
let errorMessages;
|
||||
try {
|
||||
errorMessages = validate(attributeMap, testConstraint, [options]);
|
||||
} catch (e) {
|
||||
errorMessages = ['Something went wrong during validation.', e];
|
||||
}
|
||||
|
||||
if (!errorMessages) {
|
||||
// If no errorMessages were returned, then validation passed.
|
||||
|
||||
// Add the rule's weight to the total score
|
||||
score += parseInt(rule.weight, 10);
|
||||
|
||||
// Log that this rule passed in the matching details object
|
||||
details.passed.push({
|
||||
rule,
|
||||
});
|
||||
} else {
|
||||
// If errorMessages were present, then validation failed
|
||||
|
||||
// If the rule that failed validation was Required, then
|
||||
// mark that a required Rule has failed
|
||||
if (rule.required) {
|
||||
requiredFailed = true;
|
||||
}
|
||||
|
||||
// Log that this rule failed in the matching details object
|
||||
// and include any error messages
|
||||
details.failed.push({
|
||||
rule,
|
||||
errorMessages,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// If a required Rule has failed Validation, set the matching score to zero
|
||||
if (requiredFailed) {
|
||||
score = 0;
|
||||
}
|
||||
|
||||
return {
|
||||
score,
|
||||
details,
|
||||
requiredFailed,
|
||||
};
|
||||
};
|
||||
|
||||
const HPMatcher = {
|
||||
match,
|
||||
};
|
||||
|
||||
export { HPMatcher };
|
||||
@ -1,74 +1,476 @@
|
||||
const name = 'HangingProtocolService';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import pubSubServiceInterface from '../_shared/pubSubServiceInterface';
|
||||
import sortBy from '../../utils/sortBy.js';
|
||||
import ProtocolEngine from './ProtocolEngine';
|
||||
|
||||
const publicAPI = {
|
||||
name,
|
||||
getState: _getState,
|
||||
setHangingProtocol: _setHangingProtocol,
|
||||
setHangingProtocolAppliedForViewport: _setHangingProtocolAppliedForViewport,
|
||||
setServiceImplementation,
|
||||
reset: _reset,
|
||||
set: _set,
|
||||
const EVENTS = {
|
||||
STAGE_CHANGE: 'event::hanging_protocol_stage_change',
|
||||
NEW_LAYOUT: 'event::hanging_protocol_new_layout',
|
||||
};
|
||||
|
||||
const serviceImplementation = {
|
||||
_getState: () => console.warn('getState() NOT IMPLEMENTED'),
|
||||
_setHangingProtocol: () =>
|
||||
console.warn('_setHangingProtocol() NOT IMPLEMENTED'),
|
||||
_setHangingProtocolAppliedForViewport: () =>
|
||||
console.warn('_setHangingProtocolAppliedForViewport() NOT IMPLEMENTED'),
|
||||
_reset: () => console.warn('reset() NOT IMPLEMENTED'),
|
||||
_set: () => console.warn('set() NOT IMPLEMENTED'),
|
||||
const VIEWPORT_SETTING_TYPES = {
|
||||
PROPS: 'props',
|
||||
VIEWPORT: 'viewport',
|
||||
};
|
||||
|
||||
function _getState() {
|
||||
return serviceImplementation._getState();
|
||||
}
|
||||
|
||||
function _setHangingProtocol(hangingProtocol) {
|
||||
return serviceImplementation._setHangingProtocol(hangingProtocol);
|
||||
}
|
||||
|
||||
function _setHangingProtocolAppliedForViewport(hpAlreadyApplied) {
|
||||
return serviceImplementation._setHangingProtocolAppliedForViewport(
|
||||
hpAlreadyApplied
|
||||
);
|
||||
}
|
||||
|
||||
function _set(state) {
|
||||
return serviceImplementation._set(state);
|
||||
}
|
||||
|
||||
function _reset() {
|
||||
return serviceImplementation._reset({});
|
||||
}
|
||||
|
||||
function setServiceImplementation({
|
||||
getState: getStateImplementation,
|
||||
setHangingProtocol: setHangingProtocolImplementation,
|
||||
setHangingProtocolAppliedForViewport: setHangingProtocolAppliedForViewportImplementation,
|
||||
reset: resetImplementation,
|
||||
set: setImplementation,
|
||||
}) {
|
||||
if (getStateImplementation) {
|
||||
serviceImplementation._getState = getStateImplementation;
|
||||
class HangingProtocolService {
|
||||
constructor(commandsManager) {
|
||||
this._commandsManager = commandsManager;
|
||||
this.protocols = [];
|
||||
this.ProtocolEngine = undefined;
|
||||
this.protocol = undefined;
|
||||
this.stage = undefined;
|
||||
this.matchDetails = [];
|
||||
this.hpAlreadyApplied = [];
|
||||
this.studies = [];
|
||||
this.customViewportSettings = [];
|
||||
this.customAttributeRetrievalCallbacks = {};
|
||||
this.listeners = {};
|
||||
Object.defineProperty(this, 'EVENTS', {
|
||||
value: EVENTS,
|
||||
writable: false,
|
||||
enumerable: true,
|
||||
configurable: false,
|
||||
});
|
||||
Object.assign(this, pubSubServiceInterface);
|
||||
}
|
||||
if (setHangingProtocolImplementation) {
|
||||
serviceImplementation._setHangingProtocol = setHangingProtocolImplementation;
|
||||
|
||||
reset() {
|
||||
this.studies = [];
|
||||
this.protocols = [];
|
||||
this.hpAlreadyApplied = [];
|
||||
this.matchDetails = [];
|
||||
// this.ProtocolEngine.reset()
|
||||
}
|
||||
if (setHangingProtocolAppliedForViewportImplementation) {
|
||||
serviceImplementation._setHangingProtocolAppliedForViewport = setHangingProtocolAppliedForViewportImplementation;
|
||||
|
||||
getState() {
|
||||
return [this.matchDetails, this.hpAlreadyApplied];
|
||||
}
|
||||
if (resetImplementation) {
|
||||
serviceImplementation._reset = resetImplementation;
|
||||
|
||||
getProtocols() {
|
||||
return this.protocols;
|
||||
}
|
||||
if (setImplementation) {
|
||||
serviceImplementation._set = setImplementation;
|
||||
|
||||
addProtocols(protocols) {
|
||||
protocols.forEach(protocol => {
|
||||
if (this.protocols.indexOf(protocol) === -1) {
|
||||
this.protocols.push(protocol);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
run(studyMetaData, protocol) {
|
||||
if (!this.studies.includes(studyMetaData)) {
|
||||
this.studies.push(studyMetaData);
|
||||
}
|
||||
// copy here so we don't mutate it
|
||||
const metaData = Object.assign({}, studyMetaData);
|
||||
|
||||
this.ProtocolEngine = new ProtocolEngine(
|
||||
this.protocols,
|
||||
this.customAttributeRetrievalCallbacks
|
||||
);
|
||||
|
||||
// if there is no pre-defiend protocol
|
||||
if (!protocol || protocol.id === undefined) {
|
||||
const matchedProtocol = this.ProtocolEngine.run(metaData);
|
||||
this._setProtocol(matchedProtocol);
|
||||
return;
|
||||
}
|
||||
|
||||
this._setProtocol(protocol);
|
||||
}
|
||||
|
||||
setHangingProtocolAppliedForViewport(i) {
|
||||
this.hpAlreadyApplied[i] = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a custom setting that can be chosen in the HangingProtocol UI and applied to a Viewport
|
||||
*
|
||||
* @param settingId The ID used to refer to the setting (e.g. 'displayCADMarkers')
|
||||
* @param settingName The name of the setting to be displayed (e.g. 'Display CAD Markers')
|
||||
* @param options
|
||||
* @param callback A function to be run after a viewport is rendered with a series
|
||||
*/
|
||||
addCustomViewportSetting(...params) {
|
||||
this.customViewportSettings.push(...params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a custom attribute to be used in the HangingProtocol UI and matching rules, including a
|
||||
* callback that will be used to calculate the attribute value.
|
||||
*
|
||||
* @param attributeId The ID used to refer to the attribute (e.g. 'timepointType')
|
||||
* @param attributeName The name of the attribute to be displayed (e.g. 'Timepoint Type')
|
||||
* @param callback The function used to calculate the attribute value from the other attributes at its level (e.g. study/series/image)
|
||||
*/
|
||||
addCustomAttribute(attributeId, attributeName, callback) {
|
||||
this.customAttributeRetrievalCallbacks[attributeId] = {
|
||||
name: attributeName,
|
||||
callback: callback,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Switches to the next protocol stage in the display set sequence
|
||||
*/
|
||||
nextProtocolStage() {
|
||||
console.log('ProtocolEngine::nextProtocolStage');
|
||||
|
||||
if (!this._setCurrentProtocolStage(1)) {
|
||||
console.log('ProtocolEngine::nextProtocolStage failed');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Switches to the previous protocol stage in the display set sequence
|
||||
*/
|
||||
previousProtocolStage() {
|
||||
console.log('ProtocolEngine::previousProtocolStage');
|
||||
|
||||
if (!this._setCurrentProtocolStage(-1)) {
|
||||
console.log('ProtocolEngine::previousProtocolStage failed');
|
||||
}
|
||||
}
|
||||
|
||||
_setProtocol(protocol) {
|
||||
// TODO: Add proper Protocol class to validate the protocols
|
||||
// which are entered manually
|
||||
this.stage = 0;
|
||||
this.protocol = protocol;
|
||||
this._updateViewports(protocol);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the number of Stages in the current Protocol or
|
||||
* undefined if no protocol or stages are set
|
||||
*/
|
||||
_getNumProtocolStages() {
|
||||
if (
|
||||
!this.protocol ||
|
||||
!this.protocol.stages ||
|
||||
!this.protocol.stages.length
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
return this.protocol.stages.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the current Stage from the current Protocol and stage index
|
||||
*
|
||||
* @returns {*} The Stage model for the currently displayed Stage
|
||||
*/
|
||||
_getCurrentStageModel() {
|
||||
return this.protocol.stages[this.stage];
|
||||
}
|
||||
|
||||
_updateViewports() {
|
||||
// Make sure we have an active protocol with a non-empty array of display sets
|
||||
if (!this._getNumProtocolStages()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Retrieve the current stage
|
||||
const stageModel = this._getCurrentStageModel();
|
||||
|
||||
// If the current stage does not fulfill the requirements to be displayed,
|
||||
// stop here.
|
||||
if (
|
||||
!stageModel ||
|
||||
!stageModel.viewportStructure ||
|
||||
!stageModel.viewports ||
|
||||
!stageModel.viewports.length
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Retrieve the layoutTemplate associated with the current display set's viewport structure
|
||||
// If no such template name exists, stop here.
|
||||
// const layoutTemplateName = stageModel.viewportStructure.getLayoutTemplateName();
|
||||
const layoutTemplateName = 'gridLayout';
|
||||
if (!layoutTemplateName) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Retrieve the properties associated with the current display set's viewport structure template
|
||||
// If no such layout properties exist, stop here.
|
||||
const layoutProps = stageModel.viewportStructure.properties;
|
||||
if (!layoutProps) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { columns: numCols, rows: numRows } = layoutProps;
|
||||
this._broadcastChange(this.EVENTS.NEW_LAYOUT, {
|
||||
numRows,
|
||||
numCols,
|
||||
});
|
||||
|
||||
// Empty the matchDetails associated with the ProtocolEngine.
|
||||
// This will be used to store the pass/fail details and score
|
||||
// for each of the viewport matching procedures
|
||||
|
||||
// Loop through each viewport
|
||||
stageModel.viewports.forEach((viewport, viewportIndex) => {
|
||||
this.hpAlreadyApplied.push(false);
|
||||
const details = this._matchImages(viewport);
|
||||
|
||||
let currentMatch = details.bestMatch;
|
||||
|
||||
const currentViewportData = {
|
||||
viewportIndex,
|
||||
SeriesInstanceUID: currentMatch && currentMatch.SeriesInstanceUID,
|
||||
};
|
||||
|
||||
// Viewport Settings
|
||||
//
|
||||
// protocol defined callback
|
||||
const protocolCallbacks = viewport.viewportSettings.filter(
|
||||
setting => setting.type === VIEWPORT_SETTING_TYPES.PROPS
|
||||
);
|
||||
// manually added callback
|
||||
const customCallbacks = this.customViewportSettings.filter(
|
||||
setting => setting.type === VIEWPORT_SETTING_TYPES.PROPS
|
||||
);
|
||||
const callbacks = protocolCallbacks.concat(customCallbacks);
|
||||
|
||||
// if we have callbacks to applied at the app level or at the HP level
|
||||
if (callbacks.length) {
|
||||
currentViewportData.renderedCallback = (element, ToolBarService) => {
|
||||
callbacks.forEach(setting => {
|
||||
const { commandName, options } = setting;
|
||||
options.viewportIndex = viewportIndex;
|
||||
options.element = element;
|
||||
// Toolbar service to handle tool activation
|
||||
if (commandName === 'setToolActive') {
|
||||
ToolBarService.recordInteraction(options);
|
||||
return;
|
||||
}
|
||||
// other commands
|
||||
this._commandsManager.runCommand(commandName, options);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
// initial viewport settings defined by protocol
|
||||
const protocolInitialViewport = viewport.viewportSettings.filter(
|
||||
setting => setting.type === VIEWPORT_SETTING_TYPES.VIEWPORT
|
||||
);
|
||||
// custom added initial viewport settings
|
||||
const customInitialViewport = this.customViewportSettings.filter(
|
||||
setting => setting.type === VIEWPORT_SETTING_TYPES.VIEWPORT
|
||||
);
|
||||
// TODO: conflict might happen between protocol and custom viewport settings
|
||||
const viewportSettings = protocolInitialViewport.concat(
|
||||
customInitialViewport
|
||||
);
|
||||
|
||||
if (viewportSettings.length) {
|
||||
const initialViewport = {};
|
||||
viewportSettings.forEach(setting => {
|
||||
const { options } = setting;
|
||||
if (!options) return;
|
||||
// Do not manipulate the hp settings
|
||||
const viewportOptions = cloneDeep(options);
|
||||
Object.entries(viewportOptions).forEach(([key, value]) => {
|
||||
initialViewport[key] = value;
|
||||
});
|
||||
});
|
||||
currentViewportData.initialViewport = initialViewport;
|
||||
}
|
||||
|
||||
this.matchDetails[viewportIndex] = currentViewportData;
|
||||
});
|
||||
}
|
||||
|
||||
// Match images given a list of Studies and a Viewport's image matching reqs
|
||||
_matchImages(viewport) {
|
||||
console.log('ProtocolEngine::matchImages');
|
||||
|
||||
// TODO: matching is applied on study and series level, instance
|
||||
// level matching needs to be added in future
|
||||
|
||||
const { studyMatchingRules, seriesMatchingRules } = viewport;
|
||||
|
||||
const matchingScores = [];
|
||||
let highestStudyMatchingScore = 0;
|
||||
let highestSeriesMatchingScore = 0;
|
||||
|
||||
this.studies.forEach(study => {
|
||||
const studyMatchDetails = this.ProtocolEngine.findMatch(
|
||||
study,
|
||||
studyMatchingRules
|
||||
);
|
||||
|
||||
// Prevent bestMatch from being updated if the matchDetails' required attribute check has failed
|
||||
if (
|
||||
studyMatchDetails.requiredFailed === true ||
|
||||
studyMatchDetails.score < highestStudyMatchingScore
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
highestStudyMatchingScore = studyMatchDetails.score;
|
||||
|
||||
study.series.forEach(aSeries => {
|
||||
const seriesMatchDetails = this.ProtocolEngine.findMatch(
|
||||
aSeries,
|
||||
seriesMatchingRules
|
||||
);
|
||||
|
||||
// Prevent bestMatch from being updated if the matchDetails' required attribute check has failed
|
||||
if (
|
||||
seriesMatchDetails.requiredFailed === true ||
|
||||
seriesMatchDetails.score < highestSeriesMatchingScore
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
highestSeriesMatchingScore = seriesMatchDetails.score;
|
||||
|
||||
const matchDetails = {
|
||||
passed: [],
|
||||
failed: [],
|
||||
};
|
||||
|
||||
matchDetails.passed = matchDetails.passed.concat(
|
||||
seriesMatchDetails.details.passed
|
||||
);
|
||||
matchDetails.passed = matchDetails.passed.concat(
|
||||
studyMatchDetails.details.passed
|
||||
);
|
||||
|
||||
matchDetails.failed = matchDetails.failed.concat(
|
||||
seriesMatchDetails.details.failed
|
||||
);
|
||||
matchDetails.failed = matchDetails.failed.concat(
|
||||
studyMatchDetails.details.failed
|
||||
);
|
||||
|
||||
const totalMatchScore =
|
||||
seriesMatchDetails.score + studyMatchDetails.score;
|
||||
|
||||
const imageDetails = {
|
||||
StudyInstanceUID: study.StudyInstanceUID,
|
||||
SeriesInstanceUID: aSeries.SeriesInstanceUID,
|
||||
matchingScore: totalMatchScore,
|
||||
matchDetails: matchDetails,
|
||||
sortingInfo: {
|
||||
score: totalMatchScore,
|
||||
study: study.StudyInstanceUID,
|
||||
series: parseInt(aSeries.SeriesNumber),
|
||||
},
|
||||
};
|
||||
|
||||
matchingScores.push(imageDetails);
|
||||
});
|
||||
});
|
||||
|
||||
// Sort the matchingScores
|
||||
const sortingFunction = sortBy(
|
||||
{
|
||||
name: 'score',
|
||||
reverse: true,
|
||||
},
|
||||
{
|
||||
name: 'study',
|
||||
reverse: true,
|
||||
},
|
||||
{
|
||||
name: 'series',
|
||||
}
|
||||
);
|
||||
matchingScores.sort((a, b) =>
|
||||
sortingFunction(a.sortingInfo, b.sortingInfo)
|
||||
);
|
||||
|
||||
const bestMatch = matchingScores[0];
|
||||
|
||||
console.log('ProtocolEngine::matchImages bestMatch', bestMatch);
|
||||
|
||||
return {
|
||||
bestMatch,
|
||||
matchingScores,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the next stage is available
|
||||
* @return {Boolean} True if next stage is available or false otherwise
|
||||
*/
|
||||
_isNextStageAvailable() {
|
||||
const numberOfStages = this._getNumProtocolStages();
|
||||
|
||||
return this.stage + 1 < numberOfStages;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the previous stage is available
|
||||
* @return {Boolean} True if previous stage is available or false otherwise
|
||||
*/
|
||||
_isPreviousStageAvailable() {
|
||||
return this.stage - 1 >= 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the current stage to a new stage index in the display set sequence.
|
||||
* It checks if the next stage exists.
|
||||
*
|
||||
* @param {Integer} stageAction An integer value specifying wheater next (1) or previous (-1) stage
|
||||
* @return {Boolean} True if new stage has set or false, otherwise
|
||||
*/
|
||||
_setCurrentProtocolStage(stageAction) {
|
||||
//reseting the applied protocols
|
||||
this.hpAlreadyApplied = [];
|
||||
// Check if previous or next stage is available
|
||||
if (stageAction === -1 && !this._isPreviousStageAvailable()) {
|
||||
return false;
|
||||
} else if (stageAction === 1 && !this._isNextStageAvailable()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Sets the new stage
|
||||
this.stage += stageAction;
|
||||
|
||||
// Log the new stage
|
||||
console.log(
|
||||
`ProtocolEngine::setCurrentProtocolStage stage = ${this.stage}`
|
||||
);
|
||||
|
||||
// Since stage has changed, we need to update the viewports
|
||||
// and redo matchings
|
||||
this._updateViewports();
|
||||
|
||||
// Everything went well
|
||||
this._broadcastChange(this.EVENTS.STAGE_CHANGE, {
|
||||
matchDetails: this.matchDetails,
|
||||
hpAlreadyApplied: this.hpAlreadyApplied,
|
||||
});
|
||||
return true;
|
||||
}
|
||||
/**
|
||||
* Broadcasts hanging protocols changes.
|
||||
*
|
||||
* @param {string} eventName The event name.add
|
||||
* @param {object} eventData.source The measurement source.
|
||||
* @param {object} eventData.measurement The measurement.
|
||||
* @param {boolean} eventData.notYetUpdatedAtSource True if the measurement was edited
|
||||
* within the measurement service and the source needs to update.
|
||||
* @return void
|
||||
*/
|
||||
_broadcastChange(eventName, eventData) {
|
||||
const hasListeners = Object.keys(this.listeners).length > 0;
|
||||
const hasCallbacks = Array.isArray(this.listeners[eventName]);
|
||||
|
||||
if (hasListeners && hasCallbacks) {
|
||||
this.listeners[eventName].forEach(listener => {
|
||||
listener.callback(eventData);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
name,
|
||||
create: ({ configuration = {} }) => {
|
||||
return publicAPI;
|
||||
},
|
||||
};
|
||||
export default HangingProtocolService;
|
||||
export { EVENTS };
|
||||
|
||||
@ -0,0 +1,789 @@
|
||||
import { HPMatcher } from './HPMatcher.js';
|
||||
import { sortByScore } from './lib/sortByScore';
|
||||
|
||||
const deafultProtocol = {
|
||||
id: 'defaultProtocol',
|
||||
locked: true,
|
||||
hasUpdatedPriorsInformation: false,
|
||||
name: 'Default',
|
||||
createdDate: '2021-02-23T19:22:08.894Z',
|
||||
modifiedDate: '2021-02-23T19:22:08.894Z',
|
||||
availableTo: {},
|
||||
editableBy: {},
|
||||
protocolMatchingRules: [],
|
||||
stages: [
|
||||
{
|
||||
id: 'nwzau7jDkEkL8djfr',
|
||||
name: 'oneByOne',
|
||||
viewportStructure: {
|
||||
type: 'grid',
|
||||
properties: {
|
||||
rows: 1,
|
||||
columns: 1,
|
||||
},
|
||||
},
|
||||
viewports: [
|
||||
{
|
||||
viewportSettings: [],
|
||||
imageMatchingRules: [],
|
||||
seriesMatchingRules: [],
|
||||
studyMatchingRules: [],
|
||||
},
|
||||
],
|
||||
createdDate: '2021-02-23T19:22:08.894Z',
|
||||
},
|
||||
],
|
||||
numberOfPriorsReferenced: -1,
|
||||
};
|
||||
|
||||
export default class ProtocolEngine {
|
||||
constructor(protocols, customAttributeRetrievalCallbacks) {
|
||||
this.protocols = protocols;
|
||||
this.customAttributeRetrievalCallbacks = customAttributeRetrievalCallbacks;
|
||||
this.matchedProtocols = new Map();
|
||||
this.matchedProtocolScores = {};
|
||||
this.study = undefined;
|
||||
}
|
||||
|
||||
run(studyMetaData) {
|
||||
this.study = studyMetaData;
|
||||
return this.getBestProtocolMatch();
|
||||
}
|
||||
|
||||
// /**
|
||||
// * Resets the ProtocolEngine to the best match
|
||||
// */
|
||||
// reset() {
|
||||
// const protocol = this.getBestProtocolMatch();
|
||||
|
||||
// this.setHangingProtocol(protocol);
|
||||
// }
|
||||
|
||||
/**
|
||||
* Return the best matched Protocol to the current study or set of studies
|
||||
* @returns {*}
|
||||
*/
|
||||
getBestProtocolMatch() {
|
||||
// Run the matching to populate matchedProtocols Set and Map
|
||||
this.updateProtocolMatches();
|
||||
|
||||
// Retrieve the highest scoring Protocol
|
||||
const bestMatch = this._getHighestScoringProtocol();
|
||||
|
||||
console.log('ProtocolEngine::getBestProtocolMatch bestMatch', bestMatch);
|
||||
|
||||
return bestMatch;
|
||||
}
|
||||
|
||||
/**
|
||||
* Populates the MatchedProtocols Collection by running the matching procedure
|
||||
*/
|
||||
updateProtocolMatches() {
|
||||
console.log('ProtocolEngine::updateProtocolMatches');
|
||||
|
||||
// Clear all data currently in matchedProtocols
|
||||
this._clearMatchedProtocols();
|
||||
|
||||
// TODO: handle more than one study
|
||||
const study = this.study;
|
||||
const matched = this.findMatchByStudy(study);
|
||||
|
||||
// For each matched protocol, check if it is already in MatchedProtocols
|
||||
matched.forEach(matchedDetail => {
|
||||
const protocol = matchedDetail.protocol;
|
||||
if (!protocol) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If it is not already in the MatchedProtocols Collection, insert it with its score
|
||||
if (!this.matchedProtocols.has(protocol.id)) {
|
||||
console.log(
|
||||
'ProtocolEngine::updateProtocolMatches inserting protocol match',
|
||||
matchedDetail
|
||||
);
|
||||
this.matchedProtocols.set(protocol.id, protocol);
|
||||
this.matchedProtocolScores[protocol.id] = matchedDetail.score;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
findMatch(metaData, rules) {
|
||||
return HPMatcher.match(
|
||||
metaData,
|
||||
rules,
|
||||
this.customAttributeRetrievalCallbacks
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the best protocols from Protocol Store, matching each protocol matching rules
|
||||
* with the given study. The best protocol are orded by score and returned in an array
|
||||
* @param {Object} study StudyMetadata instance object
|
||||
* @return {Array} Array of match objects or an empty array if no match was found
|
||||
* Each match object has the score of the matching and the matched
|
||||
* protocol
|
||||
*/
|
||||
findMatchByStudy(study) {
|
||||
const matched = [];
|
||||
|
||||
this.protocols.forEach(protocol => {
|
||||
// Clone the protocol's protocolMatchingRules array
|
||||
// We clone it so that we don't accidentally add the
|
||||
// numberOfPriorsReferenced rule to the Protocol itself.
|
||||
let rules = protocol.protocolMatchingRules.slice();
|
||||
if (!rules) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Run the matcher and get matching details
|
||||
const matchedDetails = this.findMatch(study, rules);
|
||||
const score = matchedDetails.score;
|
||||
|
||||
// The protocol matched some rule, add it to the matched list
|
||||
if (score > 0) {
|
||||
matched.push({
|
||||
score,
|
||||
protocol,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// If no matches were found, select the default protocol
|
||||
if (!matched.length) {
|
||||
return [
|
||||
{
|
||||
score: 1,
|
||||
protocol: deafultProtocol,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
// Sort the matched list by score
|
||||
sortByScore(matched);
|
||||
|
||||
console.log('ProtocolEngine::findMatchByStudy matched', matched);
|
||||
|
||||
return matched;
|
||||
}
|
||||
|
||||
_clearMatchedProtocols() {
|
||||
this.matchedProtocols.clear();
|
||||
this.matchedProtocolScores = {};
|
||||
}
|
||||
|
||||
_largestKeyByValue(obj) {
|
||||
return Object.keys(obj).reduce((a, b) => (obj[a] > obj[b] ? a : b));
|
||||
}
|
||||
|
||||
_getHighestScoringProtocol() {
|
||||
if (!Object.keys(this.matchedProtocolScores).length) {
|
||||
return;
|
||||
}
|
||||
const highestScoringProtocolId = this._largestKeyByValue(
|
||||
this.matchedProtocolScores
|
||||
);
|
||||
return this.matchedProtocols.get(highestScoringProtocolId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the ProtocolEngine to the best match
|
||||
*/
|
||||
|
||||
/**
|
||||
* Retrieves the current Stage from the current Protocol and stage index
|
||||
*
|
||||
* @returns {*} The Stage model for the currently displayed Stage
|
||||
*/
|
||||
// getCurrentStageModel() {
|
||||
// return this.protocol.stages[this.stage];
|
||||
// }
|
||||
|
||||
// /**
|
||||
// * Get the number of prior studies supplied in the priorStudies map property.
|
||||
// *
|
||||
// * @param {String} studyObjectID The study object ID of the study whose priors are needed
|
||||
// * @returns {number} The number of available prior studies with the same PatientID
|
||||
// */
|
||||
// getNumberOfAvailablePriors(studyObjectID) {
|
||||
// return this.getAvailableStudyPriors(studyObjectID).length;
|
||||
// }
|
||||
|
||||
// /**
|
||||
// * Get the array of prior studies from a specific study.
|
||||
// *
|
||||
// * @param {String} studyObjectID The study object ID of the study whose priors are needed
|
||||
// * @returns {Array} The array of available priors or an empty array
|
||||
// */
|
||||
// getAvailableStudyPriors(studyObjectID) {
|
||||
// const priors = this.priorStudies.get(studyObjectID);
|
||||
|
||||
// return priors instanceof Array ? priors : [];
|
||||
// }
|
||||
|
||||
// // Match images given a list of Studies and a Viewport's image matching reqs
|
||||
// matchImages(viewport, viewportIndex) {
|
||||
// log.trace('ProtocolEngine::matchImages');
|
||||
|
||||
// const {
|
||||
// studyMatchingRules,
|
||||
// seriesMatchingRules,
|
||||
// imageMatchingRules: instanceMatchingRules,
|
||||
// } = viewport;
|
||||
|
||||
// const matchingScores = [];
|
||||
// const currentStudy = this.studies[0]; // @TODO: Should this be: this.studies[this.currentStudy] ???
|
||||
// const firstInstance = currentStudy.getFirstInstance();
|
||||
|
||||
// let highestStudyMatchingScore = 0;
|
||||
// let highestSeriesMatchingScore = 0;
|
||||
|
||||
// // Set custom attribute for study metadata and it's first instance
|
||||
// currentStudy.setCustomAttribute(ABSTRACT_PRIOR_VALUE, 0);
|
||||
// if (firstInstance instanceof InstanceMetadata) {
|
||||
// firstInstance.setCustomAttribute(ABSTRACT_PRIOR_VALUE, 0);
|
||||
// }
|
||||
|
||||
// // Only used if study matching rules has abstract prior values defined...
|
||||
// let priorStudies;
|
||||
|
||||
// studyMatchingRules.forEach(rule => {
|
||||
// if (rule.attribute === ABSTRACT_PRIOR_VALUE) {
|
||||
// const validatorType = Object.keys(rule.constraint)[0];
|
||||
// const validator = Object.keys(rule.constraint[validatorType])[0];
|
||||
|
||||
// let abstractPriorValue = rule.constraint[validatorType][validator];
|
||||
// abstractPriorValue = parseInt(abstractPriorValue, 10);
|
||||
// // TODO: Restrict or clarify validators for abstractPriorValue?
|
||||
|
||||
// // No need to call it more than once...
|
||||
// if (!priorStudies) {
|
||||
// priorStudies = this.getAvailableStudyPriors(
|
||||
// currentStudy.getObjectID()
|
||||
// );
|
||||
// }
|
||||
|
||||
// // TODO: Revisit this later: What about two studies with the same
|
||||
// // study date?
|
||||
|
||||
// let priorStudy;
|
||||
// if (abstractPriorValue === -1) {
|
||||
// priorStudy = priorStudies[priorStudies.length - 1];
|
||||
// } else {
|
||||
// const studyIndex = Math.max(abstractPriorValue - 1, 0);
|
||||
// priorStudy = priorStudies[studyIndex];
|
||||
// }
|
||||
|
||||
// // Invalid data
|
||||
// if (!priorStudy instanceof StudyMetadata) {
|
||||
// return;
|
||||
// }
|
||||
|
||||
// const priorStudyObjectID = priorStudy.getObjectID();
|
||||
|
||||
// // Check if study metadata is already in studies list
|
||||
// if (
|
||||
// this.studies.find(study => study.getObjectID() === priorStudyObjectID)
|
||||
// ) {
|
||||
// return;
|
||||
// }
|
||||
|
||||
// // Get study metadata if necessary and load study in the viewer (each viewer should provide it's own load study method)
|
||||
// this.studyMetadataSource.loadStudy(priorStudy).then(
|
||||
// studyMetadata => {
|
||||
// // Set the custom attribute abstractPriorValue for the study metadata
|
||||
// studyMetadata.setCustomAttribute(
|
||||
// ABSTRACT_PRIOR_VALUE,
|
||||
// abstractPriorValue
|
||||
// );
|
||||
|
||||
// // Also add custom attribute
|
||||
// const firstInstance = studyMetadata.getFirstInstance();
|
||||
// if (firstInstance instanceof InstanceMetadata) {
|
||||
// firstInstance.setCustomAttribute(
|
||||
// ABSTRACT_PRIOR_VALUE,
|
||||
// abstractPriorValue
|
||||
// );
|
||||
// }
|
||||
|
||||
// // Insert the new study metadata
|
||||
// this.studies.push(studyMetadata);
|
||||
|
||||
// // Update the viewport to refresh layout manager with new study
|
||||
// this.updateViewports(viewportIndex);
|
||||
// },
|
||||
// error => {
|
||||
// log.warn(error);
|
||||
// throw new OHIFError(
|
||||
// `ProtocolEngine::matchImages could not get study metadata for the Study with the following ObjectID: ${priorStudyObjectID}`
|
||||
// );
|
||||
// }
|
||||
// );
|
||||
// }
|
||||
// // TODO: Add relative Date / time
|
||||
// });
|
||||
|
||||
// this.studies.forEach(study => {
|
||||
// const studyMatchDetails = HPMatcher.match(
|
||||
// study.getFirstInstance(),
|
||||
// studyMatchingRules
|
||||
// );
|
||||
|
||||
// // Prevent bestMatch from being updated if the matchDetails' required attribute check has failed
|
||||
// if (
|
||||
// studyMatchDetails.requiredFailed === true ||
|
||||
// studyMatchDetails.score < highestStudyMatchingScore
|
||||
// ) {
|
||||
// return;
|
||||
// }
|
||||
|
||||
// highestStudyMatchingScore = studyMatchDetails.score;
|
||||
|
||||
// study.forEachSeries(series => {
|
||||
// const seriesMatchDetails = HPMatcher.match(
|
||||
// series.getFirstInstance(),
|
||||
// seriesMatchingRules
|
||||
// );
|
||||
|
||||
// // Prevent bestMatch from being updated if the matchDetails' required attribute check has failed
|
||||
// if (
|
||||
// seriesMatchDetails.requiredFailed === true ||
|
||||
// seriesMatchDetails.score < highestSeriesMatchingScore
|
||||
// ) {
|
||||
// return;
|
||||
// }
|
||||
|
||||
// highestSeriesMatchingScore = seriesMatchDetails.score;
|
||||
|
||||
// series.forEachInstance((instance, index) => {
|
||||
// // This tests to make sure there is actually image data in this instance
|
||||
// // TODO: Change this when we add PDF and MPEG support
|
||||
// // See https://ohiforg.atlassian.net/browse/LT-227
|
||||
// if (
|
||||
// !isImage(instance.getTagValue('SOPClassUID')) &&
|
||||
// !instance.getTagValue('Rows')
|
||||
// ) {
|
||||
// return;
|
||||
// }
|
||||
|
||||
// const instanceMatchDetails = HPMatcher.match(
|
||||
// instance,
|
||||
// instanceMatchingRules
|
||||
// );
|
||||
|
||||
// // Prevent bestMatch from being updated if the matchDetails' required attribute check has failed
|
||||
// if (instanceMatchDetails.requiredFailed === true) {
|
||||
// return;
|
||||
// }
|
||||
|
||||
// const matchDetails = {
|
||||
// passed: [],
|
||||
// failed: [],
|
||||
// };
|
||||
|
||||
// matchDetails.passed = matchDetails.passed.concat(
|
||||
// instanceMatchDetails.details.passed
|
||||
// );
|
||||
// matchDetails.passed = matchDetails.passed.concat(
|
||||
// seriesMatchDetails.details.passed
|
||||
// );
|
||||
// matchDetails.passed = matchDetails.passed.concat(
|
||||
// studyMatchDetails.details.passed
|
||||
// );
|
||||
|
||||
// matchDetails.failed = matchDetails.failed.concat(
|
||||
// instanceMatchDetails.details.failed
|
||||
// );
|
||||
// matchDetails.failed = matchDetails.failed.concat(
|
||||
// seriesMatchDetails.details.failed
|
||||
// );
|
||||
// matchDetails.failed = matchDetails.failed.concat(
|
||||
// studyMatchDetails.details.failed
|
||||
// );
|
||||
|
||||
// const totalMatchScore =
|
||||
// instanceMatchDetails.score +
|
||||
// seriesMatchDetails.score +
|
||||
// studyMatchDetails.score;
|
||||
// const currentSOPInstanceUID = instance.getSOPInstanceUID();
|
||||
|
||||
// const imageDetails = {
|
||||
// StudyInstanceUID: study.getStudyInstanceUID(),
|
||||
// SeriesInstanceUID: series.getSeriesInstanceUID(),
|
||||
// SOPInstanceUID: currentSOPInstanceUID,
|
||||
// currentImageIdIndex: index,
|
||||
// matchingScore: totalMatchScore,
|
||||
// matchDetails: matchDetails,
|
||||
// sortingInfo: {
|
||||
// score: totalMatchScore,
|
||||
// study:
|
||||
// instance.getTagValue('StudyDate') +
|
||||
// instance.getTagValue('StudyTime'),
|
||||
// series: parseInt(instance.getTagValue('SeriesNumber')), // TODO: change for seriesDateTime
|
||||
// instance: parseInt(instance.getTagValue('InstanceNumber')), // TODO: change for acquisitionTime
|
||||
// },
|
||||
// };
|
||||
|
||||
// // Find the displaySet
|
||||
// const displaySet = study.findDisplaySet(displaySet =>
|
||||
// displaySet.images.find(
|
||||
// image => image.getSOPInstanceUID() === currentSOPInstanceUID
|
||||
// )
|
||||
// );
|
||||
|
||||
// // If the instance was found, set the displaySet ID
|
||||
// if (displaySet) {
|
||||
// imageDetails.displaySetInstanceUID = displaySet.getUID();
|
||||
// imageDetails.imageId = instance.getImageId();
|
||||
// }
|
||||
|
||||
// matchingScores.push(imageDetails);
|
||||
// });
|
||||
// });
|
||||
// });
|
||||
|
||||
// // Sort the matchingScores
|
||||
// const sortingFunction = sortBy(
|
||||
// {
|
||||
// name: 'score',
|
||||
// reverse: true,
|
||||
// },
|
||||
// {
|
||||
// name: 'study',
|
||||
// reverse: true,
|
||||
// },
|
||||
// {
|
||||
// name: 'instance',
|
||||
// },
|
||||
// {
|
||||
// name: 'series',
|
||||
// }
|
||||
// );
|
||||
// matchingScores.sort((a, b) =>
|
||||
// sortingFunction(a.sortingInfo, b.sortingInfo)
|
||||
// );
|
||||
|
||||
// const bestMatch = matchingScores[0];
|
||||
|
||||
// log.trace('ProtocolEngine::matchImages bestMatch', bestMatch);
|
||||
|
||||
// return {
|
||||
// bestMatch,
|
||||
// matchingScores,
|
||||
// };
|
||||
// }
|
||||
|
||||
/**
|
||||
* Sets the current layout
|
||||
*
|
||||
* @param {number} numRows
|
||||
* @param {number} numColumns
|
||||
*/
|
||||
// setLayout(numRows, numColumns) {
|
||||
// if (numRows < 1 && numColumns < 1) {
|
||||
// log.error(`Invalid layout ${numRows} x ${numColumns}`);
|
||||
// return;
|
||||
// }
|
||||
|
||||
// if (typeof this.options.setLayout !== 'function') {
|
||||
// log.error('Hanging Protocol Engine setLayout callback is not defined');
|
||||
// return;
|
||||
// }
|
||||
|
||||
// let viewports = [];
|
||||
// const numViewports = numRows * numColumns;
|
||||
|
||||
// for (let i = 0; i < numViewports; i++) {
|
||||
// viewports.push({});
|
||||
// }
|
||||
|
||||
// this.options.setLayout({ numRows, numColumns, viewports });
|
||||
// }
|
||||
|
||||
// /**
|
||||
// * Rerenders viewports that are part of the current layout manager
|
||||
// * using the matching rules internal to each viewport.
|
||||
// *
|
||||
// * If this function is provided the index of a viewport, only the specified viewport
|
||||
// * is rerendered.
|
||||
// *
|
||||
// * @param viewportIndex
|
||||
// */
|
||||
// updateViewports(viewportIndex) {
|
||||
// log.trace(
|
||||
// `ProtocolEngine::updateViewports viewportIndex: ${viewportIndex}`
|
||||
// );
|
||||
|
||||
// // Make sure we have an active protocol with a non-empty array of display sets
|
||||
// if (!this.getNumProtocolStages()) {
|
||||
// return;
|
||||
// }
|
||||
|
||||
// // Retrieve the current stage
|
||||
// const stageModel = this.getCurrentStageModel();
|
||||
|
||||
// // If the current stage does not fulfill the requirements to be displayed,
|
||||
// // stop here.
|
||||
// if (
|
||||
// !stageModel ||
|
||||
// !stageModel.viewportStructure ||
|
||||
// !stageModel.viewports ||
|
||||
// !stageModel.viewports.length
|
||||
// ) {
|
||||
// return;
|
||||
// }
|
||||
|
||||
// // Retrieve the layoutTemplate associated with the current display set's viewport structure
|
||||
// // If no such template name exists, stop here.
|
||||
// const layoutTemplateName = stageModel.viewportStructure.getLayoutTemplateName();
|
||||
// if (!layoutTemplateName) {
|
||||
// return;
|
||||
// }
|
||||
|
||||
// // Retrieve the properties associated with the current display set's viewport structure template
|
||||
// // If no such layout properties exist, stop here.
|
||||
// const layoutProps = stageModel.viewportStructure.properties;
|
||||
// if (!layoutProps) {
|
||||
// return;
|
||||
// }
|
||||
|
||||
// // Create an empty array to store the output viewportData
|
||||
// const viewportData = [];
|
||||
|
||||
// // Empty the matchDetails associated with the ProtocolEngine.
|
||||
// // This will be used to store the pass/fail details and score
|
||||
// // for each of the viewport matching procedures
|
||||
// this.matchDetails = [];
|
||||
|
||||
// // Loop through each viewport
|
||||
// stageModel.viewports.forEach((viewport, viewportIndex) => {
|
||||
// const details = this.matchImages(viewport, viewportIndex);
|
||||
|
||||
// this.matchDetails[viewportIndex] = details;
|
||||
|
||||
// // Convert any YES/NO values into true/false for Cornerstone
|
||||
// const cornerstoneViewportParams = {};
|
||||
|
||||
// // Cache viewportSettings keys
|
||||
// const viewportSettingsKeys = Object.keys(viewport.viewportSettings);
|
||||
|
||||
// viewportSettingsKeys.forEach(key => {
|
||||
// let value = viewport.viewportSettings[key];
|
||||
// if (value === 'YES') {
|
||||
// value = true;
|
||||
// } else if (value === 'NO') {
|
||||
// value = false;
|
||||
// }
|
||||
|
||||
// cornerstoneViewportParams[key] = value;
|
||||
// });
|
||||
|
||||
// // imageViewerViewports occasionally needs relevant layout data in order to set
|
||||
// // the element style of the viewport in question
|
||||
// const currentViewportData = {
|
||||
// viewportIndex,
|
||||
// viewport: cornerstoneViewportParams,
|
||||
// ...layoutProps,
|
||||
// };
|
||||
|
||||
// const customSettings = [];
|
||||
// viewportSettingsKeys.forEach(id => {
|
||||
// const setting = CustomViewportSettings[id];
|
||||
// if (!setting) {
|
||||
// return;
|
||||
// }
|
||||
|
||||
// customSettings.push({
|
||||
// id: id,
|
||||
// value: viewport.viewportSettings[id],
|
||||
// });
|
||||
// });
|
||||
|
||||
// currentViewportData.renderedCallback = element => {
|
||||
// //console.log('renderedCallback for ' + element.id);
|
||||
// customSettings.forEach(customSetting => {
|
||||
// log.trace(
|
||||
// `ProtocolEngine::currentViewportData.renderedCallback Applying custom setting: ${customSetting.id}`
|
||||
// );
|
||||
// log.trace(
|
||||
// `ProtocolEngine::currentViewportData.renderedCallback with value: ${customSetting.value}`
|
||||
// );
|
||||
|
||||
// const setting = CustomViewportSettings[customSetting.id];
|
||||
// setting.callback(element, customSetting.value);
|
||||
// });
|
||||
// };
|
||||
|
||||
// let currentMatch = details.bestMatch;
|
||||
// let currentPosition = 1;
|
||||
// const scoresLength = details.matchingScores.length;
|
||||
// while (
|
||||
// currentPosition < scoresLength &&
|
||||
// viewportData.find(a => a.imageId === currentMatch.imageId)
|
||||
// ) {
|
||||
// currentMatch = details.matchingScores[currentPosition];
|
||||
// currentPosition++;
|
||||
// }
|
||||
|
||||
// if (currentMatch && currentMatch.imageId) {
|
||||
// currentViewportData.StudyInstanceUID = currentMatch.StudyInstanceUID;
|
||||
// currentViewportData.SeriesInstanceUID = currentMatch.SeriesInstanceUID;
|
||||
// currentViewportData.SOPInstanceUID = currentMatch.SOPInstanceUID;
|
||||
// currentViewportData.currentImageIdIndex =
|
||||
// currentMatch.currentImageIdIndex;
|
||||
// currentViewportData.displaySetInstanceUID =
|
||||
// currentMatch.displaySetInstanceUID;
|
||||
// currentViewportData.imageId = currentMatch.imageId;
|
||||
// }
|
||||
|
||||
// // @TODO Why should we throw an exception when a best match is not found? This was aborting the whole process.
|
||||
// // if (!currentViewportData.displaySetInstanceUID) {
|
||||
// // throw new OHIFError('ProtocolEngine::updateViewports No matching display set found?');
|
||||
// // }
|
||||
|
||||
// viewportData.push(currentViewportData);
|
||||
// });
|
||||
|
||||
// this.setLayout(layoutProps.Rows, layoutProps.Columns);
|
||||
|
||||
// if (typeof this.options.setViewportSpecificData !== 'function') {
|
||||
// log.error(
|
||||
// 'Hanging Protocol Engine setViewportSpecificData callback is not defined'
|
||||
// );
|
||||
// return;
|
||||
// }
|
||||
|
||||
// // If viewportIndex is defined, then update only that viewport
|
||||
// if (viewportIndex !== undefined && viewportData[viewportIndex]) {
|
||||
// this.options.setViewportSpecificData(
|
||||
// viewportIndex,
|
||||
// viewportData[viewportIndex]
|
||||
// );
|
||||
// return;
|
||||
// }
|
||||
|
||||
// // Update all viewports
|
||||
// viewportData.forEach(viewportSpecificData => {
|
||||
// this.options.setViewportSpecificData(
|
||||
// viewportSpecificData.viewportIndex,
|
||||
// viewportSpecificData
|
||||
// );
|
||||
// });
|
||||
// }
|
||||
|
||||
// /**
|
||||
// * Sets the current Hanging Protocol to the specified Protocol
|
||||
// * An optional argument can also be used to prevent the updating of the Viewports
|
||||
// *
|
||||
// * @param newProtocol
|
||||
// * @param updateViewports
|
||||
// */
|
||||
// setHangingProtocol(newProtocol, updateViewports = true) {
|
||||
// log.trace('ProtocolEngine::setHangingProtocol newProtocol', newProtocol);
|
||||
// log.trace(
|
||||
// `ProtocolEngine::setHangingProtocol updateViewports = ${updateViewports}`
|
||||
// );
|
||||
|
||||
// // Reset the array of newStageIds
|
||||
// this.newStageIds = [];
|
||||
|
||||
// if (Protocol.prototype.isPrototypeOf(newProtocol)) {
|
||||
// this.protocol = newProtocol;
|
||||
// } else {
|
||||
// this.protocol = new Protocol();
|
||||
// this.protocol.fromObject(newProtocol);
|
||||
// }
|
||||
|
||||
// this.stage = 0;
|
||||
|
||||
// // Update viewports by default
|
||||
// if (updateViewports) {
|
||||
// this.updateViewports();
|
||||
// }
|
||||
// }
|
||||
|
||||
// /**
|
||||
// * Check if the next stage is available
|
||||
// * @return {Boolean} True if next stage is available or false otherwise
|
||||
// */
|
||||
// isNextStageAvailable() {
|
||||
// const numberOfStages = this.getNumProtocolStages();
|
||||
|
||||
// return this.stage + 1 < numberOfStages;
|
||||
// }
|
||||
|
||||
// /**
|
||||
// * Check if the previous stage is available
|
||||
// * @return {Boolean} True if previous stage is available or false otherwise
|
||||
// */
|
||||
// isPreviousStageAvailable() {
|
||||
// return this.stage - 1 >= 0;
|
||||
// }
|
||||
|
||||
// /**
|
||||
// * Changes the current stage to a new stage index in the display set sequence.
|
||||
// * It checks if the next stage exists.
|
||||
// *
|
||||
// * @param {Integer} stageAction An integer value specifying wheater next (1) or previous (-1) stage
|
||||
// * @return {Boolean} True if new stage has set or false, otherwise
|
||||
// */
|
||||
// setCurrentProtocolStage(stageAction) {
|
||||
// // Check if previous or next stage is available
|
||||
// if (stageAction === -1 && !this.isPreviousStageAvailable()) {
|
||||
// return false;
|
||||
// } else if (stageAction === 1 && !this.isNextStageAvailable()) {
|
||||
// return false;
|
||||
// }
|
||||
|
||||
// // Sets the new stage
|
||||
// this.stage += stageAction;
|
||||
|
||||
// // Log the new stage
|
||||
// log.trace(`ProtocolEngine::setCurrentProtocolStage stage = ${this.stage}`);
|
||||
|
||||
// // Since stage has changed, we need to update the viewports
|
||||
// // and redo matchings
|
||||
// this.updateViewports();
|
||||
|
||||
// // Everything went well
|
||||
// return true;
|
||||
// }
|
||||
|
||||
// /**
|
||||
// * Retrieves the number of Stages in the current Protocol or
|
||||
// * undefined if no protocol or stages are set
|
||||
// */
|
||||
// getNumProtocolStages() {
|
||||
// if (
|
||||
// !this.protocol ||
|
||||
// !this.protocol.stages ||
|
||||
// !this.protocol.stages.length
|
||||
// ) {
|
||||
// return;
|
||||
// }
|
||||
|
||||
// return this.protocol.stages.length;
|
||||
// }
|
||||
|
||||
// /**
|
||||
// * Switches to the next protocol stage in the display set sequence
|
||||
// */
|
||||
// nextProtocolStage() {
|
||||
// log.trace('ProtocolEngine::nextProtocolStage');
|
||||
|
||||
// if (!this.setCurrentProtocolStage(1)) {
|
||||
// log.trace('ProtocolEngine::nextProtocolStage failed');
|
||||
// }
|
||||
// }
|
||||
|
||||
// /**
|
||||
// * Switches to the previous protocol stage in the display set sequence
|
||||
// */
|
||||
// previousProtocolStage() {
|
||||
// log.trace('ProtocolEngine::previousProtocolStage');
|
||||
|
||||
// if (!this.setCurrentProtocolStage(-1)) {
|
||||
// log.trace('ProtocolEngine::previousProtocolStage failed');
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
@ -1,3 +1,8 @@
|
||||
import HangingProtocolService from './HangingProtocolService';
|
||||
|
||||
export default HangingProtocolService;
|
||||
export default {
|
||||
name: 'HangingProtocolService',
|
||||
create: ({ configuration = {}, commandsManager }) => {
|
||||
return new HangingProtocolService(commandsManager);
|
||||
},
|
||||
};
|
||||
|
||||
@ -0,0 +1,98 @@
|
||||
const comparators = [
|
||||
{
|
||||
id: 'equals',
|
||||
name: '= (Equals)',
|
||||
validator: 'equals',
|
||||
validatorOption: 'value',
|
||||
description: 'The attribute must equal this value.',
|
||||
},
|
||||
{
|
||||
id: 'doesNotEqual',
|
||||
name: '!= (Does not equal)',
|
||||
validator: 'doesNotEqual',
|
||||
validatorOption: 'value',
|
||||
description: 'The attribute must not equal this value.',
|
||||
},
|
||||
{
|
||||
id: 'contains',
|
||||
name: 'Contains',
|
||||
validator: 'contains',
|
||||
validatorOption: 'value',
|
||||
description: 'The attribute must contain this value.',
|
||||
},
|
||||
{
|
||||
id: 'doesNotContain',
|
||||
name: 'Does not contain',
|
||||
validator: 'doesNotContain',
|
||||
validatorOption: 'value',
|
||||
description: 'The attribute must not contain this value.',
|
||||
},
|
||||
{
|
||||
id: 'startsWith',
|
||||
name: 'Starts with',
|
||||
validator: 'startsWith',
|
||||
validatorOption: 'value',
|
||||
description: 'The attribute must start with this value.',
|
||||
},
|
||||
{
|
||||
id: 'endsWith',
|
||||
name: 'Ends with',
|
||||
validator: 'endsWith',
|
||||
validatorOption: 'value',
|
||||
description: 'The attribute must end with this value.',
|
||||
},
|
||||
{
|
||||
id: 'onlyInteger',
|
||||
name: 'Only Integers',
|
||||
validator: 'numericality',
|
||||
validatorOption: 'onlyInteger',
|
||||
description: "Real numbers won't be allowed.",
|
||||
},
|
||||
{
|
||||
id: 'greaterThan',
|
||||
name: '> (Greater than)',
|
||||
validator: 'numericality',
|
||||
validatorOption: 'greaterThan',
|
||||
description: 'The attribute has to be greater than this value.',
|
||||
},
|
||||
{
|
||||
id: 'greaterThanOrEqualTo',
|
||||
name: '>= (Greater than or equal to)',
|
||||
validator: 'numericality',
|
||||
validatorOption: 'greaterThanOrEqualTo',
|
||||
description: 'The attribute has to be at least this value.',
|
||||
},
|
||||
{
|
||||
id: 'lessThanOrEqualTo',
|
||||
name: '<= (Less than or equal to)',
|
||||
validator: 'numericality',
|
||||
validatorOption: 'lessThanOrEqualTo',
|
||||
description: 'The attribute can be this value at the most.',
|
||||
},
|
||||
{
|
||||
id: 'lessThan',
|
||||
name: '< (Less than)',
|
||||
validator: 'numericality',
|
||||
validatorOption: 'lessThan',
|
||||
description: 'The attribute has to be less than this value.',
|
||||
},
|
||||
{
|
||||
id: 'odd',
|
||||
name: 'Odd',
|
||||
validator: 'numericality',
|
||||
validatorOption: 'odd',
|
||||
description: 'The attribute has to be odd.',
|
||||
},
|
||||
{
|
||||
id: 'even',
|
||||
name: 'Even',
|
||||
validator: 'numericality',
|
||||
validatorOption: 'even',
|
||||
description: 'The attribute has to be even.',
|
||||
},
|
||||
];
|
||||
|
||||
// Immutable object
|
||||
Object.freeze(comparators);
|
||||
|
||||
export { comparators };
|
||||
@ -0,0 +1,71 @@
|
||||
const attributeCache = Object.create(null);
|
||||
const REGEXP = /^\([x0-9a-f]+\)/;
|
||||
|
||||
const humanize = text => {
|
||||
let humanized = text.replace(/([A-Z])/g, ' $1'); // insert a space before all caps
|
||||
|
||||
humanized = humanized.replace(/^./, str => {
|
||||
// uppercase the first character
|
||||
return str.toUpperCase();
|
||||
});
|
||||
|
||||
return humanized;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the text of an attribute for a given attribute
|
||||
* @param {String} attributeId The attribute ID
|
||||
* @param {Array} attributes Array of attributes objects with id and text properties
|
||||
* @return {String} If found return the attribute text or an empty string otherwise
|
||||
*/
|
||||
const getAttributeText = (attributeId, attributes) => {
|
||||
// If the attribute is already in the cache, return it
|
||||
if (attributeId in attributeCache) {
|
||||
return attributeCache[attributeId];
|
||||
}
|
||||
|
||||
// Find the attribute with given attributeId
|
||||
const attribute = attributes.find(attribute => attribute.id === attributeId);
|
||||
|
||||
let attributeText;
|
||||
|
||||
// If attribute was found get its text and save it on the cache
|
||||
if (attribute) {
|
||||
attributeText = attribute.text.replace(REGEXP, '');
|
||||
attributeCache[attributeId] = attributeText;
|
||||
}
|
||||
|
||||
return attributeText || '';
|
||||
};
|
||||
|
||||
function displayConstraint(attributeId, constraint, attributes) {
|
||||
if (!constraint || !attributeId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const validatorType = Object.keys(constraint)[0];
|
||||
if (!validatorType) {
|
||||
return;
|
||||
}
|
||||
|
||||
const validator = Object.keys(constraint[validatorType])[0];
|
||||
if (!validator) {
|
||||
return;
|
||||
}
|
||||
|
||||
const value = constraint[validatorType][validator];
|
||||
if (value === void 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
let comparator = validator;
|
||||
if (validator === 'value') {
|
||||
comparator = validatorType;
|
||||
}
|
||||
|
||||
const attributeText = getAttributeText(attributeId, attributes);
|
||||
const constraintText =
|
||||
attributeText + ' ' + humanize(comparator).toLowerCase() + ' ' + value;
|
||||
|
||||
return constraintText;
|
||||
}
|
||||
@ -0,0 +1,32 @@
|
||||
/**
|
||||
* Removes the first instance of an element from an array, if an equal value exists
|
||||
*
|
||||
* @param array
|
||||
* @param input
|
||||
*
|
||||
* @returns {boolean} Whether or not the element was found and removed
|
||||
*/
|
||||
const removeFromArray = (array, input) => {
|
||||
// If the array is empty, stop here
|
||||
if (!array || !array.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
array.forEach((value, index) => {
|
||||
// TODO: Double check whether or not this deep equality check is necessary
|
||||
//if (_.isEqual(value, input)) {
|
||||
if (value === input) {
|
||||
indexToRemove = index;
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
if (indexToRemove === void 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
array.splice(indexToRemove, 1);
|
||||
return true;
|
||||
};
|
||||
|
||||
export { removeFromArray };
|
||||
@ -0,0 +1,8 @@
|
||||
// Sorts an array by score
|
||||
const sortByScore = arr => {
|
||||
arr.sort((a, b) => {
|
||||
return b.score - a.score;
|
||||
});
|
||||
};
|
||||
|
||||
export { sortByScore };
|
||||
@ -0,0 +1,39 @@
|
||||
import validate from 'validate.js';
|
||||
|
||||
validate.validators.equals = function (value, options, key, attributes) {
|
||||
if (options && value !== options.value) {
|
||||
return key + 'must equal ' + options.value;
|
||||
}
|
||||
};
|
||||
|
||||
validate.validators.doesNotEqual = function (value, options, key) {
|
||||
if (options && value === options.value) {
|
||||
return key + 'cannot equal ' + options.value;
|
||||
}
|
||||
};
|
||||
|
||||
validate.validators.contains = function (value, options, key) {
|
||||
if (options && value.indexOf && value.indexOf(options.value) === -1) {
|
||||
return key + 'must contain ' + options.value;
|
||||
}
|
||||
};
|
||||
|
||||
validate.validators.doesNotContain = function (value, options, key) {
|
||||
if (options && value.indexOf && value.indexOf(options.value) !== -1) {
|
||||
return key + 'cannot contain ' + options.value;
|
||||
}
|
||||
};
|
||||
|
||||
validate.validators.startsWith = function (value, options, key) {
|
||||
if (options && value.startsWith && !value.startsWith(options.value)) {
|
||||
return key + 'must start with ' + options.value;
|
||||
}
|
||||
};
|
||||
|
||||
validate.validators.endsWith = function (value, options, key) {
|
||||
if (options && value.endsWith && !value.endsWith(options.value)) {
|
||||
return key + 'must end with ' + options.value;
|
||||
}
|
||||
};
|
||||
|
||||
export default validate;
|
||||
@ -54,6 +54,7 @@ const EVENTS = {
|
||||
MEASUREMENT_UPDATED: 'event::measurement_updated',
|
||||
INTERNAL_MEASUREMENT_UPDATED: 'event:internal_measurement_updated',
|
||||
MEASUREMENT_ADDED: 'event::measurement_added',
|
||||
RAW_MEASUREMENT_ADDED: 'event::raw_measurement_added',
|
||||
MEASUREMENT_REMOVED: 'event::measurement_removed',
|
||||
MEASUREMENTS_CLEARED: 'event::measurements_cleared',
|
||||
JUMP_TO_MEASUREMENT: 'event:jump_to_measurement',
|
||||
@ -314,11 +315,17 @@ class MeasurementService {
|
||||
* Converted to/from annotation in the same way. E.g. import serialized data
|
||||
* Of the same form as the measurement source.
|
||||
* @param {MeasurementSource} source The measurement source instance.
|
||||
* @param {string} definition The source definition you want to add the measuremnet to.
|
||||
* @param {string} definition The source definition you want to add the measurement to.
|
||||
* @param {object} data The data you wish to add to the source.
|
||||
* @param {function} toMeasurementSchema A function to get the `data` into the same shape as the source definition.
|
||||
*/
|
||||
addRawMeasurement(source, definition, data, toMeasurementSchema) {
|
||||
addRawMeasurement(
|
||||
source,
|
||||
definition,
|
||||
data,
|
||||
toMeasurementSchema,
|
||||
dataSource = {}
|
||||
) {
|
||||
if (!this._isValidSource(source)) {
|
||||
log.warn('Invalid source. Exiting early.');
|
||||
return;
|
||||
@ -342,7 +349,6 @@ class MeasurementService {
|
||||
try {
|
||||
/* Convert measurement */
|
||||
measurement = toMeasurementSchema(data);
|
||||
|
||||
/* Assign measurement source instance */
|
||||
measurement.source = source;
|
||||
} catch (error) {
|
||||
@ -385,9 +391,11 @@ class MeasurementService {
|
||||
} else {
|
||||
log.info(`Measurement added.`, newMeasurement);
|
||||
this.measurements[internalId] = newMeasurement;
|
||||
this._broadcastEvent(this.EVENTS.MEASUREMENT_ADDED, {
|
||||
this._broadcastEvent(this.EVENTS.RAW_MEASUREMENT_ADDED, {
|
||||
source,
|
||||
measurement: newMeasurement,
|
||||
data,
|
||||
dataSource,
|
||||
});
|
||||
}
|
||||
|
||||
@ -501,6 +509,7 @@ class MeasurementService {
|
||||
|
||||
clearMeasurements() {
|
||||
this.measurements = {};
|
||||
this._jumpToMeasurementCache = {};
|
||||
this._broadcastEvent(this.EVENTS.MEASUREMENTS_CLEARED);
|
||||
}
|
||||
|
||||
|
||||
@ -13,6 +13,7 @@ export default class ToolBarService {
|
||||
this.EVENTS = EVENTS;
|
||||
this.listeners = {};
|
||||
this.buttons = {};
|
||||
this.unsubscriptions = []; // if tools need to unsubscribe from events
|
||||
this.buttonSections = {
|
||||
/**
|
||||
* primary: ['Zoom', 'Wwwc'],
|
||||
@ -39,6 +40,16 @@ export default class ToolBarService {
|
||||
this.extensionManager = extensionManager;
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.unsubscriptions.forEach(unsub => unsub());
|
||||
this.state = {
|
||||
primaryToolId: 'Wwwc',
|
||||
toggles: {},
|
||||
groups: {},
|
||||
};
|
||||
this.unsubscriptions = [];
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {*} interaction
|
||||
@ -63,6 +74,7 @@ export default class ToolBarService {
|
||||
this.state.toggles[itemId] === undefined
|
||||
? true
|
||||
: !this.state.toggles[itemId];
|
||||
interaction.commandOptions.toggledState = this.state.toggles[itemId];
|
||||
break;
|
||||
}
|
||||
default:
|
||||
@ -73,13 +85,21 @@ export default class ToolBarService {
|
||||
//
|
||||
// NOTE: Should probably just do this for tools as well?
|
||||
// But would be nice if we could enforce at least the command name?
|
||||
let unsubscribe;
|
||||
if (interaction.commandName) {
|
||||
commandsManager.runCommand(
|
||||
unsubscribe = commandsManager.runCommand(
|
||||
interaction.commandName,
|
||||
interaction.commandOptions
|
||||
);
|
||||
}
|
||||
|
||||
// Storing the unsubscribe for later reseting
|
||||
if (unsubscribe && typeof unsubscribe === 'function') {
|
||||
if (this.unsubscriptions.indexOf(unsubscribe) === -1) {
|
||||
this.unsubscriptions.push(unsubscribe);
|
||||
}
|
||||
}
|
||||
|
||||
// Track last touched id for each group
|
||||
if (groupId) {
|
||||
this.state.groups[groupId] = itemId;
|
||||
|
||||
@ -10,6 +10,7 @@ import ToolBarService from './ToolBarService';
|
||||
import ViewportGridService from './ViewportGridService';
|
||||
import CineService from './CineService';
|
||||
import HangingProtocolService from './HangingProtocolService';
|
||||
import pubSubServiceInterface from './_shared/pubSubServiceInterface';
|
||||
|
||||
export {
|
||||
MeasurementService,
|
||||
@ -23,5 +24,6 @@ export {
|
||||
ToolBarService,
|
||||
ViewportGridService,
|
||||
HangingProtocolService,
|
||||
CineService
|
||||
CineService,
|
||||
pubSubServiceInterface,
|
||||
};
|
||||
|
||||
@ -22,6 +22,7 @@ function createAndAddStack(
|
||||
StudyInstanceUID,
|
||||
frameRate,
|
||||
isClip,
|
||||
initialImageIdIndex,
|
||||
} = displaySet;
|
||||
if (!images) {
|
||||
return;
|
||||
@ -35,6 +36,7 @@ function createAndAddStack(
|
||||
imageIds,
|
||||
frameRate,
|
||||
isClip,
|
||||
initialImageIdIndex,
|
||||
};
|
||||
|
||||
stackMap[displaySetInstanceUID] = stack;
|
||||
|
||||
@ -2,15 +2,16 @@
|
||||
* Sorting function
|
||||
* Sorts an array by seriesDate and seriesNumber if equal
|
||||
*/
|
||||
const sortBySeriesDate = array => array.sort((a, b) => {
|
||||
if (a.seriesNumber !== b.seriesNumber) {
|
||||
return a.seriesNumber - b.seriesNumber;
|
||||
}
|
||||
const sortBySeriesDate = array =>
|
||||
array.sort((a, b) => {
|
||||
if (a.seriesNumber !== b.seriesNumber) {
|
||||
return a.seriesNumber - b.seriesNumber;
|
||||
}
|
||||
|
||||
const seriesDateA = Date.parse(a.seriesDate);
|
||||
const seriesDateB = Date.parse(b.seriesDate);
|
||||
const seriesDateA = Date.parse(a.seriesDate);
|
||||
const seriesDateB = Date.parse(b.seriesDate);
|
||||
|
||||
return seriesDateA - seriesDateB;
|
||||
});
|
||||
return seriesDateA - seriesDateB;
|
||||
});
|
||||
|
||||
export default sortBySeriesDate;
|
||||
|
||||
@ -25,9 +25,6 @@ export {
|
||||
ViewportGridContext,
|
||||
ViewportGridProvider,
|
||||
useViewportGrid,
|
||||
HangingProtocolContext,
|
||||
HangingProtocolProvider,
|
||||
useHangingProtocol,
|
||||
} from './src/contextProviders';
|
||||
|
||||
/** COMPONENTS */
|
||||
|
||||
@ -6,7 +6,7 @@ import classNames from 'classnames';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { NavBar, Svg, Icon, IconButton, Dropdown } from '@ohif/ui';
|
||||
|
||||
function Header({ children, menuOptions, isReturnEnabled, isSticky }) {
|
||||
function Header({ children, menuOptions, isReturnEnabled, isSticky, WhiteLabeling }) {
|
||||
const { t } = useTranslation();
|
||||
const history = useHistory();
|
||||
|
||||
@ -18,6 +18,10 @@ function Header({ children, menuOptions, isReturnEnabled, isSticky }) {
|
||||
}
|
||||
};
|
||||
|
||||
const CustomLogo = (React) => {
|
||||
return WhiteLabeling.createLogoComponentFn(React)
|
||||
}
|
||||
|
||||
return (
|
||||
<NavBar className='justify-between border-b-4 border-black' isSticky={isSticky}>
|
||||
<div className="flex justify-between flex-1">
|
||||
@ -29,7 +33,7 @@ function Header({ children, menuOptions, isReturnEnabled, isSticky }) {
|
||||
onClick={onReturnHandler}
|
||||
>
|
||||
{isReturnEnabled && <Icon name="chevron-left" className="w-8 text-primary-active" />}
|
||||
<div className="ml-4"><Svg name="logo-ohif" /></div>
|
||||
<div className="ml-4">{WhiteLabeling ? CustomLogo(React) : <Svg name="logo-ohif" />}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center">{children}</div>
|
||||
@ -73,7 +77,8 @@ Header.propTypes = {
|
||||
),
|
||||
children: PropTypes.oneOfType([PropTypes.node, PropTypes.func]),
|
||||
isReturnEnabled: PropTypes.bool,
|
||||
isSticky: PropTypes.bool
|
||||
isSticky: PropTypes.bool,
|
||||
WhiteLabeling: PropTypes.element,
|
||||
};
|
||||
|
||||
Header.defaultProps = {
|
||||
|
||||
@ -143,7 +143,7 @@ const SidePanel = ({
|
||||
</span>
|
||||
</Button>
|
||||
</div>
|
||||
<childComponent.content/>
|
||||
<childComponent.content />
|
||||
</React.Fragment>
|
||||
) : (
|
||||
<React.Fragment>{getPanelButtons()}</React.Fragment>
|
||||
|
||||
@ -14,6 +14,7 @@ const classes = {
|
||||
const ViewportActionBar = ({
|
||||
studyData,
|
||||
showNavArrows,
|
||||
showStatus,
|
||||
showCine,
|
||||
cineProps,
|
||||
showPatientInfo: patientInfoVisibility,
|
||||
@ -255,7 +256,7 @@ const ViewportActionBar = ({
|
||||
<div className="flex flex-1 flex-grow mt-2 min-w-48">
|
||||
<div className="flex items-center">
|
||||
<span className="mr-2 text-white text-large">{label}</span>
|
||||
{renderIconStatus()}
|
||||
{showStatus && renderIconStatus()}
|
||||
</div>
|
||||
<div className="flex flex-col justify-start ml-4">
|
||||
<div className="flex">
|
||||
@ -352,6 +353,7 @@ ViewportActionBar.propTypes = {
|
||||
ViewportActionBar.defaultProps = {
|
||||
cineProps: {},
|
||||
showCine: false,
|
||||
showStatus: true,
|
||||
showNavArrows: true,
|
||||
showPatientInfo: false,
|
||||
};
|
||||
|
||||
@ -1,153 +0,0 @@
|
||||
import React, {
|
||||
createContext,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useReducer,
|
||||
} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const DEFAULT_STATE = {
|
||||
hangingProtocol: null,
|
||||
hpAlreadyApplied: {},
|
||||
};
|
||||
|
||||
export const HangingProtocolContext = createContext(DEFAULT_STATE);
|
||||
|
||||
export function HangingProtocolProvider({ children, service }) {
|
||||
const hangingProtocolReducer = (state, action) => {
|
||||
switch (action.type) {
|
||||
case 'SET_HANGING_PROTOCOL': {
|
||||
return {
|
||||
...state,
|
||||
...{ hangingProtocol: action.payload.hangingProtocol },
|
||||
};
|
||||
}
|
||||
case 'SET_HANGING_PROTOCOL_APPLIED_FOR_VIEWPORT': {
|
||||
const index = action.payload.index;
|
||||
const newHPAlreadyApplied = Object.assign({}, state.hpAlreadyApplied);
|
||||
|
||||
newHPAlreadyApplied[index] = true;
|
||||
|
||||
return {
|
||||
...state,
|
||||
...{ hpAlreadyApplied: newHPAlreadyApplied },
|
||||
};
|
||||
}
|
||||
case 'RESET': {
|
||||
return {
|
||||
hangingProtocol: null,
|
||||
hpAlreadyApplied: {},
|
||||
};
|
||||
}
|
||||
|
||||
case 'SET': {
|
||||
return {
|
||||
...state,
|
||||
...action.payload,
|
||||
};
|
||||
}
|
||||
|
||||
default:
|
||||
return action.payload;
|
||||
}
|
||||
};
|
||||
|
||||
const [hangingProtocolState, dispatch] = useReducer(
|
||||
hangingProtocolReducer,
|
||||
DEFAULT_STATE
|
||||
);
|
||||
|
||||
console.log('hangingProtocolState', hangingProtocolState);
|
||||
|
||||
const getState = useCallback(() => hangingProtocolState, [
|
||||
hangingProtocolState,
|
||||
]);
|
||||
|
||||
const setHangingProtocol = useCallback(
|
||||
hangingProtocol =>
|
||||
dispatch({
|
||||
type: 'SET_HANGING_PROTOCOL',
|
||||
payload: {
|
||||
hangingProtocol,
|
||||
},
|
||||
}),
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const setHangingProtocolAppliedForViewport = useCallback(
|
||||
index =>
|
||||
dispatch({
|
||||
type: 'SET_HANGING_PROTOCOL_APPLIED_FOR_VIEWPORT',
|
||||
payload: {
|
||||
index,
|
||||
},
|
||||
}),
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const reset = useCallback(
|
||||
() =>
|
||||
dispatch({
|
||||
type: 'RESET',
|
||||
payload: {},
|
||||
}),
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const set = useCallback(
|
||||
payload =>
|
||||
dispatch({
|
||||
type: 'SET',
|
||||
payload,
|
||||
}),
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
/**
|
||||
* Sets the implementation of the HangingProtocolService that can be used by extensions.
|
||||
*
|
||||
* @returns void
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (service) {
|
||||
service.setServiceImplementation({
|
||||
getState,
|
||||
setHangingProtocol,
|
||||
setHangingProtocolAppliedForViewport,
|
||||
reset,
|
||||
set,
|
||||
});
|
||||
}
|
||||
}, [
|
||||
getState,
|
||||
service,
|
||||
setHangingProtocol,
|
||||
setHangingProtocolAppliedForViewport,
|
||||
reset,
|
||||
set,
|
||||
]);
|
||||
|
||||
const api = {
|
||||
// getState,
|
||||
setHangingProtocol,
|
||||
setHangingProtocolAppliedForViewport,
|
||||
reset,
|
||||
set,
|
||||
};
|
||||
|
||||
return (
|
||||
<HangingProtocolContext.Provider value={[hangingProtocolState, api]}>
|
||||
{children}
|
||||
</HangingProtocolContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
HangingProtocolProvider.propTypes = {
|
||||
children: PropTypes.any,
|
||||
service: PropTypes.shape({
|
||||
setServiceImplementation: PropTypes.func,
|
||||
}).isRequired,
|
||||
};
|
||||
|
||||
export const useHangingProtocol = () => useContext(HangingProtocolContext);
|
||||
@ -1,4 +1,4 @@
|
||||
import React, { createContext, useContext, useReducer } from 'react';
|
||||
import React, { createContext, useContext, useReducer, useMemo } from 'react';
|
||||
|
||||
// export const IMAGE_VIEWER_DEFAULT_VALUE = {
|
||||
// StudyInstanceUIDs: [],
|
||||
@ -7,9 +7,18 @@ import React, { createContext, useContext, useReducer } from 'react';
|
||||
|
||||
export const ImageViewerContext = createContext();
|
||||
|
||||
export function ImageViewerProvider({ reducer, initialState, children }) {
|
||||
export function ImageViewerProvider({
|
||||
StudyInstanceUIDs,
|
||||
reducer,
|
||||
initialState,
|
||||
children,
|
||||
}) {
|
||||
const value = useMemo(() => {
|
||||
return { StudyInstanceUIDs };
|
||||
}, [StudyInstanceUIDs]);
|
||||
|
||||
return (
|
||||
<ImageViewerContext.Provider value={useReducer(reducer, initialState)}>
|
||||
<ImageViewerContext.Provider value={value}>
|
||||
{children}
|
||||
</ImageViewerContext.Provider>
|
||||
);
|
||||
|
||||
@ -8,8 +8,10 @@ import React, {
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const DEFAULT_STATE = {
|
||||
numRows: 1,
|
||||
numCols: 1,
|
||||
// starting from null, hanging
|
||||
// protocol will defined number of rows and cols
|
||||
numRows: null,
|
||||
numCols: null,
|
||||
viewports: [
|
||||
/*
|
||||
* {
|
||||
@ -58,12 +60,14 @@ export function ViewportGridProvider({ children, service }) {
|
||||
}
|
||||
case 'RESET': {
|
||||
return {
|
||||
numCols: 1,
|
||||
numRows: 1,
|
||||
numCols: null,
|
||||
numRows: null,
|
||||
activeViewportIndex: 0,
|
||||
viewports: [{
|
||||
displaySetInstanceUID: null,
|
||||
}],
|
||||
viewports: [
|
||||
{
|
||||
displaySetInstanceUID: null,
|
||||
},
|
||||
],
|
||||
cachedLayout: null,
|
||||
};
|
||||
}
|
||||
|
||||
@ -41,9 +41,3 @@ export {
|
||||
ViewportGridProvider,
|
||||
useViewportGrid,
|
||||
} from './ViewportGridProvider';
|
||||
|
||||
export {
|
||||
HangingProtocolContext,
|
||||
HangingProtocolProvider,
|
||||
useHangingProtocol,
|
||||
} from './HangingProtocolProvider';
|
||||
|
||||
@ -10,9 +10,9 @@ services:
|
||||
ohif_viewer:
|
||||
build:
|
||||
# Project root
|
||||
context: ./../../
|
||||
context: ./../../../../
|
||||
# Relative to context
|
||||
dockerfile: ./docker/OpenResty-Orthanc/dockerfile
|
||||
dockerfile: ./platform/viewer/.recipes/OpenResty-Orthanc/dockerfile
|
||||
image: webapp:latest
|
||||
container_name: webapp
|
||||
volumes:
|
||||
|
||||
@ -23,20 +23,39 @@
|
||||
|
||||
|
||||
# Stage 1: Build the application
|
||||
FROM node:11.2.0-slim as builder
|
||||
FROM node:12.22.1-slim as builder
|
||||
|
||||
RUN mkdir /usr/src/app
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
# Copy Files
|
||||
COPY .docker /usr/src/app/.docker
|
||||
COPY .webpack /usr/src/app/.webpack
|
||||
COPY extensions /usr/src/app/extensions
|
||||
COPY modes /usr/src/app/modes
|
||||
COPY platform /usr/src/app/platform
|
||||
COPY .browserslistrc /usr/src/app/.browserslistrc
|
||||
COPY aliases.config.js /usr/src/app/aliases.config.js
|
||||
COPY babel.config.js /usr/src/app/babel.config.js
|
||||
COPY lerna.json /usr/src/app/lerna.json
|
||||
COPY package.json /usr/src/app/package.json
|
||||
COPY postcss.config.js /usr/src/app/postcss.config.js
|
||||
COPY yarn.lock /usr/src/app/yarn.lock
|
||||
|
||||
# ADD . /usr/src/app/
|
||||
RUN yarn config set workspaces-experimental true
|
||||
RUN yarn install
|
||||
|
||||
ENV APP_CONFIG=config/docker_openresty-orthanc.js
|
||||
ENV PATH /usr/src/app/node_modules/.bin:$PATH
|
||||
|
||||
COPY package.json /usr/src/app/package.json
|
||||
COPY yarn.lock /usr/src/app/yarn.lock
|
||||
ENV QUICK_BUILD true
|
||||
RUN yarn run build
|
||||
|
||||
# ADD . /usr/src/app/
|
||||
# RUN yarn install
|
||||
# RUN yarn run build:web
|
||||
|
||||
ADD . /usr/src/app/
|
||||
RUN yarn install
|
||||
RUN yarn run build:web
|
||||
|
||||
# Stage 2: Bundle the built application into a Docker container
|
||||
# which runs openresty (nginx) using Alpine Linux
|
||||
@ -60,6 +79,6 @@ RUN luarocks install lua-resty-openidc
|
||||
RUN luarocks install luacrypto
|
||||
|
||||
# Copy build output to image
|
||||
COPY --from=builder /usr/src/app/build /var/www/html
|
||||
COPY --from=builder /usr/src/app/platform/viewer/dist /var/www/html
|
||||
|
||||
ENTRYPOINT ["/usr/local/openresty/nginx/sbin/nginx", "-g", "daemon off;"]
|
||||
|
||||
21
platform/viewer/cypress/plugins/index.js
Normal file
21
platform/viewer/cypress/plugins/index.js
Normal file
@ -0,0 +1,21 @@
|
||||
/// <reference types="cypress" />
|
||||
// ***********************************************************
|
||||
// This example plugins/index.js can be used to load plugins
|
||||
//
|
||||
// You can change the location of this file or turn off loading
|
||||
// the plugins file with the 'pluginsFile' configuration option.
|
||||
//
|
||||
// You can read more here:
|
||||
// https://on.cypress.io/plugins-guide
|
||||
// ***********************************************************
|
||||
|
||||
// This function is called when a project is opened or re-opened (e.g. due to
|
||||
// the project's config changing)
|
||||
|
||||
/**
|
||||
* @type {Cypress.PluginConfig}
|
||||
*/
|
||||
module.exports = (on, config) => {
|
||||
// `on` is used to hook into various events Cypress emits
|
||||
// `config` is the resolved Cypress config
|
||||
}
|
||||
@ -55,7 +55,7 @@
|
||||
"@types/react": "^16.0.0",
|
||||
"classnames": "^2.2.6",
|
||||
"core-js": "^3.2.1",
|
||||
"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",
|
||||
|
||||
@ -25,5 +25,24 @@ window.config = {
|
||||
},
|
||||
},
|
||||
],
|
||||
// whiteLabeling: {
|
||||
// /* Optional: Should return a React component to be rendered in the "Logo" section of the application's Top Navigation bar */
|
||||
// createLogoComponentFn: function (React) {
|
||||
// return React.createElement(
|
||||
// 'a',
|
||||
// {
|
||||
// target: '_self',
|
||||
// rel: 'noopener noreferrer',
|
||||
// className: 'text-purple-600 line-through',
|
||||
// href: '/',
|
||||
// },
|
||||
// React.createElement('img',
|
||||
// {
|
||||
// src: './customLogo.svg',
|
||||
// className: 'w-8 h-8',
|
||||
// }
|
||||
// ))
|
||||
// },
|
||||
// },
|
||||
defaultDataSourceName: 'dicomweb',
|
||||
};
|
||||
|
||||
@ -1,9 +1,14 @@
|
||||
window.config = {
|
||||
routerBasename: '/',
|
||||
showStudyList: true,
|
||||
servers: {
|
||||
dicomWeb: [
|
||||
{
|
||||
extensions: [],
|
||||
modes: [],
|
||||
dataSources: [
|
||||
{
|
||||
friendlyName: 'Orthanc Server',
|
||||
namespace: 'org.ohif.default.dataSourcesModule.dicomweb',
|
||||
sourceName: 'dicomweb',
|
||||
configuration: {
|
||||
name: 'Orthanc',
|
||||
wadoUriRoot: '/wado',
|
||||
qidoRoot: '/dicom-web',
|
||||
@ -12,6 +17,7 @@ window.config = {
|
||||
imageRendering: 'wadors',
|
||||
thumbnailRendering: 'wadors',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
defaultDataSourceName: 'dicomweb',
|
||||
};
|
||||
|
||||
@ -1,10 +1,14 @@
|
||||
window.config = {
|
||||
routerBasename: '/',
|
||||
showStudyList: true,
|
||||
servers: {
|
||||
// This is an array, but we'll only use the first entry for now
|
||||
dicomWeb: [
|
||||
{
|
||||
extensions: [],
|
||||
modes: [],
|
||||
dataSources: [
|
||||
{
|
||||
friendlyName: 'Orthanc Server',
|
||||
namespace: 'org.ohif.default.dataSourcesModule.dicomweb',
|
||||
sourceName: 'dicomweb',
|
||||
configuration: {
|
||||
name: 'Orthanc',
|
||||
wadoUriRoot: 'http://127.0.0.1/pacs/wado',
|
||||
qidoRoot: 'http://127.0.0.1/pacs/dicom-web',
|
||||
@ -12,11 +16,8 @@ window.config = {
|
||||
qidoSupportsIncludeField: false,
|
||||
imageRendering: 'wadors',
|
||||
thumbnailRendering: 'wadors',
|
||||
// requestOptions: {
|
||||
// undefined to use JWT + Bearer auth
|
||||
// auth: 'orthanc:orthanc',
|
||||
// },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
defaultDataSourceName: 'dicomweb',
|
||||
};
|
||||
|
||||
@ -1,11 +1,14 @@
|
||||
window.config = {
|
||||
// default: '/'
|
||||
routerBasename: '/',
|
||||
// default: ''
|
||||
showStudyList: true,
|
||||
servers: {
|
||||
dicomWeb: [
|
||||
{
|
||||
extensions: [],
|
||||
modes: [],
|
||||
dataSources: [
|
||||
{
|
||||
friendlyName: 'DCM4CHEE Server',
|
||||
namespace: 'org.ohif.default.dataSourcesModule.dicomweb',
|
||||
sourceName: 'dicomweb',
|
||||
configuration: {
|
||||
name: 'DCM4CHEE',
|
||||
wadoUriRoot: 'http://localhost:8080/dcm4chee-arc/aets/DCM4CHEE/wado',
|
||||
qidoRoot: 'http://localhost:8080/dcm4chee-arc/aets/DCM4CHEE/rs',
|
||||
@ -17,7 +20,8 @@ window.config = {
|
||||
auth: 'admin:admin',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
studyListFunctionsEnabled: true,
|
||||
defaultDataSourceName: 'dicomweb',
|
||||
};
|
||||
|
||||
65
platform/viewer/public/customLogo.svg
Normal file
65
platform/viewer/public/customLogo.svg
Normal file
@ -0,0 +1,65 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="265" height="24" viewBox="0 0 265 24">
|
||||
<g fill="none" fill-rule="evenodd">
|
||||
<g>
|
||||
<path fill="#FFF" d="M2.431 8.257c.733.734 1.582 1.12 2.547 1.12.964 0 1.813-.348 2.546-1.12.733-.733 1.08-1.736 1.08-3.01 0-1.273-.385-2.276-1.118-3.01-.733-.732-1.582-1.08-2.547-1.08-.965 0-1.814.348-2.547 1.08-.733.734-1.119 1.737-1.119 3.01.077 1.274.425 2.316 1.158 3.01m7.563-3.01c0 1.583-.463 2.856-1.428 3.82-.965.965-2.122 1.467-3.55 1.467-1.427 0-2.624-.502-3.588-1.505C.463 8.026 0 6.753 0 5.21c0-1.543.502-2.817 1.466-3.781C2.431.463 3.627 0 5.016 0c1.35 0 2.547.463 3.511 1.428.965.964 1.467 2.238 1.467 3.82" transform="translate(1 1) translate(33.803 4.985)"/>
|
||||
<path stroke="#FFF" stroke-width=".75" d="M2.431 8.257c.733.734 1.582 1.12 2.547 1.12.964 0 1.813-.348 2.546-1.12.733-.733 1.08-1.736 1.08-3.01 0-1.273-.385-2.276-1.118-3.01-.733-.732-1.582-1.08-2.547-1.08-.965 0-1.814.348-2.547 1.08-.733.734-1.119 1.737-1.119 3.01.077 1.274.425 2.316 1.158 3.01zm7.563-3.01c0 1.583-.463 2.856-1.428 3.82-.965.965-2.122 1.467-3.55 1.467-1.427 0-2.624-.502-3.588-1.505C.463 8.026 0 6.753 0 5.21c0-1.543.502-2.817 1.466-3.781C2.431.463 3.627 0 5.016 0c1.35 0 2.547.463 3.511 1.428.965.964 1.467 2.238 1.467 3.82z" transform="translate(1 1) translate(33.803 4.985)"/>
|
||||
<path fill="#FFF" d="M13.35 6.945c0 .85.232 1.467.657 1.93.424.463.964.694 1.582.694.617 0 1.119-.231 1.543-.733.425-.501.618-1.119.618-1.929s-.193-1.428-.618-1.89c-.424-.464-.926-.734-1.543-.734-.618 0-1.158.231-1.582.695-.425.463-.656 1.119-.656 1.967m.964 6.715h-2.893c-.116 0-.193-.04-.193-.155v-.617c0-.116.077-.155.193-.155h.81V4.322h-.772c-.116 0-.193-.039-.193-.155V3.55c0-.116.077-.154.193-.154h1.698c.116 0 .193.038.193.154v1.119c.193-.386.463-.733.887-1.003.425-.27.926-.425 1.505-.425.965 0 1.737.347 2.354 1.003.617.656.887 1.544.887 2.624s-.308 1.968-.887 2.663c-.579.694-1.35 1.003-2.354 1.003-.579 0-1.08-.154-1.505-.424-.386-.27-.694-.618-.849-1.004h-.038c.038.31.038.54.038.656v2.933h.926c.116 0 .193.038.193.154v.618c0 .154-.077.193-.193.193" transform="translate(1 1) translate(33.803 4.985)"/>
|
||||
<path stroke="#FFF" stroke-width=".75" d="M13.35 6.945c0 .85.232 1.467.657 1.93.424.463.964.694 1.582.694.617 0 1.119-.231 1.543-.733.425-.501.618-1.119.618-1.929s-.193-1.428-.618-1.89c-.424-.464-.926-.734-1.543-.734-.618 0-1.158.231-1.582.695-.425.463-.656 1.119-.656 1.967zm.965 6.715h-2.893c-.116 0-.193-.04-.193-.155v-.617c0-.116.077-.155.193-.155h.81V4.322h-.772c-.116 0-.193-.039-.193-.155V3.55c0-.116.077-.154.193-.154h1.698c.116 0 .193.038.193.154v1.119c.193-.386.463-.733.887-1.003.425-.27.926-.425 1.505-.425.965 0 1.737.347 2.354 1.003.617.656.887 1.544.887 2.624s-.308 1.968-.887 2.663c-.579.694-1.35 1.003-2.354 1.003-.579 0-1.08-.154-1.505-.424-.386-.27-.694-.618-.849-1.004h-.038c.038.31.038.54.038.656v2.933h.926c.116 0 .193.038.193.154v.618c0 .154-.077.193-.193.193z" transform="translate(1 1) translate(33.803 4.985)"/>
|
||||
<path fill="#FFF" d="M24.811 4.746c-.347-.347-.81-.54-1.39-.54-.578 0-1.08.154-1.465.501-.425.31-.656.81-.734 1.467h4.168c-.039-.618-.232-1.08-.579-1.428m1.582 2.315h-5.17c.038.81.308 1.39.771 1.852.463.464 1.003.656 1.582.656.579 0 1.08-.115 1.505-.386.424-.231.694-.501.849-.733.077-.077.154-.115.27-.038l.386.386c.039.038.077.115.077.154 0 .039-.038.077-.077.154s-.116.155-.27.309c-.155.154-.309.309-.579.502-.27.192-.579.347-.965.463-.386.115-.81.193-1.234.193-1.004 0-1.853-.348-2.509-1.004-.656-.656-1.003-1.543-1.003-2.662s.347-2.007 1.003-2.662c.656-.656 1.505-.965 2.47-.965.965 0 1.736.309 2.276.926.54.617.85 1.466.85 2.547v.115c-.04.116-.116.193-.232.193" transform="translate(1 1) translate(33.803 4.985)"/>
|
||||
<path stroke="#FFF" stroke-width=".75" d="M24.811 4.746c-.347-.347-.81-.54-1.39-.54-.578 0-1.08.154-1.465.501-.425.31-.656.81-.734 1.467h4.168c-.039-.618-.232-1.08-.579-1.428zm1.582 2.315h-5.17c.038.81.308 1.39.771 1.852.463.464 1.003.656 1.582.656.579 0 1.08-.115 1.505-.386.424-.231.694-.501.849-.733.077-.077.154-.115.27-.038l.386.386c.039.038.077.115.077.154 0 .039-.038.077-.077.154s-.116.155-.27.309c-.155.154-.309.309-.579.502-.27.192-.579.347-.965.463-.386.115-.81.193-1.234.193-1.004 0-1.853-.348-2.509-1.004-.656-.656-1.003-1.543-1.003-2.662s.347-2.007 1.003-2.662c.656-.656 1.505-.965 2.47-.965.965 0 1.736.309 2.276.926.54.617.85 1.466.85 2.547v.115c-.04.116-.116.193-.232.193z" transform="translate(1 1) translate(33.803 4.985)"/>
|
||||
<path fill="#FFF" d="M35.345 10.457h-1.35c-.425 0-.618-.193-.618-.618V6.058c0-.54-.154-1.003-.386-1.312-.27-.309-.656-.463-1.157-.463-.733 0-1.312.309-1.737.926-.038.039-.116.155-.193.309v3.936h.772c.116 0 .193.038.193.154v.617c0 .116-.077.155-.193.155h-2.74c-.115 0-.192-.039-.192-.155v-.617c0-.116.077-.154.192-.154h.772V4.322h-.772c-.115 0-.192-.039-.192-.155V3.55c0-.116.077-.154.192-.154h1.698c.116 0 .193.038.193.154l-.038.965h.038c.502-.85 1.274-1.235 2.315-1.235.772 0 1.35.231 1.814.694.463.463.656 1.042.656 1.814v3.704h.81c.116 0 .193.039.193.155v.617c-.077.116-.116.193-.27.193" transform="translate(1 1) translate(33.803 4.985)"/>
|
||||
<path stroke="#FFF" stroke-width=".75" d="M35.345 10.457h-1.35c-.425 0-.618-.193-.618-.618V6.058c0-.54-.154-1.003-.386-1.312-.27-.309-.656-.463-1.157-.463-.733 0-1.312.309-1.737.926-.038.039-.116.155-.193.309v3.936h.772c.116 0 .193.038.193.154v.617c0 .116-.077.155-.193.155h-2.74c-.115 0-.192-.039-.192-.155v-.617c0-.116.077-.154.192-.154h.772V4.322h-.772c-.115 0-.192-.039-.192-.155V3.55c0-.116.077-.154.192-.154h1.698c.116 0 .193.038.193.154l-.038.965h.038c.502-.85 1.274-1.235 2.315-1.235.772 0 1.35.231 1.814.694.463.463.656 1.042.656 1.814v3.704h.81c.116 0 .193.039.193.155v.617c-.077.116-.116.193-.27.193z" transform="translate(1 1) translate(33.803 4.985)"/>
|
||||
<path fill="#FFF" d="M51.513 10.457H48.04c-.116 0-.193-.039-.193-.155V9.57c0-.115.039-.154.193-.154h1.119V5.904h-5.364v3.511h1.12c.115 0 .192.039.192.154v.733c0 .116-.077.155-.193.155H41.48c-.116 0-.193-.039-.193-.155V9.57c0-.115.077-.154.193-.154h1.08V1.196h-1.08c-.116 0-.193-.038-.193-.154V.309c0-.116.077-.155.193-.155h3.434c.116 0 .193.039.193.155v.733c0 .116-.077.154-.193.154h-1.119v3.589h5.364V1.196h-1.12c-.115 0-.192-.038-.192-.154V.309c0-.116.039-.155.193-.155h3.473c.115 0 .192.039.192.155v.733c0 .116-.038.154-.192.154h-1.12v8.18h1.12c.115 0 .192.039.192.155v.733c0 .116-.077.193-.192.193" transform="translate(1 1) translate(33.803 4.985)"/>
|
||||
<path stroke="#FFF" stroke-width=".75" d="M51.513 10.457H48.04c-.116 0-.193-.039-.193-.155V9.57c0-.115.039-.154.193-.154h1.119V5.904h-5.364v3.511h1.12c.115 0 .192.039.192.154v.733c0 .116-.077.155-.193.155H41.48c-.116 0-.193-.039-.193-.155V9.57c0-.115.077-.154.193-.154h1.08V1.196h-1.08c-.116 0-.193-.038-.193-.154V.309c0-.116.077-.155.193-.155h3.434c.116 0 .193.039.193.155v.733c0 .116-.077.154-.193.154h-1.119v3.589h5.364V1.196h-1.12c-.115 0-.192-.038-.192-.154V.309c0-.116.039-.155.193-.155h3.473c.115 0 .192.039.192.155v.733c0 .116-.038.154-.192.154h-1.12v8.18h1.12c.115 0 .192.039.192.155v.733c0 .116-.077.193-.192.193z" transform="translate(1 1) translate(33.803 4.985)"/>
|
||||
<path fill="#FFF" d="M57.34 4.746c-.348-.347-.811-.54-1.39-.54-.579 0-1.08.154-1.466.501-.425.31-.656.81-.733 1.467h4.167c-.039-.618-.232-1.08-.579-1.428m1.544 2.315h-5.171c.039.81.309 1.39.772 1.852.463.425 1.003.656 1.582.656.578 0 1.08-.115 1.505-.386.424-.231.694-.501.849-.733.077-.077.154-.115.27-.038l.385.386c.039.038.078.115.078.154 0 .039-.039.077-.078.154-.038.077-.115.155-.27.309-.154.154-.308.309-.578.502-.27.192-.58.347-.965.463-.386.115-.81.193-1.235.193-1.003 0-1.852-.348-2.508-1.004-.656-.656-1.003-1.543-1.003-2.662s.347-2.007 1.003-2.662c.656-.656 1.505-.965 2.47-.965.964 0 1.736.309 2.276.926.54.617.85 1.466.85 2.547v.115c-.04.116-.078.193-.232.193" transform="translate(1 1) translate(33.803 4.985)"/>
|
||||
<path stroke="#FFF" stroke-width=".75" d="M57.34 4.746c-.348-.347-.811-.54-1.39-.54-.579 0-1.08.154-1.466.501-.425.31-.656.81-.733 1.467h4.167c-.039-.618-.232-1.08-.579-1.428zm1.543 2.315h-5.171c.039.81.309 1.39.772 1.852.463.425 1.003.656 1.582.656.578 0 1.08-.115 1.505-.386.424-.231.694-.501.849-.733.077-.077.154-.115.27-.038l.385.386c.039.038.078.115.078.154 0 .039-.039.077-.078.154-.038.077-.115.155-.27.309-.154.154-.308.309-.578.502-.27.192-.58.347-.965.463-.386.115-.81.193-1.235.193-1.003 0-1.852-.348-2.508-1.004-.656-.656-1.003-1.543-1.003-2.662s.347-2.007 1.003-2.662c.656-.656 1.505-.965 2.47-.965.964 0 1.736.309 2.276.926.54.617.85 1.466.85 2.547v.115c-.04.116-.078.193-.232.193z" transform="translate(1 1) translate(33.803 4.985)"/>
|
||||
<path fill="#FFF" d="M65.25 7.138h-1.12c-.54 0-1.003.04-1.389.078-.347.038-.694.193-.926.385-.27.193-.386.464-.386.81 0 .348.155.618.425.811.27.193.617.309 1.08.309.926 0 1.698-.425 2.354-1.235l-.039-1.158zm2.006 3.319H66.02c-.424 0-.617-.193-.617-.618v-.733h-.039c-.694.965-1.62 1.467-2.817 1.467-.694 0-1.234-.193-1.698-.54-.424-.348-.656-.85-.656-1.467 0-.772.348-1.35 1.042-1.736.695-.386 1.621-.54 2.817-.54h1.196v-.155c0-1.196-.617-1.813-1.852-1.813-.578 0-1.003.038-1.273.154-.27.116-.425.193-.54.27-.116.077-.193.116-.232.154-.154.116-.231.116-.309.04l-.385-.503c-.078-.077-.04-.154.038-.231.154-.232.502-.425 1.003-.617.502-.193 1.12-.31 1.814-.31 2.006 0 3.01.966 3.01 2.856v3.319h.81c.116 0 .193.038.193.154v.617c-.077.155-.154.232-.27.232z" transform="translate(1 1) translate(33.803 4.985)"/>
|
||||
<path stroke="#FFF" stroke-width=".75" d="M65.25 7.138h-1.12c-.54 0-1.003.04-1.389.078-.347.038-.694.193-.926.385-.27.193-.386.464-.386.81 0 .348.155.618.425.811.27.193.617.309 1.08.309.926 0 1.698-.425 2.354-1.235l-.039-1.158zm2.006 3.319H66.02c-.424 0-.617-.193-.617-.618v-.733h-.039c-.694.965-1.62 1.467-2.817 1.467-.694 0-1.234-.193-1.698-.54-.424-.348-.656-.85-.656-1.467 0-.772.348-1.35 1.042-1.736.695-.386 1.621-.54 2.817-.54h1.196v-.155c0-1.196-.617-1.813-1.852-1.813-.578 0-1.003.038-1.273.154-.27.116-.425.193-.54.27-.116.077-.193.116-.232.154-.154.116-.231.116-.309.04l-.385-.503c-.078-.077-.04-.154.038-.231.154-.232.502-.425 1.003-.617.502-.193 1.12-.31 1.814-.31 2.006 0 3.01.966 3.01 2.856v3.319h.81c.116 0 .193.038.193.154v.617c-.077.155-.154.232-.27.232z" transform="translate(1 1) translate(33.803 4.985)"/>
|
||||
<path fill="#FFF" d="M71.385 10.457h-2.74c-.116 0-.193-.039-.193-.155v-.617c0-.116.077-.154.193-.154h.772V1.08h-.772c-.116 0-.193-.04-.193-.155V.309c0-.116.077-.155.193-.155h1.582c.27 0 .386.116.386.348v8.952h.772c.115 0 .192.038.192.154v.617c0 .155-.077.232-.192.232" transform="translate(1 1) translate(33.803 4.985)"/>
|
||||
<path stroke="#FFF" stroke-width=".75" d="M71.385 10.457h-2.74c-.116 0-.193-.039-.193-.155v-.617c0-.116.077-.154.193-.154h.772V1.08h-.772c-.116 0-.193-.04-.193-.155V.309c0-.116.077-.155.193-.155h1.582c.27 0 .386.116.386.348v8.952h.772c.115 0 .192.038.192.154v.617c0 .155-.077.232-.192.232z" transform="translate(1 1) translate(33.803 4.985)"/>
|
||||
<path fill="#FFF" d="M77.404 9.724c.039.038.039.077.039.116 0 .038-.04.077-.078.154-.424.386-.964.54-1.582.54-.617 0-1.157-.193-1.582-.617-.424-.425-.617-1.004-.617-1.814V4.322h-.81c-.155 0-.193-.078-.193-.193V3.55c0-.116.077-.193.193-.193h.81V1.89c0-.155.077-.193.193-.193h.81c.116 0 .193.077.193.193v1.466h1.814c.154 0 .193.077.193.193v.579c0 .115-.078.193-.193.193H74.78v3.743c0 .462.116.848.309 1.08.193.231.501.347.849.347.347 0 .617-.077.81-.27.077-.077.193-.077.27 0l.386.502z" transform="translate(1 1) translate(33.803 4.985)"/>
|
||||
<path stroke="#FFF" stroke-width=".75" d="M77.404 9.724c.039.038.039.077.039.116 0 .038-.04.077-.078.154-.424.386-.964.54-1.582.54-.617 0-1.157-.193-1.582-.617-.424-.425-.617-1.004-.617-1.814V4.322h-.81c-.155 0-.193-.078-.193-.193V3.55c0-.116.077-.193.193-.193h.81V1.89c0-.155.077-.193.193-.193h.81c.116 0 .193.077.193.193v1.466h1.814c.154 0 .193.077.193.193v.579c0 .115-.078.193-.193.193H74.78v3.743c0 .462.116.848.309 1.08.193.231.501.347.849.347.347 0 .617-.077.81-.27.077-.077.193-.077.27 0l.386.502z" transform="translate(1 1) translate(33.803 4.985)"/>
|
||||
<path fill="#FFF" d="M85.893 10.457h-1.35c-.425 0-.618-.193-.618-.618V6.058c0-.54-.154-1.003-.386-1.312-.27-.309-.656-.463-1.157-.463-.386 0-.695.077-.888.193-.193.116-.347.231-.463.309-.116.077-.193.193-.27.27-.116.154-.193.309-.309.463v3.936h.772c.116 0 .193.038.193.154v.617c0 .116-.077.155-.193.155h-2.74c-.115 0-.193-.039-.193-.155v-.617c0-.116.078-.154.194-.154h.771V1.08h-.771c-.116 0-.194-.038-.194-.154V.309c0-.116.078-.155.194-.155h1.813c.116 0 .193.039.193.155V3.51c0 .31 0 .618-.039.888h.039c.463-.733 1.196-1.12 2.238-1.12.772 0 1.35.232 1.813.695.463.463.656 1.042.656 1.814v3.704h.81c.116 0 .194.039.194.155v.617c-.116.116-.193.193-.309.193" transform="translate(1 1) translate(33.803 4.985)"/>
|
||||
<path stroke="#FFF" stroke-width=".75" d="M85.893 10.457h-1.35c-.425 0-.618-.193-.618-.618V6.058c0-.54-.154-1.003-.386-1.312-.27-.309-.656-.463-1.157-.463-.386 0-.695.077-.888.193-.193.116-.347.231-.463.309-.116.077-.193.193-.27.27-.116.154-.193.309-.309.463v3.936h.772c.116 0 .193.038.193.154v.617c0 .116-.077.155-.193.155h-2.74c-.115 0-.193-.039-.193-.155v-.617c0-.116.078-.154.194-.154h.771V1.08h-.771c-.116 0-.194-.038-.194-.154V.309c0-.116.078-.155.194-.155h1.813c.116 0 .193.039.193.155V3.51c0 .31 0 .618-.039.888h.039c.463-.733 1.196-1.12 2.238-1.12.772 0 1.35.232 1.813.695.463.463.656 1.042.656 1.814v3.704h.81c.116 0 .194.039.194.155v.617c-.116.116-.193.193-.309.193z" transform="translate(1 1) translate(33.803 4.985)"/>
|
||||
<g>
|
||||
<path fill="#FFF" d="M3.627 10.302H.193c-.116 0-.193-.038-.193-.154v-.733c0-.116.077-.154.193-.154h1.08V1.042H.193C.077 1.042 0 1.003 0 .888V.154C0 .04.077 0 .193 0h3.434c.116 0 .193.039.193.154v.734c0 .115-.077.154-.193.154H2.508v8.18h1.119c.116 0 .193.039.193.154v.734c0 .115-.077.192-.193.192" transform="translate(1 1) translate(33.803 4.985) translate(91.924 .113)"/>
|
||||
<path stroke="#FFF" stroke-width=".75" d="M3.627 10.302H.193c-.116 0-.193-.038-.193-.154v-.733c0-.116.077-.154.193-.154h1.08V1.042H.193C.077 1.042 0 1.003 0 .888V.154C0 .04.077 0 .193 0h3.434c.116 0 .193.039.193.154v.734c0 .115-.077.154-.193.154H2.508v8.18h1.119c.116 0 .193.039.193.154v.734c0 .115-.077.192-.193.192z" transform="translate(1 1) translate(33.803 4.985) translate(91.924 .113)"/>
|
||||
<path fill="#FFF" d="M17.094 10.302h-1.35c-.232 0-.387-.038-.464-.154-.116-.077-.154-.231-.154-.463V5.904c0-.54-.116-1.004-.386-1.312-.27-.309-.656-.463-1.158-.463-.771 0-1.35.347-1.775 1.003.039.154.077.347.077.579v3.627h.81c.117 0 .194.038.194.154v.618c0 .115-.039.154-.193.154h-2.74c-.116 0-.193-.039-.193-.154v-.618c0-.116.077-.154.193-.154h.772V5.904c0-.54-.154-1.004-.386-1.312-.27-.309-.656-.463-1.158-.463-.385 0-.694.077-.887.193-.193.116-.347.231-.463.308-.116.078-.193.193-.27.27-.116.155-.193.31-.309.464v3.935h.695c.116 0 .193.039.193.155v.617c0 .116-.039.154-.193.154H5.248c-.116 0-.193-.038-.193-.154v-.617c0-.116.077-.155.193-.155h.771V4.206h-.771c-.116 0-.193-.039-.193-.154v-.618c0-.116.077-.154.193-.154h1.389c.154 0 .27.038.347.077.077.077.116.154.116.347l-.039.734H7.1c.501-.85 1.273-1.235 2.315-1.235s1.775.386 2.16 1.196c.541-.81 1.313-1.196 2.316-1.196.772 0 1.35.231 1.775.694.424.463.656 1.042.656 1.814v3.704h.81c.116 0 .193.039.193.154v.618c-.038.038-.116.115-.231.115" transform="translate(1 1) translate(33.803 4.985) translate(91.924 .113)"/>
|
||||
<path stroke="#FFF" stroke-width=".75" d="M17.094 10.302h-1.35c-.232 0-.387-.038-.464-.154-.116-.077-.154-.231-.154-.463V5.904c0-.54-.116-1.004-.386-1.312-.27-.309-.656-.463-1.158-.463-.771 0-1.35.347-1.775 1.003.039.154.077.347.077.579v3.627h.81c.117 0 .194.038.194.154v.618c0 .115-.039.154-.193.154h-2.74c-.116 0-.193-.039-.193-.154v-.618c0-.116.077-.154.193-.154h.772V5.904c0-.54-.154-1.004-.386-1.312-.27-.309-.656-.463-1.158-.463-.385 0-.694.077-.887.193-.193.116-.347.231-.463.308-.116.078-.193.193-.27.27-.116.155-.193.31-.309.464v3.935h.695c.116 0 .193.039.193.155v.617c0 .116-.039.154-.193.154H5.248c-.116 0-.193-.038-.193-.154v-.617c0-.116.077-.155.193-.155h.771V4.206h-.771c-.116 0-.193-.039-.193-.154v-.618c0-.116.077-.154.193-.154h1.389c.154 0 .27.038.347.077.077.077.116.154.116.347l-.039.734H7.1c.501-.85 1.273-1.235 2.315-1.235s1.775.386 2.16 1.196c.541-.81 1.313-1.196 2.316-1.196.772 0 1.35.231 1.775.694.424.463.656 1.042.656 1.814v3.704h.81c.116 0 .193.039.193.154v.618c-.038.038-.116.115-.231.115z" transform="translate(1 1) translate(33.803 4.985) translate(91.924 .113)"/>
|
||||
<path fill="#FFF" d="M23.267 6.984h-1.119c-.54 0-1.003.039-1.389.077-.347.039-.694.193-.926.386-.27.193-.386.463-.386.81 0 .348.155.618.425.81.27.194.617.31 1.08.31.926 0 1.698-.425 2.354-1.235l-.039-1.158zm2.007 3.318h-1.235c-.424 0-.617-.192-.617-.617v-.733h-.039c-.694.965-1.62 1.466-2.817 1.466-.694 0-1.234-.193-1.697-.54-.425-.347-.656-.849-.656-1.466 0-.772.347-1.35 1.042-1.736.694-.387 1.62-.54 2.816-.54h1.196V5.98c0-1.196-.617-1.814-1.852-1.814-.579 0-1.003.04-1.273.155-.27.115-.425.193-.54.27-.116.077-.193.115-.232.154-.154.116-.231.116-.309.039l-.385-.502c-.077-.077-.039-.154.038-.231.155-.232.502-.425 1.003-.618.502-.193 1.12-.309 1.814-.309 2.007 0 3.01.965 3.01 2.856v3.318h.81c.116 0 .193.039.193.155v.617c-.077.154-.154.231-.27.231z" transform="translate(1 1) translate(33.803 4.985) translate(91.924 .113)"/>
|
||||
<path stroke="#FFF" stroke-width=".75" d="M23.267 6.984h-1.119c-.54 0-1.003.039-1.389.077-.347.039-.694.193-.926.386-.27.193-.386.463-.386.81 0 .348.155.618.425.81.27.194.617.31 1.08.31.926 0 1.698-.425 2.354-1.235l-.039-1.158zm2.007 3.318h-1.235c-.424 0-.617-.192-.617-.617v-.733h-.039c-.694.965-1.62 1.466-2.817 1.466-.694 0-1.234-.193-1.697-.54-.425-.347-.656-.849-.656-1.466 0-.772.347-1.35 1.042-1.736.694-.387 1.62-.54 2.816-.54h1.196V5.98c0-1.196-.617-1.814-1.852-1.814-.579 0-1.003.04-1.273.155-.27.115-.425.193-.54.27-.116.077-.193.115-.232.154-.154.116-.231.116-.309.039l-.385-.502c-.077-.077-.039-.154.038-.231.155-.232.502-.425 1.003-.618.502-.193 1.12-.309 1.814-.309 2.007 0 3.01.965 3.01 2.856v3.318h.81c.116 0 .193.039.193.155v.617c-.077.154-.154.231-.27.231z" transform="translate(1 1) translate(33.803 4.985) translate(91.924 .113)"/>
|
||||
<path fill="#FFF" d="M27.55 6.753c0 .81.193 1.427.618 1.929.424.463.965.733 1.582.733.617 0 1.158-.231 1.582-.733.425-.502.617-1.12.617-1.93 0-.81-.192-1.427-.617-1.929-.424-.501-.926-.694-1.582-.694-.617 0-1.158.231-1.582.733-.386.463-.617 1.119-.617 1.89m2.122 6.908c-.463 0-.849-.039-1.08-.116-.232-.077-.425-.116-.58-.193-.154-.039-.27-.116-.385-.155-.116-.038-.193-.115-.309-.154-.424-.27-.579-.463-.463-.617l.386-.54c.038-.077.154-.077.231 0l.078.077c.038.038.115.077.154.116.038.038.116.077.231.154.078.038.193.116.31.154.115.039.23.077.385.116.309.077.656.116 1.042.116.694 0 1.235-.155 1.659-.502.425-.347.617-.849.617-1.543v-1.66c-.192.386-.463.734-.848 1.042-.386.31-.888.425-1.505.425-.965 0-1.775-.347-2.354-1.003-.579-.695-.888-1.582-.888-2.663 0-1.08.309-1.968.888-2.624.579-.656 1.389-1.003 2.354-1.003.578 0 1.08.154 1.505.424.424.27.694.618.887 1.004h.039v-1.12c0-.115.038-.154.192-.154h1.698c.116 0 .193.039.193.155v.617c0 .116-.038.154-.193.154h-.771v6.367c0 1.042-.31 1.814-.927 2.315-.694.54-1.504.81-2.546.81" transform="translate(1 1) translate(33.803 4.985) translate(91.924 .113)"/>
|
||||
<path stroke="#FFF" stroke-width=".75" d="M27.55 6.753c0 .81.193 1.427.618 1.929.424.463.965.733 1.582.733.617 0 1.158-.231 1.582-.733.425-.502.617-1.12.617-1.93 0-.81-.192-1.427-.617-1.929-.424-.501-.926-.694-1.582-.694-.617 0-1.158.231-1.582.733-.386.463-.617 1.119-.617 1.89zm2.123 6.907c-.463 0-.849-.039-1.08-.116-.232-.077-.425-.116-.58-.193-.154-.039-.27-.116-.385-.155-.116-.038-.193-.115-.309-.154-.424-.27-.579-.463-.463-.617l.386-.54c.038-.077.154-.077.231 0l.078.077c.038.038.115.077.154.116.038.038.116.077.231.154.078.038.193.116.31.154.115.039.23.077.385.116.309.077.656.116 1.042.116.694 0 1.235-.155 1.659-.502.425-.347.617-.849.617-1.543v-1.66c-.192.386-.463.734-.848 1.042-.386.31-.888.425-1.505.425-.965 0-1.775-.347-2.354-1.003-.579-.695-.888-1.582-.888-2.663 0-1.08.309-1.968.888-2.624.579-.656 1.389-1.003 2.354-1.003.578 0 1.08.154 1.505.424.424.27.694.618.887 1.004h.039v-1.12c0-.115.038-.154.192-.154h1.698c.116 0 .193.039.193.155v.617c0 .116-.038.154-.193.154h-.771v6.367c0 1.042-.31 1.814-.927 2.315-.694.54-1.504.81-2.546.81z" transform="translate(1 1) translate(33.803 4.985) translate(91.924 .113)"/>
|
||||
<path fill="#FFF" d="M37.274 1.08c.155.155.232.348.232.58 0 .23-.077.424-.232.54-.154.154-.347.231-.579.231-.231 0-.424-.077-.578-.232-.155-.154-.232-.347-.232-.54 0-.231.077-.424.232-.579.154-.154.347-.23.578-.23.232 0 .425.076.58.23m.81 9.223h-2.74c-.116 0-.193-.039-.193-.155v-.617c0-.116.077-.155.193-.155h.772V4.167h-.772c-.116 0-.193-.038-.193-.154v-.617c0-.116.077-.155.193-.155h1.814c.115 0 .192.039.192.155v5.942h.772c.116 0 .193.038.193.154v.618c-.039.115-.077.193-.231.193" transform="translate(1 1) translate(33.803 4.985) translate(91.924 .113)"/>
|
||||
<path stroke="#FFF" stroke-width=".75" d="M37.274 1.08c.155.155.232.348.232.58 0 .23-.077.424-.232.54-.154.154-.347.231-.579.231-.231 0-.424-.077-.578-.232-.155-.154-.232-.347-.232-.54 0-.231.077-.424.232-.579.154-.154.347-.23.578-.23.232 0 .425.076.58.23zm.81 9.223h-2.739c-.116 0-.193-.039-.193-.155v-.617c0-.116.077-.155.193-.155h.772V4.167h-.772c-.116 0-.193-.038-.193-.154v-.617c0-.116.077-.155.193-.155h1.814c.115 0 .192.039.192.155v5.942h.772c.116 0 .193.038.193.154v.618c-.039.115-.077.193-.231.193z" transform="translate(1 1) translate(33.803 4.985) translate(91.924 .113)"/>
|
||||
<path fill="#FFF" d="M47.037 10.302h-1.351c-.424 0-.617-.192-.617-.617V5.904c0-.54-.155-1.004-.386-1.312-.27-.309-.656-.463-1.158-.463-.733 0-1.312.309-1.736.926-.039.039-.116.154-.193.309v3.935h.772c.115 0 .193.039.193.155v.617c0 .116-.078.154-.193.154h-2.74c-.116 0-.193-.038-.193-.154v-.617c0-.116.077-.155.193-.155h.772V4.206h-.772c-.116 0-.193-.039-.193-.154v-.618c0-.116.077-.154.193-.154h1.698c.116 0 .193.038.193.154l-.039.965h.039c.501-.849 1.273-1.235 2.315-1.235.772 0 1.35.232 1.814.695.462.463.655 1.041.655 1.813v3.704h.81c.116 0 .194.04.194.155v.617c-.116.077-.155.154-.27.154" transform="translate(1 1) translate(33.803 4.985) translate(91.924 .113)"/>
|
||||
<path stroke="#FFF" stroke-width=".75" d="M47.037 10.302h-1.351c-.424 0-.617-.192-.617-.617V5.904c0-.54-.155-1.004-.386-1.312-.27-.309-.656-.463-1.158-.463-.733 0-1.312.309-1.736.926-.039.039-.116.154-.193.309v3.935h.772c.115 0 .193.039.193.155v.617c0 .116-.078.154-.193.154h-2.74c-.116 0-.193-.038-.193-.154v-.617c0-.116.077-.155.193-.155h.772V4.206h-.772c-.116 0-.193-.039-.193-.154v-.618c0-.116.077-.154.193-.154h1.698c.116 0 .193.038.193.154l-.039.965h.039c.501-.849 1.273-1.235 2.315-1.235.772 0 1.35.232 1.814.695.462.463.655 1.041.655 1.813v3.704h.81c.116 0 .194.04.194.155v.617c-.116.077-.155.154-.27.154z" transform="translate(1 1) translate(33.803 4.985) translate(91.924 .113)"/>
|
||||
<path fill="#FFF" d="M49.197 6.753c0 .81.193 1.427.618 1.929.424.463.965.733 1.582.733.617 0 1.157-.231 1.582-.733.424-.502.617-1.12.617-1.93 0-.81-.193-1.427-.617-1.929-.425-.501-.926-.694-1.582-.694-.617 0-1.158.231-1.582.733-.425.501-.618 1.119-.618 1.89m2.161 6.908c-.463 0-.849-.039-1.08-.116-.232-.077-.425-.116-.579-.193-.154-.039-.27-.116-.386-.155-.116-.038-.193-.115-.308-.154-.425-.27-.58-.463-.464-.617l.386-.54c.039-.077.155-.077.232 0l.077.077c.039.038.116.077.154.116.039.038.116.077.232.154.077.038.193.116.309.154.115.039.231.077.385.116.31.077.656.116 1.042.116.695 0 1.235-.155 1.66-.502.424-.347.617-.849.617-1.543v-1.66c-.193.386-.463.734-.85 1.042-.385.31-.887.425-1.504.425-.965 0-1.775-.347-2.354-1.003-.578-.695-.887-1.582-.887-2.663 0-1.08.309-1.968.887-2.624.58-.656 1.39-1.003 2.354-1.003.579 0 1.08.154 1.505.424.424.27.695.618.887 1.004h.039v-1.12c0-.115.039-.154.193-.154h1.698c.115 0 .193.039.193.155v.617c0 .116-.039.154-.193.154h-.772v6.367c0 1.042-.309 1.814-.926 2.315-.733.54-1.544.81-2.547.81" transform="translate(1 1) translate(33.803 4.985) translate(91.924 .113)"/>
|
||||
<path stroke="#FFF" stroke-width=".75" d="M49.197 6.753c0 .81.193 1.427.618 1.929.424.463.965.733 1.582.733.617 0 1.157-.231 1.582-.733.424-.502.617-1.12.617-1.93 0-.81-.193-1.427-.617-1.929-.425-.501-.926-.694-1.582-.694-.617 0-1.158.231-1.582.733-.425.501-.618 1.119-.618 1.89zm2.161 6.907c-.463 0-.849-.039-1.08-.116-.232-.077-.425-.116-.579-.193-.154-.039-.27-.116-.386-.155-.116-.038-.193-.115-.308-.154-.425-.27-.58-.463-.464-.617l.386-.54c.039-.077.155-.077.232 0l.077.077c.039.038.116.077.154.116.039.038.116.077.232.154.077.038.193.116.309.154.115.039.231.077.385.116.31.077.656.116 1.042.116.695 0 1.235-.155 1.66-.502.424-.347.617-.849.617-1.543v-1.66c-.193.386-.463.734-.85 1.042-.385.31-.887.425-1.504.425-.965 0-1.775-.347-2.354-1.003-.578-.695-.887-1.582-.887-2.663 0-1.08.309-1.968.887-2.624.58-.656 1.39-1.003 2.354-1.003.579 0 1.08.154 1.505.424.424.27.695.618.887 1.004h.039v-1.12c0-.115.039-.154.193-.154h1.698c.115 0 .193.039.193.155v.617c0 .116-.039.154-.193.154h-.772v6.367c0 1.042-.309 1.814-.926 2.315-.733.54-1.544.81-2.547.81z" transform="translate(1 1) translate(33.803 4.985) translate(91.924 .113)"/>
|
||||
<path fill="#FFF" d="M65.596 10.302h-3.974c-.116 0-.193-.038-.193-.154v-.733c0-.116.077-.154.193-.154h1.08V1.042h-1.08c-.116 0-.193-.039-.193-.154V.154c0-.115.077-.154.193-.154h7.37c.116 0 .193.039.193.154V2.47c0 .116-.077.194-.193.194h-.771c-.116 0-.194-.078-.194-.194V1.08h-4.09v3.55h3.396c.116 0 .193.039.193.155v.733c0 .116-.077.154-.193.154h-3.396v3.512h1.62c.117 0 .194.038.194.154v.733c.038.154-.039.231-.155.231" transform="translate(1 1) translate(33.803 4.985) translate(91.924 .113)"/>
|
||||
<path stroke="#FFF" stroke-width=".75" d="M65.596 10.302h-3.974c-.116 0-.193-.038-.193-.154v-.733c0-.116.077-.154.193-.154h1.08V1.042h-1.08c-.116 0-.193-.039-.193-.154V.154c0-.115.077-.154.193-.154h7.37c.116 0 .193.039.193.154V2.47c0 .116-.077.194-.193.194h-.771c-.116 0-.194-.078-.194-.194V1.08h-4.09v3.55h3.396c.116 0 .193.039.193.155v.733c0 .116-.077.154-.193.154h-3.396v3.512h1.62c.117 0 .194.038.194.154v.733c.038.154-.039.231-.155.231z" transform="translate(1 1) translate(33.803 4.985) translate(91.924 .113)"/>
|
||||
<path fill="#FFF" d="M70.535 6.753c0 .771.232 1.427.695 1.929.463.502 1.003.733 1.698.733.694 0 1.235-.231 1.698-.733.463-.502.694-1.12.694-1.93 0-.771-.231-1.427-.694-1.89-.463-.463-1.004-.733-1.66-.733-.656 0-1.196.231-1.697.733-.502.463-.734 1.08-.734 1.89m4.94 2.625c-.656.656-1.544 1.003-2.586 1.003-1.042 0-1.89-.347-2.585-1.003-.656-.656-1.003-1.544-1.003-2.663 0-1.119.347-1.968 1.003-2.624.695-.656 1.544-.965 2.547-.965 1.003 0 1.89.348 2.546 1.004.656.656 1.004 1.543 1.004 2.624.077 1.08-.232 1.968-.926 2.624" transform="translate(1 1) translate(33.803 4.985) translate(91.924 .113)"/>
|
||||
<path stroke="#FFF" stroke-width=".75" d="M70.535 6.753c0 .771.232 1.427.695 1.929.463.502 1.003.733 1.698.733.694 0 1.235-.231 1.698-.733.463-.502.694-1.12.694-1.93 0-.771-.231-1.427-.694-1.89-.463-.463-1.004-.733-1.66-.733-.656 0-1.196.231-1.697.733-.502.463-.734 1.08-.734 1.89zm4.94 2.624c-.656.656-1.544 1.003-2.586 1.003-1.042 0-1.89-.347-2.585-1.003-.656-.656-1.003-1.544-1.003-2.663 0-1.119.347-1.968 1.003-2.624.695-.656 1.544-.965 2.547-.965 1.003 0 1.89.348 2.546 1.004.656.656 1.004 1.543 1.004 2.624.077 1.08-.232 1.968-.926 2.624z" transform="translate(1 1) translate(33.803 4.985) translate(91.924 .113)"/>
|
||||
<path fill="#FFF" d="M84.812 10.302h-1.234c-.425 0-.618-.192-.618-.617v-.54h-.038c-.386.81-1.158 1.235-2.238 1.235-.772 0-1.39-.232-1.814-.695-.424-.463-.656-1.08-.694-1.813V4.167h-.81c-.116 0-.194-.038-.194-.154v-.617c0-.116.039-.155.193-.155h1.39c.424 0 .617.193.617.618V7.64c0 .54.154 1.003.386 1.312.27.309.656.463 1.196.463.695 0 1.234-.27 1.659-.772.193-.231.27-.385.27-.424V4.167h-.772c-.115 0-.193-.038-.193-.154v-.617c0-.116.04-.155.193-.155h1.775c.116 0 .193.039.193.155v5.942h.81c.116 0 .194.038.194.154v.618c-.078.115-.155.192-.27.192" transform="translate(1 1) translate(33.803 4.985) translate(91.924 .113)"/>
|
||||
<path stroke="#FFF" stroke-width=".75" d="M84.812 10.302h-1.234c-.425 0-.618-.192-.618-.617v-.54h-.038c-.386.81-1.158 1.235-2.238 1.235-.772 0-1.39-.232-1.814-.695-.424-.463-.656-1.08-.694-1.813V4.167h-.81c-.116 0-.194-.038-.194-.154v-.617c0-.116.039-.155.193-.155h1.39c.424 0 .617.193.617.618V7.64c0 .54.154 1.003.386 1.312.27.309.656.463 1.196.463.695 0 1.234-.27 1.659-.772.193-.231.27-.385.27-.424V4.167h-.772c-.115 0-.193-.038-.193-.154v-.617c0-.116.04-.155.193-.155h1.775c.116 0 .193.039.193.155v5.942h.81c.116 0 .194.038.194.154v.618c-.078.115-.155.192-.27.192z" transform="translate(1 1) translate(33.803 4.985) translate(91.924 .113)"/>
|
||||
<path fill="#FFF" d="M93.687 10.302h-1.35c-.425 0-.618-.192-.618-.617V5.904c0-.54-.154-1.004-.385-1.312-.27-.309-.656-.463-1.158-.463-.733 0-1.312.309-1.736.926-.04.039-.116.154-.193.309v3.935h.771c.116 0 .193.039.193.155v.617c0 .116-.077.154-.193.154h-2.74c-.115 0-.192-.038-.192-.154v-.617c0-.116.077-.155.193-.155h.772V4.206h-.772c-.116 0-.193-.039-.193-.154v-.618c0-.116.077-.154.193-.154h1.698c.115 0 .192.038.192.154l-.038.965h.038c.502-.849 1.274-1.235 2.316-1.235.771 0 1.35.232 1.813.695.463.463.656 1.041.656 1.813v3.704h.81c.116 0 .193.04.193.155v.617c-.077.077-.154.154-.27.154" transform="translate(1 1) translate(33.803 4.985) translate(91.924 .113)"/>
|
||||
<path stroke="#FFF" stroke-width=".75" d="M93.687 10.302h-1.35c-.425 0-.618-.192-.618-.617V5.904c0-.54-.154-1.004-.385-1.312-.27-.309-.656-.463-1.158-.463-.733 0-1.312.309-1.736.926-.04.039-.116.154-.193.309v3.935h.771c.116 0 .193.039.193.155v.617c0 .116-.077.154-.193.154h-2.74c-.115 0-.192-.038-.192-.154v-.617c0-.116.077-.155.193-.155h.772V4.206h-.772c-.116 0-.193-.039-.193-.154v-.618c0-.116.077-.154.193-.154h1.698c.115 0 .192.038.192.154l-.038.965h.038c.502-.849 1.274-1.235 2.316-1.235.771 0 1.35.232 1.813.695.463.463.656 1.041.656 1.813v3.704h.81c.116 0 .193.04.193.155v.617c-.077.077-.154.154-.27.154z" transform="translate(1 1) translate(33.803 4.985) translate(91.924 .113)"/>
|
||||
<path fill="#FFF" d="M95.77 6.714c0 .81.194 1.428.618 1.93.425.5.965.732 1.582.732.656 0 1.158-.231 1.583-.733.424-.501.617-1.119.617-1.929s-.193-1.428-.617-1.89c-.425-.464-.965-.695-1.583-.695-.617 0-1.157.231-1.582.694-.386.463-.617 1.12-.617 1.891m6.367 3.589h-1.235c-.232 0-.386-.039-.463-.155-.116-.077-.154-.231-.154-.463v-.81c-.155.386-.425.772-.85 1.042-.424.308-.925.463-1.543.463-.965 0-1.736-.348-2.353-1.004-.58-.655-.888-1.543-.888-2.623 0-1.08.309-1.968.888-2.624.578-.656 1.388-1.003 2.353-1.003.579 0 1.08.154 1.466.424.386.27.695.617.85 1.003V.965h-.811c-.116 0-.193-.039-.193-.155V.193c0-.116.077-.154.193-.154h1.775c.116 0 .193.038.193.154v9.183h.81c.116 0 .193.04.193.155v.617c-.077.077-.116.155-.231.155" transform="translate(1 1) translate(33.803 4.985) translate(91.924 .113)"/>
|
||||
<path stroke="#FFF" stroke-width=".75" d="M95.77 6.714c0 .81.194 1.428.618 1.93.425.5.965.732 1.582.732.656 0 1.158-.231 1.583-.733.424-.501.617-1.119.617-1.929s-.193-1.428-.617-1.89c-.425-.464-.965-.695-1.583-.695-.617 0-1.157.231-1.582.694-.386.463-.617 1.12-.617 1.891zm6.368 3.589h-1.235c-.232 0-.386-.039-.463-.155-.116-.077-.154-.231-.154-.463v-.81c-.155.386-.425.772-.85 1.042-.424.308-.925.463-1.543.463-.965 0-1.736-.348-2.353-1.004-.58-.655-.888-1.543-.888-2.623 0-1.08.309-1.968.888-2.624.578-.656 1.388-1.003 2.353-1.003.579 0 1.08.154 1.466.424.386.27.695.617.85 1.003V.965h-.811c-.116 0-.193-.039-.193-.155V.193c0-.116.077-.154.193-.154h1.775c.116 0 .193.038.193.154v9.183h.81c.116 0 .193.04.193.155v.617c-.077.077-.116.155-.231.155z" transform="translate(1 1) translate(33.803 4.985) translate(91.924 .113)"/>
|
||||
<path fill="#FFF" d="M108.466 6.984h-1.158c-.54 0-1.003.039-1.389.077-.347.039-.695.193-.926.386-.27.193-.386.463-.386.81 0 .348.116.618.425.81.308.194.617.31 1.08.31.926 0 1.698-.425 2.354-1.235V6.984zm2.006 3.318h-1.234c-.425 0-.618-.192-.618-.617v-.733h-.038c-.695.965-1.621 1.466-2.817 1.466-.695 0-1.235-.193-1.698-.54-.424-.347-.656-.849-.656-1.466 0-.772.347-1.35 1.042-1.736.694-.387 1.62-.54 2.817-.54h1.196V5.98c0-1.196-.618-1.814-1.852-1.814-.579 0-1.004.04-1.274.155-.27.115-.463.193-.54.27-.116.077-.193.115-.27.154-.154.116-.27.116-.309.039l-.385-.502c-.078-.077-.078-.154.038-.231.154-.232.502-.425 1.003-.618.502-.193 1.12-.309 1.814-.309 2.006 0 3.01.965 3.01 2.856v3.318h.81c.116 0 .193.039.193.155v.617c-.077.154-.116.231-.232.231z" transform="translate(1 1) translate(33.803 4.985) translate(91.924 .113)"/>
|
||||
<path stroke="#FFF" stroke-width=".75" d="M108.466 6.984h-1.158c-.54 0-1.003.039-1.389.077-.347.039-.695.193-.926.386-.27.193-.386.463-.386.81 0 .348.116.618.425.81.308.194.617.31 1.08.31.926 0 1.698-.425 2.354-1.235V6.984zm2.006 3.318h-1.234c-.425 0-.618-.192-.618-.617v-.733h-.038c-.695.965-1.621 1.466-2.817 1.466-.695 0-1.235-.193-1.698-.54-.424-.347-.656-.849-.656-1.466 0-.772.347-1.35 1.042-1.736.694-.387 1.62-.54 2.817-.54h1.196V5.98c0-1.196-.618-1.814-1.852-1.814-.579 0-1.004.04-1.274.155-.27.115-.463.193-.54.27-.116.077-.193.115-.27.154-.154.116-.27.116-.309.039l-.385-.502c-.078-.077-.078-.154.038-.231.154-.232.502-.425 1.003-.618.502-.193 1.12-.309 1.814-.309 2.006 0 3.01.965 3.01 2.856v3.318h.81c.116 0 .193.039.193.155v.617c-.077.154-.116.231-.232.231z" transform="translate(1 1) translate(33.803 4.985) translate(91.924 .113)"/>
|
||||
<path fill="#FFF" d="M116.106 9.57c.039.038.077.077.077.115 0 .039-.038.077-.077.155-.425.385-.965.54-1.582.54-.618 0-1.158-.193-1.582-.618-.425-.424-.618-1.003-.618-1.813V4.167h-.81c-.116 0-.193-.077-.193-.193v-.578c0-.116.077-.194.193-.194h.81V1.736c0-.154.078-.192.193-.192h.81c.117 0 .194.077.194.192v1.466h1.813c.116 0 .193.078.193.194v.578c0 .116-.077.193-.193.193h-1.813V7.91c0 .463.115.85.308 1.08.193.232.502.348.85.348.346 0 .617-.077.81-.27.077-.077.192-.077.27 0l.347.501z" transform="translate(1 1) translate(33.803 4.985) translate(91.924 .113)"/>
|
||||
<path stroke="#FFF" stroke-width=".75" d="M116.106 9.57c.039.038.077.077.077.115 0 .039-.038.077-.077.155-.425.385-.965.54-1.582.54-.618 0-1.158-.193-1.582-.618-.425-.424-.618-1.003-.618-1.813V4.167h-.81c-.116 0-.193-.077-.193-.193v-.578c0-.116.077-.194.193-.194h.81V1.736c0-.154.078-.192.193-.192h.81c.117 0 .194.077.194.192v1.466h1.813c.116 0 .193.078.193.194v.578c0 .116-.077.193-.193.193h-1.813V7.91c0 .463.115.85.308 1.08.193.232.502.348.85.348.346 0 .617-.077.81-.27.077-.077.192-.077.27 0l.347.501z" transform="translate(1 1) translate(33.803 4.985) translate(91.924 .113)"/>
|
||||
<path fill="#FFF" d="M119.077 1.08c.154.155.231.348.231.58 0 .23-.077.424-.231.54-.154.154-.347.231-.579.231-.231 0-.424-.077-.579-.232-.154-.154-.231-.347-.231-.54 0-.231.077-.424.231-.579.155-.154.348-.23.58-.23.23 0 .424.076.578.23m.81 9.223h-2.74c-.115 0-.192-.039-.192-.155v-.617c0-.116.077-.155.193-.155h.771V4.167h-.771c-.116 0-.193-.038-.193-.154v-.617c0-.116.077-.155.193-.155h1.813c.116 0 .193.039.193.155v5.942h.772c.116 0 .193.038.193.154v.618c-.039.115-.077.193-.232.193" transform="translate(1 1) translate(33.803 4.985) translate(91.924 .113)"/>
|
||||
<path stroke="#FFF" stroke-width=".75" d="M119.077 1.08c.154.155.231.348.231.58 0 .23-.077.424-.231.54-.154.154-.347.231-.579.231-.231 0-.424-.077-.579-.232-.154-.154-.231-.347-.231-.54 0-.231.077-.424.231-.579.155-.154.348-.23.58-.23.23 0 .424.076.578.23zm.81 9.223h-2.74c-.115 0-.192-.039-.192-.155v-.617c0-.116.077-.155.193-.155h.771V4.167h-.771c-.116 0-.193-.038-.193-.154v-.617c0-.116.077-.155.193-.155h1.813c.116 0 .193.039.193.155v5.942h.772c.116 0 .193.038.193.154v.618c-.039.115-.077.193-.232.193z" transform="translate(1 1) translate(33.803 4.985) translate(91.924 .113)"/>
|
||||
<path fill="#FFF" d="M122.202 6.753c0 .771.232 1.427.695 1.929.463.502 1.042.733 1.698.733.656 0 1.235-.231 1.698-.733.463-.502.694-1.12.694-1.93 0-.771-.231-1.427-.694-1.89-.463-.463-1.042-.733-1.66-.733-.617 0-1.196.231-1.697.733-.502.463-.734 1.08-.734 1.89m4.978 2.625c-.656.656-1.543 1.003-2.585 1.003s-1.89-.347-2.585-1.003c-.695-.656-1.004-1.544-1.004-2.663 0-1.119.348-1.968 1.042-2.624.695-.656 1.544-.965 2.585-.965 1.042 0 1.891.348 2.547 1.004.656.656 1.003 1.543 1.003 2.624 0 1.08-.347 1.968-1.003 2.624" transform="translate(1 1) translate(33.803 4.985) translate(91.924 .113)"/>
|
||||
<path stroke="#FFF" stroke-width=".75" d="M122.202 6.753c0 .771.232 1.427.695 1.929.463.502 1.042.733 1.698.733.656 0 1.235-.231 1.698-.733.463-.502.694-1.12.694-1.93 0-.771-.231-1.427-.694-1.89-.463-.463-1.042-.733-1.66-.733-.617 0-1.196.231-1.697.733-.502.463-.734 1.08-.734 1.89zm4.978 2.624c-.656.656-1.543 1.003-2.585 1.003s-1.89-.347-2.585-1.003c-.695-.656-1.004-1.544-1.004-2.663 0-1.119.348-1.968 1.042-2.624.695-.656 1.544-.965 2.585-.965 1.042 0 1.891.348 2.547 1.004.656.656 1.003 1.543 1.003 2.624 0 1.08-.347 1.968-1.003 2.624z" transform="translate(1 1) translate(33.803 4.985) translate(91.924 .113)"/>
|
||||
<path fill="#FFF" d="M136.865 10.302h-1.35c-.424 0-.618-.192-.618-.617V5.904c0-.54-.115-1.004-.385-1.312-.27-.309-.656-.463-1.158-.463-.733 0-1.312.309-1.737.926-.038.039-.115.154-.192.309v3.935h.771c.116 0 .193.039.193.155v.617c0 .116-.077.154-.193.154h-2.74c-.115 0-.192-.038-.192-.154v-.617c0-.116.077-.155.193-.155h.771V4.206h-.771c-.116 0-.193-.039-.193-.154v-.618c0-.116.077-.154.193-.154h1.698c.115 0 .193.038.193.154l-.04.965h.04c.501-.849 1.273-1.235 2.315-1.235.771 0 1.35.232 1.813.695.463.463.656 1.041.656 1.813v3.704h.81c.116 0 .193.04.193.155v.617c-.077.077-.115.154-.27.154" transform="translate(1 1) translate(33.803 4.985) translate(91.924 .113)"/>
|
||||
<path stroke="#FFF" stroke-width=".75" d="M136.865 10.302h-1.35c-.424 0-.618-.192-.618-.617V5.904c0-.54-.115-1.004-.385-1.312-.27-.309-.656-.463-1.158-.463-.733 0-1.312.309-1.737.926-.038.039-.115.154-.192.309v3.935h.771c.116 0 .193.039.193.155v.617c0 .116-.077.154-.193.154h-2.74c-.115 0-.192-.038-.192-.154v-.617c0-.116.077-.155.193-.155h.771V4.206h-.771c-.116 0-.193-.039-.193-.154v-.618c0-.116.077-.154.193-.154h1.698c.115 0 .193.038.193.154l-.04.965h.04c.501-.849 1.273-1.235 2.315-1.235.771 0 1.35.232 1.813.695.463.463.656 1.041.656 1.813v3.704h.81c.116 0 .193.04.193.155v.617c-.077.077-.115.154-.27.154z" transform="translate(1 1) translate(33.803 4.985) translate(91.924 .113)"/>
|
||||
</g>
|
||||
</g>
|
||||
<g fill="#FFF" fill-rule="nonzero">
|
||||
<path d="M22.85 21.43c0 .713-.58 1.294-1.293 1.294h-8.308c-.713 0-1.293-.58-1.293-1.293v-8.308c0-.713.58-1.293 1.293-1.293h8.308c.712 0 1.293.58 1.293 1.293v8.308zm-1.327-8.273h-8.24v8.24h8.24v-8.24zM22.85 8.937c0 .713-.58 1.293-1.293 1.293h-8.308c-.713 0-1.293-.58-1.293-1.293V.63c0-.712.58-1.292 1.293-1.292h8.308c.712 0 1.293.58 1.293 1.292v8.308zM21.523.663h-8.24v8.24h8.24V.663zM10.23 21.43c0 .713-.58 1.294-1.293 1.294H.63c-.712 0-1.292-.58-1.292-1.293v-8.308c0-.713.58-1.293 1.292-1.293h8.308c.713 0 1.293.58 1.293 1.293v8.308zm-1.327-8.273H.663v8.24h8.24v-8.24zM10.23 8.937c0 .713-.58 1.293-1.293 1.293H.63c-.712 0-1.292-.58-1.292-1.293V.63c0-.712.58-1.292 1.292-1.292h8.308c.713 0 1.293.58 1.293 1.292v8.308zm-1.327-.034V.663H.663v8.24h8.24z" transform="translate(1 1)"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 39 KiB |
@ -3,7 +3,7 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import i18n from '@ohif/i18n';
|
||||
import { I18nextProvider } from 'react-i18next';
|
||||
import { BrowserRouter, HashRouter } from 'react-router-dom';
|
||||
import { Router } from 'react-router-dom';
|
||||
import {
|
||||
DialogProvider,
|
||||
Modal,
|
||||
@ -12,7 +12,6 @@ import {
|
||||
ThemeWrapper,
|
||||
ViewportDialogProvider,
|
||||
ViewportGridProvider,
|
||||
HangingProtocolProvider,
|
||||
CineProvider,
|
||||
} from '@ohif/ui';
|
||||
// Viewer Project
|
||||
@ -20,6 +19,7 @@ import {
|
||||
import { AppConfigProvider } from '@state';
|
||||
import createRoutes from './routes';
|
||||
import appInit from './appInit.js';
|
||||
import history from './history'
|
||||
|
||||
// TODO: Temporarily for testing
|
||||
import '@ohif/mode-longitudinal';
|
||||
@ -27,9 +27,7 @@ import '@ohif/mode-longitudinal';
|
||||
/**
|
||||
* ENV Variable to determine routing behavior
|
||||
*/
|
||||
const Router = JSON.parse(process.env.USE_HASH_ROUTER)
|
||||
? HashRouter
|
||||
: BrowserRouter;
|
||||
const OHIFRouter = Router
|
||||
|
||||
let commandsManager, extensionManager, servicesManager, hotkeysManager;
|
||||
|
||||
@ -59,32 +57,29 @@ function App({ config, defaultExtensions }) {
|
||||
UINotificationService,
|
||||
UIViewportDialogService,
|
||||
ViewportGridService, // TODO: Should this be a "UI" Service?
|
||||
HangingProtocolService,
|
||||
CineService
|
||||
} = servicesManager.services;
|
||||
|
||||
return (
|
||||
<AppConfigProvider value={appConfigState}>
|
||||
<I18nextProvider i18n={i18n}>
|
||||
<Router basename={routerBasename}>
|
||||
<OHIFRouter basename={routerBasename} history={history}>
|
||||
<ThemeWrapper>
|
||||
<ViewportGridProvider service={ViewportGridService}>
|
||||
<HangingProtocolProvider service={HangingProtocolService}>
|
||||
<ViewportDialogProvider service={UIViewportDialogService}>
|
||||
<CineProvider service={CineService}>
|
||||
<SnackbarProvider service={UINotificationService}>
|
||||
<DialogProvider service={UIDialogService}>
|
||||
<ModalProvider modal={Modal} service={UIModalService}>
|
||||
{appRoutes}
|
||||
</ModalProvider>
|
||||
</DialogProvider>
|
||||
</SnackbarProvider>
|
||||
</CineProvider>
|
||||
</ViewportDialogProvider>
|
||||
</HangingProtocolProvider>
|
||||
<ViewportDialogProvider service={UIViewportDialogService}>
|
||||
<CineProvider service={CineService}>
|
||||
<SnackbarProvider service={UINotificationService}>
|
||||
<DialogProvider service={UIDialogService}>
|
||||
<ModalProvider modal={Modal} service={UIModalService}>
|
||||
{appRoutes}
|
||||
</ModalProvider>
|
||||
</DialogProvider>
|
||||
</SnackbarProvider>
|
||||
</CineProvider>
|
||||
</ViewportDialogProvider>
|
||||
</ViewportGridProvider>
|
||||
</ThemeWrapper>
|
||||
</Router>
|
||||
</OHIFRouter>
|
||||
</I18nextProvider>
|
||||
</AppConfigProvider>
|
||||
);
|
||||
|
||||
@ -81,6 +81,7 @@ function appInit(appConfigOrFunc, defaultExtensions) {
|
||||
// appConfig.modes.push(window.segmentationMode);
|
||||
}
|
||||
|
||||
|
||||
return {
|
||||
appConfig,
|
||||
commandsManager,
|
||||
|
||||
@ -26,38 +26,38 @@ function ViewerViewportGrid(props) {
|
||||
HangingProtocolService,
|
||||
} = servicesManager.services;
|
||||
|
||||
// This is a placeholder for applying hanging protocols
|
||||
// It probably shouldn't be done here
|
||||
// For now it just hangs the first display set in the study in 1x1
|
||||
// as sorted by SeriesNumber
|
||||
|
||||
// Using Hanging protocol engine to match the displaysets
|
||||
useEffect(() => {
|
||||
const { unsubscribe } = DisplaySetService.subscribe(
|
||||
DisplaySetService.EVENTS.DISPLAY_SETS_ADDED,
|
||||
eventData => {
|
||||
const { displaySetsAdded } = eventData;
|
||||
|
||||
const data = HangingProtocolService.getState();
|
||||
|
||||
// TODO: Sometimes this is undefined?
|
||||
const { hpAlreadyApplied } = data;
|
||||
const [
|
||||
matchDetails,
|
||||
hpAlreadyApplied,
|
||||
] = HangingProtocolService.getState();
|
||||
|
||||
if (!matchDetails.length) return;
|
||||
// Match each viewport individually
|
||||
const numViewports = numRows * numCols;
|
||||
|
||||
const numViewports = viewportGrid.numRows * viewportGrid.numCols;
|
||||
for (let i = 0; i < numViewports; i++) {
|
||||
if (hpAlreadyApplied[i] === true) {
|
||||
return;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Temporary until matching is ported back over from the Meteor version.
|
||||
const reqSeriesInstanceUID =
|
||||
data.hangingProtocol.stages[0].viewports[0].seriesMatchingRules[0]
|
||||
.constraint.equals.value;
|
||||
// if current viewport doesn't have a match
|
||||
if (matchDetails[i] === undefined) return
|
||||
|
||||
const { SeriesInstanceUID } = matchDetails[i];
|
||||
const matchingDisplaySet = displaySetsAdded.find(ds => {
|
||||
return ds.SeriesInstanceUID === reqSeriesInstanceUID;
|
||||
return ds.SeriesInstanceUID === SeriesInstanceUID;
|
||||
});
|
||||
|
||||
if (!matchingDisplaySet) {
|
||||
return;
|
||||
continue;
|
||||
}
|
||||
|
||||
viewportGridService.setDisplaysetForViewport({
|
||||
@ -73,7 +73,23 @@ function ViewerViewportGrid(props) {
|
||||
return () => {
|
||||
unsubscribe();
|
||||
};
|
||||
}, []);
|
||||
}, [numRows, numCols]);
|
||||
|
||||
|
||||
// Layout change based on hanging protocols
|
||||
useEffect(() => {
|
||||
const { unsubscribe } = HangingProtocolService.subscribe(
|
||||
HangingProtocolService.EVENTS.NEW_LAYOUT,
|
||||
({ numRows, numCols }) => {
|
||||
viewportGridService.setLayout({ numRows, numCols });
|
||||
}
|
||||
);
|
||||
|
||||
return () => {
|
||||
unsubscribe();
|
||||
};
|
||||
}, [viewports]);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
const { unsubscribe } = MeasurementService.subscribe(
|
||||
@ -82,16 +98,24 @@ function ViewerViewportGrid(props) {
|
||||
const referencedDisplaySetInstanceUID =
|
||||
measurement.displaySetInstanceUID;
|
||||
|
||||
// If the viewport does not contain the displaySet, then hang that displaySet.
|
||||
const viewportsDisplaySetInstanceUIDs = viewports.map(
|
||||
vp => vp.displaySetInstanceUID
|
||||
);
|
||||
|
||||
// if we already have the displayset in one of the viewports
|
||||
if (
|
||||
viewports[viewportIndex].displaySetInstanceUID !==
|
||||
referencedDisplaySetInstanceUID
|
||||
viewportsDisplaySetInstanceUIDs.indexOf(
|
||||
referencedDisplaySetInstanceUID
|
||||
) > -1
|
||||
) {
|
||||
viewportGridService.setDisplaysetForViewport({
|
||||
viewportIndex,
|
||||
displaySetInstanceUID: referencedDisplaySetInstanceUID,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// If not in any of the viewports, hang it inside the active viewport
|
||||
viewportGridService.setDisplaysetForViewport({
|
||||
viewportIndex,
|
||||
displaySetInstanceUID: referencedDisplaySetInstanceUID,
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
@ -208,11 +232,9 @@ function ViewerViewportGrid(props) {
|
||||
return viewportPanes;
|
||||
};
|
||||
|
||||
// const ViewportPanes = React.useMemo(getViewportPanes, [
|
||||
// viewportComponents,
|
||||
// activeViewportIndex,
|
||||
// viewportGrid,
|
||||
// ]);
|
||||
if (!numCols || !numCols) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<ViewportGrid numRows={numRows} numCols={numCols}>
|
||||
|
||||
5
platform/viewer/src/history.js
Normal file
5
platform/viewer/src/history.js
Normal file
@ -0,0 +1,5 @@
|
||||
import { createBrowserHistory, createHashHistory } from 'history';
|
||||
const useHashRouting = JSON.parse(process.env.USE_HASH_ROUTER);
|
||||
const router = useHashRouting ? createHashHistory() : createBrowserHistory();
|
||||
|
||||
export default router;
|
||||
@ -1,14 +1,57 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import React, { useEffect, useState, useRef } from 'react';
|
||||
import { useParams } from 'react-router';
|
||||
import PropTypes from 'prop-types';
|
||||
// TODO: DicomMetadataStore should be injected?
|
||||
import { DicomMetadataStore, utils } from '@ohif/core';
|
||||
import { DicomMetadataStore } from '@ohif/core';
|
||||
import { DragAndDropProvider, ImageViewerProvider } from '@ohif/ui';
|
||||
import { useQuery } from '@hooks';
|
||||
import ViewportGrid from '@components/ViewportGrid';
|
||||
import Compose from './Compose';
|
||||
|
||||
const { isLowPriorityModality } = utils;
|
||||
async function defaultRouteInit({
|
||||
servicesManager,
|
||||
studyInstanceUIDs,
|
||||
dataSource,
|
||||
}) {
|
||||
const {
|
||||
DisplaySetService,
|
||||
HangingProtocolService,
|
||||
} = servicesManager.services;
|
||||
|
||||
const unsubscriptions = [];
|
||||
// TODO: This should be baked into core, not manuall?
|
||||
// DisplaySetService would wire this up?
|
||||
const {
|
||||
unsubscribe: instanceAddedUnsubscribe,
|
||||
} = DicomMetadataStore.subscribe(
|
||||
DicomMetadataStore.EVENTS.INSTANCES_ADDED,
|
||||
({ StudyInstanceUID, SeriesInstanceUID, madeInClient = false }) => {
|
||||
const seriesMetadata = DicomMetadataStore.getSeries(
|
||||
StudyInstanceUID,
|
||||
SeriesInstanceUID
|
||||
);
|
||||
|
||||
DisplaySetService.makeDisplaySets(seriesMetadata.instances, madeInClient);
|
||||
}
|
||||
);
|
||||
|
||||
unsubscriptions.push(instanceAddedUnsubscribe);
|
||||
|
||||
studyInstanceUIDs.forEach(StudyInstanceUID => {
|
||||
dataSource.retrieveSeriesMetadata({ StudyInstanceUID });
|
||||
});
|
||||
|
||||
const { unsubscribe: seriesAddedUnsubscribe } = DicomMetadataStore.subscribe(
|
||||
DicomMetadataStore.EVENTS.SERIES_ADDED,
|
||||
({ StudyInstanceUID }) => {
|
||||
const studyMetadata = DicomMetadataStore.getStudy(StudyInstanceUID);
|
||||
HangingProtocolService.run(studyMetadata);
|
||||
}
|
||||
);
|
||||
unsubscriptions.push(seriesAddedUnsubscribe);
|
||||
|
||||
return unsubscriptions;
|
||||
}
|
||||
|
||||
export default function ModeRoute({
|
||||
location,
|
||||
@ -20,15 +63,25 @@ export default function ModeRoute({
|
||||
}) {
|
||||
// Parse route params/querystring
|
||||
const query = useQuery();
|
||||
const queryStudyInstanceUIDs = query.get('StudyInstanceUIDs');
|
||||
const { StudyInstanceUIDs: paramsStudyInstanceUIDs } = useParams();
|
||||
const StudyInstanceUIDs = queryStudyInstanceUIDs || paramsStudyInstanceUIDs;
|
||||
const StudyInstanceUIDsAsArray =
|
||||
StudyInstanceUIDs && Array.isArray(StudyInstanceUIDs)
|
||||
? StudyInstanceUIDs
|
||||
: [StudyInstanceUIDs];
|
||||
const params = useParams();
|
||||
|
||||
const { extensions, sopClassHandlers, hotkeys } = mode;
|
||||
const [studyInstanceUIDs, setStudyInstanceUIDs] = useState();
|
||||
|
||||
const [refresh, setRefresh] = useState(false);
|
||||
const layoutTemplateData = useRef(false);
|
||||
const locationRef = useRef(null);
|
||||
const isMounted = useRef(false);
|
||||
|
||||
if (location !== locationRef.current) {
|
||||
layoutTemplateData.current = null;
|
||||
locationRef.current = location;
|
||||
}
|
||||
|
||||
const {
|
||||
DisplaySetService,
|
||||
HangingProtocolService,
|
||||
} = servicesManager.services;
|
||||
const { extensions, sopClassHandlers, hotkeys, hangingProtocols } = mode;
|
||||
|
||||
if (dataSourceName === undefined) {
|
||||
dataSourceName = extensionManager.defaultDataSourceName;
|
||||
@ -43,19 +96,6 @@ export default function ModeRoute({
|
||||
// Only handling one route per mode for now
|
||||
const route = mode.routes[0];
|
||||
|
||||
const {
|
||||
DisplaySetService,
|
||||
MeasurementService,
|
||||
ViewportGridService,
|
||||
HangingProtocolService,
|
||||
} = servicesManager.services;
|
||||
|
||||
const layoutTemplateData = route.layoutTemplate({ location });
|
||||
const layoutTemplateModuleEntry = extensionManager.getModuleEntry(
|
||||
layoutTemplateData.id
|
||||
);
|
||||
const LayoutComponent = layoutTemplateModuleEntry.component;
|
||||
|
||||
// For each extension, look up their context modules
|
||||
// TODO: move to extension manager.
|
||||
let contextModules = [];
|
||||
@ -81,9 +121,52 @@ export default function ModeRoute({
|
||||
return ViewportGrid({ ...props, dataSource });
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
// Preventing state update for unmounted component
|
||||
isMounted.current = true;
|
||||
return () => {
|
||||
isMounted.current = false;
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
// Todo: this should not be here, data source should not care about params
|
||||
const initializeDataSource = async (params, query) => {
|
||||
const studyInstanceUIDs = await dataSource.parseRouteParams({
|
||||
params,
|
||||
query,
|
||||
});
|
||||
setStudyInstanceUIDs(studyInstanceUIDs);
|
||||
};
|
||||
|
||||
initializeDataSource(params, query);
|
||||
return () => {
|
||||
layoutTemplateData.current = null;
|
||||
};
|
||||
}, [location]);
|
||||
|
||||
useEffect(() => {
|
||||
const retrieveLayoutData = async () => {
|
||||
const layoutData = await route.layoutTemplate({
|
||||
location,
|
||||
servicesManager,
|
||||
studyInstanceUIDs,
|
||||
});
|
||||
if (isMounted.current) {
|
||||
layoutTemplateData.current = layoutData;
|
||||
setRefresh(!refresh);
|
||||
}
|
||||
};
|
||||
if (studyInstanceUIDs?.length && studyInstanceUIDs[0] !== undefined) {
|
||||
retrieveLayoutData();
|
||||
}
|
||||
return () => {
|
||||
layoutTemplateData.current = null;
|
||||
};
|
||||
}, [studyInstanceUIDs]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!hotkeys) {
|
||||
console.warn('[hotkeys] No bindings defined for hotkeys hook!');
|
||||
return;
|
||||
}
|
||||
|
||||
@ -96,19 +179,61 @@ export default function ModeRoute({
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!layoutTemplateData.current) {
|
||||
return;
|
||||
}
|
||||
// TODO: For some reason this is running before the Providers
|
||||
// are calling setServiceImplementation
|
||||
// are calling setServiceImplementationf
|
||||
// TOOD -> iterate through services.
|
||||
|
||||
// Extension
|
||||
|
||||
// Add SOPClassHandlers to a new SOPClassManager.
|
||||
DisplaySetService.init(extensionManager, sopClassHandlers);
|
||||
|
||||
extensionManager.onModeEnter();
|
||||
mode?.onModeEnter({ servicesManager, extensionManager });
|
||||
// Mode
|
||||
route.init({ servicesManager, extensionManager });
|
||||
|
||||
// Adding hanging protocols of extensions after onModeEnter since
|
||||
// it will reset the protocols
|
||||
hangingProtocols.forEach(extentionProtocols => {
|
||||
const hangingProtocolModule = extensionManager.getModuleEntry(extentionProtocols);
|
||||
if (hangingProtocolModule?.protocols) {
|
||||
HangingProtocolService.addProtocols(hangingProtocolModule.protocols);
|
||||
}
|
||||
});
|
||||
|
||||
const setupRouteInit = async () => {
|
||||
if (route.init) {
|
||||
return await route.init({
|
||||
servicesManager,
|
||||
extensionManager,
|
||||
hotkeysManager,
|
||||
studyInstanceUIDs,
|
||||
dataSource,
|
||||
});
|
||||
}
|
||||
|
||||
return await defaultRouteInit({
|
||||
servicesManager,
|
||||
extensionManager,
|
||||
hotkeysManager,
|
||||
studyInstanceUIDs,
|
||||
dataSource,
|
||||
});
|
||||
};
|
||||
|
||||
let unsubscriptions;
|
||||
setupRouteInit().then(unsubs => {
|
||||
unsubscriptions = unsubs;
|
||||
});
|
||||
|
||||
return () => {
|
||||
extensionManager.onModeExit();
|
||||
mode?.onModeExit({ servicesManager, extensionManager });
|
||||
unsubscriptions.forEach(unsub => {
|
||||
unsub();
|
||||
});
|
||||
};
|
||||
}, [
|
||||
mode,
|
||||
@ -118,140 +243,35 @@ export default function ModeRoute({
|
||||
servicesManager,
|
||||
extensionManager,
|
||||
hotkeysManager,
|
||||
studyInstanceUIDs,
|
||||
refresh,
|
||||
hangingProtocols,
|
||||
]);
|
||||
|
||||
// This queries for series, but... What does it do with them?
|
||||
useEffect(() => {
|
||||
// Add SOPClassHandlers to a new SOPClassManager.
|
||||
DisplaySetService.init(extensionManager, sopClassHandlers);
|
||||
|
||||
// TODO: This should be baked into core, not manuel?
|
||||
// DisplaySetService would wire this up?
|
||||
const { unsubscribe } = DicomMetadataStore.subscribe(
|
||||
DicomMetadataStore.EVENTS.INSTANCES_ADDED,
|
||||
({ StudyInstanceUID, SeriesInstanceUID, madeInClient = false }) => {
|
||||
const seriesMetadata = DicomMetadataStore.getSeries(
|
||||
StudyInstanceUID,
|
||||
SeriesInstanceUID
|
||||
);
|
||||
|
||||
DisplaySetService.makeDisplaySets(
|
||||
seriesMetadata.instances,
|
||||
madeInClient
|
||||
);
|
||||
}
|
||||
const renderLayoutData = props => {
|
||||
const layoutTemplateModuleEntry = extensionManager.getModuleEntry(
|
||||
layoutTemplateData.current.id
|
||||
);
|
||||
const LayoutComponent = layoutTemplateModuleEntry.component;
|
||||
|
||||
StudyInstanceUIDsAsArray.forEach(StudyInstanceUID => {
|
||||
dataSource.retrieveSeriesMetadata({ StudyInstanceUID });
|
||||
});
|
||||
|
||||
return unsubscribe;
|
||||
}, [
|
||||
mode,
|
||||
dataSourceName,
|
||||
location,
|
||||
DisplaySetService,
|
||||
extensionManager,
|
||||
sopClassHandlers,
|
||||
StudyInstanceUIDsAsArray,
|
||||
dataSource,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
const { unsubscribe } = DicomMetadataStore.subscribe(
|
||||
DicomMetadataStore.EVENTS.SERIES_ADDED,
|
||||
({ StudyInstanceUID }) => {
|
||||
const studyMetadata = DicomMetadataStore.getStudy(StudyInstanceUID);
|
||||
|
||||
const sortedSeries = studyMetadata.series.sort((a, b) => {
|
||||
const aLowPriority = isLowPriorityModality(a.Modality);
|
||||
const bLowPriority = isLowPriorityModality(b.Modality);
|
||||
if (!aLowPriority && bLowPriority) {
|
||||
return -1;
|
||||
}
|
||||
if (aLowPriority && !bLowPriority) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return a.SeriesNumber - b.SeriesNumber;
|
||||
});
|
||||
|
||||
const { SeriesInstanceUID } = sortedSeries[0];
|
||||
|
||||
HangingProtocolService.setHangingProtocol({
|
||||
/*protocolMatchingRules: [
|
||||
{
|
||||
id: '7tmuq7KzDMCWFeapc',
|
||||
weight: 2,
|
||||
required: false,
|
||||
attribute: 'x00081030',
|
||||
constraint: {
|
||||
contains: {
|
||||
value: 'DFCI CT CHEST',
|
||||
},
|
||||
},
|
||||
},
|
||||
],*/
|
||||
stages: [
|
||||
{
|
||||
/*id: 'v5PfGt9F6mffZPif5',
|
||||
viewportStructure: {
|
||||
type: 'grid',
|
||||
properties: {
|
||||
Rows: 1,
|
||||
Columns: 1,
|
||||
},
|
||||
layoutTemplateName: 'gridLayout',
|
||||
},*/
|
||||
viewports: [
|
||||
{
|
||||
viewportSettings: {},
|
||||
imageMatchingRules: [],
|
||||
seriesMatchingRules: [
|
||||
{
|
||||
id: 'mXnsCcNzZL56z7mTZ',
|
||||
weight: 1,
|
||||
required: true,
|
||||
attribute: 'SeriesInstanceUID',
|
||||
constraint: {
|
||||
equals: {
|
||||
value: SeriesInstanceUID,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
studyMatchingRules: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
);
|
||||
return unsubscribe;
|
||||
}, [
|
||||
mode,
|
||||
dataSourceName,
|
||||
location,
|
||||
DisplaySetService,
|
||||
extensionManager,
|
||||
sopClassHandlers,
|
||||
StudyInstanceUIDsAsArray,
|
||||
dataSource,
|
||||
]);
|
||||
return <LayoutComponent {...props} />;
|
||||
};
|
||||
|
||||
return (
|
||||
<ImageViewerProvider
|
||||
initialState={{ StudyInstanceUIDs: StudyInstanceUIDsAsArray }}
|
||||
// initialState={{ StudyInstanceUIDs: StudyInstanceUIDs }}
|
||||
StudyInstanceUIDs={studyInstanceUIDs}
|
||||
// reducer={reducer}
|
||||
>
|
||||
<CombinedContextProvider>
|
||||
<DragAndDropProvider>
|
||||
<LayoutComponent
|
||||
{...layoutTemplateData.props}
|
||||
StudyInstanceUIDs={StudyInstanceUIDs}
|
||||
ViewportGridComp={ViewportGridWithDataSource}
|
||||
/>
|
||||
{layoutTemplateData.current &&
|
||||
studyInstanceUIDs?.length &&
|
||||
studyInstanceUIDs[0] !== undefined &&
|
||||
renderLayoutData({
|
||||
...layoutTemplateData.current.props,
|
||||
ViewportGridComp: ViewportGridWithDataSource,
|
||||
})}
|
||||
</DragAndDropProvider>
|
||||
</CombinedContextProvider>
|
||||
</ImageViewerProvider>
|
||||
|
||||
@ -408,7 +408,7 @@ function WorkList({
|
||||
'h-screen': !hasStudies,
|
||||
})}
|
||||
>
|
||||
<Header isSticky menuOptions={menuOptions} isReturnEnabled={false} />
|
||||
<Header isSticky menuOptions={menuOptions} isReturnEnabled={false} WhiteLabeling={appConfig.whiteLabeling} />
|
||||
<StudyListFilter
|
||||
numOfStudies={pageNumber * resultsPerPage > 100 ? 101 : numOfStudies}
|
||||
filtersMeta={filtersMeta}
|
||||
|
||||
140
yarn.lock
140
yarn.lock
@ -3729,10 +3729,10 @@ acorn-globals@^4.1.0:
|
||||
acorn "^6.0.1"
|
||||
acorn-walk "^6.0.1"
|
||||
|
||||
acorn-jsx@^5.0.0, acorn-jsx@^5.2.0:
|
||||
version "5.3.1"
|
||||
resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.1.tgz#fc8661e11b7ac1539c47dbfea2e72b3af34d267b"
|
||||
integrity sha512-K0Ptm/47OKfQRpNQ2J/oIN/3QYiK6FwW+eJbILhsdxh2WTLdl+30o8aGdTbm5JbffpFFAg/g+zi1E+jvJha5ng==
|
||||
acorn-jsx@^5.2.0:
|
||||
version "5.2.0"
|
||||
resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.2.0.tgz#4c66069173d6fdd68ed85239fc256226182b2ebe"
|
||||
integrity sha512-HiUX/+K2YpkpJ+SzBffkM/AQ2YE03S0U1kjTLVpoJdhZMOWy8qvXVN9JdLqv2QsaQ6MPYQIuNmwD8zOiYUofLQ==
|
||||
|
||||
acorn-node@^1.6.1:
|
||||
version "1.8.2"
|
||||
@ -3758,10 +3758,10 @@ acorn@^5.0.3, acorn@^5.5.3:
|
||||
resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.7.4.tgz#3e8d8a9947d0599a1796d10225d7432f4a4acf5e"
|
||||
integrity sha512-1D++VG7BhrtvQpNbBzovKNc1FLGGEE/oGe7b9xJm/RFHMBeUaUGpluV9RLjZa47YFdPcDAenEYuq9pQPcMdLJg==
|
||||
|
||||
acorn@^6.0.1, acorn@^6.0.7, acorn@^6.4.1:
|
||||
version "6.4.2"
|
||||
resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.4.2.tgz#35866fd710528e92de10cf06016498e47e39e1e6"
|
||||
integrity sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ==
|
||||
acorn@^6.0.1, acorn@^6.4.1:
|
||||
version "6.4.1"
|
||||
resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.4.1.tgz#531e58ba3f51b9dacb9a6646ca4debf5b14ca474"
|
||||
integrity sha512-ZVA9k326Nwrj3Cj9jlh3wGFutC2ZornPNARZwsNYqQYgN0EsV2d53w5RN/co65Ohn4sUAUtb1rSUAOD6XN9idA==
|
||||
|
||||
acorn@^7.0.0, acorn@^7.1.0, acorn@^7.1.1:
|
||||
version "7.4.1"
|
||||
@ -3857,7 +3857,17 @@ ajv@6.5.3:
|
||||
json-schema-traverse "^0.4.1"
|
||||
uri-js "^4.2.2"
|
||||
|
||||
ajv@^6.1.0, ajv@^6.10.0, ajv@^6.10.2, ajv@^6.12.3, ajv@^6.12.4, ajv@^6.12.5, ajv@^6.9.1:
|
||||
ajv@^6.1.0, ajv@^6.10.0, ajv@^6.10.2:
|
||||
version "6.12.2"
|
||||
resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.2.tgz#c629c5eced17baf314437918d2da88c99d5958cd"
|
||||
integrity sha512-k+V+hzjm5q/Mr8ef/1Y9goCmlsK4I6Sm74teeyGvFk1XrOsbsKLjEdrvny42CZ+a8sXbk8KWpY/bDwS+FLL2UQ==
|
||||
dependencies:
|
||||
fast-deep-equal "^3.1.1"
|
||||
fast-json-stable-stringify "^2.0.0"
|
||||
json-schema-traverse "^0.4.1"
|
||||
uri-js "^4.2.2"
|
||||
|
||||
ajv@^6.12.3, ajv@^6.12.4, ajv@^6.12.5:
|
||||
version "6.12.6"
|
||||
resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4"
|
||||
integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==
|
||||
@ -6388,7 +6398,7 @@ cornerstone-math@0.1.7:
|
||||
resolved "https://registry.yarnpkg.com/cornerstone-math/-/cornerstone-math-0.1.7.tgz#7c55536f02e7221b05fc49a4c780bb82c234ce39"
|
||||
integrity sha512-svsDSoqLNFM9niCV2AtV2DNmRkYztT1v80lVdbA5hkgdGdpWg5/h2On794zbqBK+aI3hNOHufERDsI4wdXGknA==
|
||||
|
||||
cornerstone-math@0.1.9, cornerstone-math@^0.1.8:
|
||||
cornerstone-math@0.1.9, cornerstone-math@^0.1.9:
|
||||
version "0.1.9"
|
||||
resolved "https://registry.yarnpkg.com/cornerstone-math/-/cornerstone-math-0.1.9.tgz#7ce5509e8b9f465b01f7c548470725e7569859fc"
|
||||
integrity sha512-NxdooV73asEQgav1S+0e+a4K+W3CXJdLXyFkVN24qqCtmIpzZzwtw3F9KWPCekzSAJmbhtQ3HicOQj3d4vRtuw==
|
||||
@ -8226,10 +8236,10 @@ eslint-plugin-promise@^4.2.1:
|
||||
resolved "https://registry.yarnpkg.com/eslint-plugin-promise/-/eslint-plugin-promise-4.3.1.tgz#61485df2a359e03149fdafc0a68b0e030ad2ac45"
|
||||
integrity sha512-bY2sGqyptzFBDLh/GMbAxfdJC+b0f23ME63FOE4+Jao0oZ3E1LEwFtWJX/1pGMJLiTtrSSern2CRM/g+dfc0eQ==
|
||||
|
||||
eslint-plugin-react-hooks@1.5.0:
|
||||
version "1.5.0"
|
||||
resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-1.5.0.tgz#cdd958cfff55bd5fa4f84db90d1490fb5ca4ae2b"
|
||||
integrity sha512-iwDuWR2ReRgvJsNm8fXPtTKdg78IVQF8I4+am3ntztPf/+nPnWZfArFu6aXpaC75/iCYRrkqI8nPCYkxJstmpA==
|
||||
eslint-plugin-react-hooks@4.2.0:
|
||||
version "4.2.0"
|
||||
resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.2.0.tgz#8c229c268d468956334c943bb45fc860280f5556"
|
||||
integrity sha512-623WEiZJqxR7VdxFCKLI6d6LLpwJkGPYKODnkH3D7WpOG5KM8yWueBd8TLsNAetEJNF5iJmolaAKO3F8yzyVBQ==
|
||||
|
||||
eslint-plugin-react-hooks@^1.7.0:
|
||||
version "1.7.0"
|
||||
@ -8277,7 +8287,7 @@ eslint-scope@^5.0.0, eslint-scope@^5.1.1:
|
||||
esrecurse "^4.3.0"
|
||||
estraverse "^4.1.1"
|
||||
|
||||
eslint-utils@^1.3.1, eslint-utils@^1.4.2, eslint-utils@^1.4.3:
|
||||
eslint-utils@^1.4.2, eslint-utils@^1.4.3:
|
||||
version "1.4.3"
|
||||
resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-1.4.3.tgz#74fec7c54d0776b6f67e0251040b5806564e981f"
|
||||
integrity sha512-fbBN5W2xdY45KulGXmLHZ3c3FHfVYmKg0IrAKGOkT/464PQsx2UeIzfz1RmEci+KLm1bBaAzZAh8+/E+XAeZ8Q==
|
||||
@ -8296,49 +8306,7 @@ eslint-visitor-keys@^1.0.0, eslint-visitor-keys@^1.1.0:
|
||||
resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz#30ebd1ef7c2fdff01c3a4f151044af25fab0523e"
|
||||
integrity sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==
|
||||
|
||||
eslint@5.16.0:
|
||||
version "5.16.0"
|
||||
resolved "https://registry.yarnpkg.com/eslint/-/eslint-5.16.0.tgz#a1e3ac1aae4a3fbd8296fcf8f7ab7314cbb6abea"
|
||||
integrity sha512-S3Rz11i7c8AA5JPv7xAH+dOyq/Cu/VXHiHXBPOU1k/JAM5dXqQPt3qcrhpHSorXmrpu2g0gkIBVXAqCpzfoZIg==
|
||||
dependencies:
|
||||
"@babel/code-frame" "^7.0.0"
|
||||
ajv "^6.9.1"
|
||||
chalk "^2.1.0"
|
||||
cross-spawn "^6.0.5"
|
||||
debug "^4.0.1"
|
||||
doctrine "^3.0.0"
|
||||
eslint-scope "^4.0.3"
|
||||
eslint-utils "^1.3.1"
|
||||
eslint-visitor-keys "^1.0.0"
|
||||
espree "^5.0.1"
|
||||
esquery "^1.0.1"
|
||||
esutils "^2.0.2"
|
||||
file-entry-cache "^5.0.1"
|
||||
functional-red-black-tree "^1.0.1"
|
||||
glob "^7.1.2"
|
||||
globals "^11.7.0"
|
||||
ignore "^4.0.6"
|
||||
import-fresh "^3.0.0"
|
||||
imurmurhash "^0.1.4"
|
||||
inquirer "^6.2.2"
|
||||
js-yaml "^3.13.0"
|
||||
json-stable-stringify-without-jsonify "^1.0.1"
|
||||
levn "^0.3.0"
|
||||
lodash "^4.17.11"
|
||||
minimatch "^3.0.4"
|
||||
mkdirp "^0.5.1"
|
||||
natural-compare "^1.4.0"
|
||||
optionator "^0.8.2"
|
||||
path-is-inside "^1.0.2"
|
||||
progress "^2.0.0"
|
||||
regexpp "^2.0.1"
|
||||
semver "^5.5.1"
|
||||
strip-ansi "^4.0.0"
|
||||
strip-json-comments "^2.0.1"
|
||||
table "^5.2.3"
|
||||
text-table "^0.2.0"
|
||||
|
||||
eslint@^6.8.0:
|
||||
eslint@6.8.0, eslint@^6.8.0:
|
||||
version "6.8.0"
|
||||
resolved "https://registry.yarnpkg.com/eslint/-/eslint-6.8.0.tgz#62262d6729739f9275723824302fb227c8c93ffb"
|
||||
integrity sha512-K+Iayyo2LtyYhDSYwz5D5QdWw0hCacNzyq1Y821Xna2xSJj7cijoLLYmLxTQgcgZ9mC61nryMy9S7GRbYpI5Ig==
|
||||
@ -8381,15 +8349,6 @@ eslint@^6.8.0:
|
||||
text-table "^0.2.0"
|
||||
v8-compile-cache "^2.0.3"
|
||||
|
||||
espree@^5.0.1:
|
||||
version "5.0.1"
|
||||
resolved "https://registry.yarnpkg.com/espree/-/espree-5.0.1.tgz#5d6526fa4fc7f0788a5cf75b15f30323e2f81f7a"
|
||||
integrity sha512-qWAZcWh4XE/RwzLJejfcofscgMc9CamR6Tn1+XRXNzrvUSSbiAjGOI/fggztjIi7y9VLPqnICMIPiGyr8JaZ0A==
|
||||
dependencies:
|
||||
acorn "^6.0.7"
|
||||
acorn-jsx "^5.0.0"
|
||||
eslint-visitor-keys "^1.0.0"
|
||||
|
||||
espree@^6.1.2:
|
||||
version "6.2.1"
|
||||
resolved "https://registry.yarnpkg.com/espree/-/espree-6.2.1.tgz#77fc72e1fd744a2052c20f38a5b575832e82734a"
|
||||
@ -10284,7 +10243,7 @@ global@^4.3.0:
|
||||
min-document "^2.19.0"
|
||||
process "^0.11.10"
|
||||
|
||||
globals@^11.1.0, globals@^11.7.0:
|
||||
globals@^11.1.0:
|
||||
version "11.12.0"
|
||||
resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e"
|
||||
integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==
|
||||
@ -11405,7 +11364,7 @@ inquirer@3.3.0:
|
||||
strip-ansi "^4.0.0"
|
||||
through "^2.3.6"
|
||||
|
||||
inquirer@^6.2.0, inquirer@^6.2.2:
|
||||
inquirer@^6.2.0:
|
||||
version "6.5.2"
|
||||
resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-6.5.2.tgz#ad50942375d036d327ff528c08bd5fab089928ca"
|
||||
integrity sha512-cntlB5ghuB0iuO65Ovoi8ogLHiWGs/5yNrtUcKjFhSSiVeAIVpD7koaSU9RM8mpXw5YDi9RdYXGQMaOURB7ycQ==
|
||||
@ -12591,10 +12550,10 @@ js-tokens@^3.0.1, js-tokens@^3.0.2:
|
||||
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b"
|
||||
integrity sha1-mGbfOVECEw449/mWvOtlRDIJwls=
|
||||
|
||||
js-yaml@^3.11.0, js-yaml@^3.13.0, js-yaml@^3.13.1, js-yaml@^3.9.0:
|
||||
version "3.14.1"
|
||||
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.1.tgz#dae812fdb3825fa306609a8717383c50c36a0537"
|
||||
integrity sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==
|
||||
js-yaml@^3.11.0, js-yaml@^3.13.1, js-yaml@^3.9.0:
|
||||
version "3.14.0"
|
||||
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.0.tgz#a7a34170f26a21bb162424d8adacb4113a69e482"
|
||||
integrity sha512-/4IbIeHcD9VMHFqDR/gQ7EdZdLimOvW2DdcxFjdyyZ9NsbS+ccrXqVWDtab/lRl5AlUqmpBx8EhPaWR+OtY17A==
|
||||
dependencies:
|
||||
argparse "^1.0.7"
|
||||
esprima "^4.0.0"
|
||||
@ -14893,7 +14852,7 @@ optimize-css-assets-webpack-plugin@^5.0.3:
|
||||
cssnano "^4.1.10"
|
||||
last-call-webpack-plugin "^3.0.0"
|
||||
|
||||
optionator@^0.8.1, optionator@^0.8.2, optionator@^0.8.3:
|
||||
optionator@^0.8.1, optionator@^0.8.3:
|
||||
version "0.8.3"
|
||||
resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.3.tgz#84fa1d036fe9d3c7e21d99884b601167ec8fb495"
|
||||
integrity sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==
|
||||
@ -16849,10 +16808,10 @@ react-ace@^7.0.2:
|
||||
lodash.isequal "^4.5.0"
|
||||
prop-types "^15.7.2"
|
||||
|
||||
react-cornerstone-viewport@4.0.2:
|
||||
version "4.0.2"
|
||||
resolved "https://registry.yarnpkg.com/react-cornerstone-viewport/-/react-cornerstone-viewport-4.0.2.tgz#49bc8f8464164d8e779138dd9244d5c2524e0418"
|
||||
integrity sha512-mF/piahoPG5U5TbxVMtc15bXrQZxcaPycbu4i4KD2rZkiWh1Zs24ryY1S+lFlDB/RcOAm0u+luEEt1tOg8F+UQ==
|
||||
react-cornerstone-viewport@4.0.4:
|
||||
version "4.0.4"
|
||||
resolved "https://registry.yarnpkg.com/react-cornerstone-viewport/-/react-cornerstone-viewport-4.0.4.tgz#0e4291d9b0fe55c003e275175b0902ced61b0c80"
|
||||
integrity sha512-RjCqOtQep04dRoHtsju0USlxN/vGUI2WnEt1aN84iYU7eZ+AXnwJo1nAd0RMX2obcXaGCIqMDtMRkE/zm3oZHw==
|
||||
dependencies:
|
||||
classnames "^2.2.6"
|
||||
date-fns "^2.2.1"
|
||||
@ -16860,10 +16819,10 @@ react-cornerstone-viewport@4.0.2:
|
||||
prop-types "^15.7.2"
|
||||
react-resize-detector "^4.2.1"
|
||||
|
||||
react-cornerstone-viewport@4.0.4:
|
||||
version "4.0.4"
|
||||
resolved "https://registry.yarnpkg.com/react-cornerstone-viewport/-/react-cornerstone-viewport-4.0.4.tgz#0e4291d9b0fe55c003e275175b0902ced61b0c80"
|
||||
integrity sha512-RjCqOtQep04dRoHtsju0USlxN/vGUI2WnEt1aN84iYU7eZ+AXnwJo1nAd0RMX2obcXaGCIqMDtMRkE/zm3oZHw==
|
||||
react-cornerstone-viewport@4.0.5:
|
||||
version "4.0.5"
|
||||
resolved "https://registry.yarnpkg.com/react-cornerstone-viewport/-/react-cornerstone-viewport-4.0.5.tgz#3de83cdd3643980e0330f3743f631dbbb24d98a7"
|
||||
integrity sha512-uamDtFljWWEHe26kOBJJ1Bu//6hAgEra7BDVykOvtMC/W7kW34Q2fFX3ZwkL6TDdCQ94O6K98IXPS9HbrEJWYg==
|
||||
dependencies:
|
||||
classnames "^2.2.6"
|
||||
date-fns "^2.2.1"
|
||||
@ -19549,16 +19508,16 @@ strip-indent@^3.0.0:
|
||||
dependencies:
|
||||
min-indent "^1.0.0"
|
||||
|
||||
strip-json-comments@^2.0.1, strip-json-comments@~2.0.1:
|
||||
strip-json-comments@^3.0.1:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.0.tgz#7638d31422129ecf4457440009fba03f9f9ac180"
|
||||
integrity sha512-e6/d0eBu7gHtdCqFt0xJr642LdToM5/cN4Qb9DbHjVx1CP5RyeM+zH7pbecEmDv/lBqb0QH+6Uqq75rxFPkM0w==
|
||||
|
||||
strip-json-comments@~2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a"
|
||||
integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo=
|
||||
|
||||
strip-json-comments@^3.0.1:
|
||||
version "3.1.1"
|
||||
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006"
|
||||
integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==
|
||||
|
||||
strong-log-transformer@^2.0.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/strong-log-transformer/-/strong-log-transformer-2.1.0.tgz#0f5ed78d325e0421ac6f90f7f10e691d6ae3ae10"
|
||||
@ -20972,6 +20931,11 @@ validate-npm-package-name@^3.0.0:
|
||||
dependencies:
|
||||
builtins "^1.0.3"
|
||||
|
||||
validate.js@^0.12.0:
|
||||
version "0.12.0"
|
||||
resolved "https://registry.yarnpkg.com/validate.js/-/validate.js-0.12.0.tgz#17f989e37c192ea2f826bbf19bf4e97e6e4be68f"
|
||||
integrity sha512-/x2RJSvbqEyxKj0RPN4xaRquK+EggjeVXiDDEyrJzsJogjtiZ9ov7lj/svVb4DM5Q5braQF4cooAryQbUwOxlA==
|
||||
|
||||
value-equal@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/value-equal/-/value-equal-1.0.1.tgz#1e0b794c734c5c0cade179c437d356d931a34d6c"
|
||||
|
||||
Loading…
Reference in New Issue
Block a user