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

* modified

* new files in existing extensions/libraries/modes

* fixed bugs and tests

* Clean up

* fix: viewport display fixed

* Fixed configs for deployment

* Removed unnecessary files and functions

* Fixed default HP module

* Added white labelling

* Fixed hash routing

* Removed unnecessary routers

Co-authored-by: Alireza Sedghi <ar.sedghi@gmail.com>
This commit is contained in:
Erik Ziegler 2021-05-28 14:32:48 -04:00 committed by GitHub
parent 0db81b30f3
commit 99b8dc4759
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
93 changed files with 3825 additions and 1293 deletions

2
.gitignore vendored
View File

@ -14,7 +14,7 @@ coverage/
# YALC (for Erik)
.yalc
yalc.lock
*.dcm
# Logging, System files, misc.
.idea/
.npm

View File

@ -32,7 +32,7 @@
"@ohif/core": "^0.50.0",
"@ohif/ui": "^0.50.0",
"cornerstone-core": "^2.3.0",
"cornerstone-math": "^0.1.8",
"cornerstone-math": "^0.1.9",
"cornerstone-tools": "5.1.2",
"cornerstone-wado-image-loader": "^3.1.2",
"dcmjs": "0.16.1",
@ -47,6 +47,6 @@
"@babel/runtime": "7.7.6",
"lodash.merge": "^4.6.2",
"lodash.debounce": "4.0.8",
"react-cornerstone-viewport": "4.0.2"
"react-cornerstone-viewport": "4.0.5"
}
}

View File

@ -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);
}
}

View 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;

View 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;

View 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 };

View File

@ -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', {});
}

View File

@ -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
}
});
}

View File

@ -9,6 +9,30 @@ const scroll = cornerstoneTools.import('util/scroll');
const { studyMetadataManager } = OHIF.utils;
const imagePositionSynchronizer = new cornerstoneTools.Synchronizer(
cornerstone.EVENTS.NEW_IMAGE,
cornerstoneTools.stackImagePositionSynchronizer
);
const panZoomSynchronizer = new cornerstoneTools.Synchronizer(
cornerstone.EVENTS.IMAGE_RENDERED,
cornerstoneTools.panZoomSynchronizer
);
function onElementEnabledAddToSync(event) {
const { element } = event.detail;
imagePositionSynchronizer.add(element);
// panZoomSynchronizer.add(element);
}
function onElementDisabledRemoveFromSync(event) {
const { element } = event.detail;
imagePositionSynchronizer.remove(element);
panZoomSynchronizer.remove(element);
}
const commandsModule = ({ servicesManager, commandsManager }) => {
const { ViewportGridService } = servicesManager.services;
@ -70,8 +94,60 @@ const commandsModule = ({ servicesManager, commandsManager }) => {
cornerstone.reset(enabledElement);
}
},
invertViewport: () => {
const enabledElement = _getActiveViewportsEnabledElement();
toggleSynchronizer: ({ toggledState }) => {
const synchronizers = [imagePositionSynchronizer];
// Set synchronizer state when the command is run.
synchronizers.forEach(s => {
s.enabled = toggledState;
});
const unsubscribe = () => {
cornerstone.events.removeEventListener(
cornerstone.EVENTS.ELEMENT_ENABLED,
onElementEnabledAddToSync
);
cornerstone.events.removeEventListener(
cornerstone.EVENTS.ELEMENT_DISABLED,
onElementDisabledRemoveFromSync
);
};
const subscribe = () => {
cornerstone.events.addEventListener(
cornerstone.EVENTS.ELEMENT_ENABLED,
onElementEnabledAddToSync
);
cornerstone.events.addEventListener(
cornerstone.EVENTS.ELEMENT_DISABLED,
onElementDisabledRemoveFromSync
);
};
// Add event handlers so that if the layout is changed, new elements
// are automatically added to the synchronizer while it is enabled.
if (toggledState === true) {
subscribe();
} else {
// If the synchronizer is disabled, remove the event handlers
unsubscribe();
}
// Erase existing state and then set up all currently existing elements
cornerstone.getEnabledElements().map(e => {
synchronizers.forEach(s => {
s.remove(e.element);
s.add(e.element);
});
});
return unsubscribe;
},
invertViewport: ({ element }) => {
let enabledElement;
if (element === undefined) {
enabledElement = _getActiveViewportsEnabledElement();
} else {
enabledElement = element;
}
if (enabledElement) {
let viewport = cornerstone.getViewport(enabledElement);
@ -111,11 +187,11 @@ const commandsModule = ({ servicesManager, commandsManager }) => {
const viewportInfo = getEnabledElement(i);
const hasCornerstoneContext =
viewportInfo.context == 'ACTIVE_VIEWPORT::CORNERSTONE';
viewportInfo.context === 'ACTIVE_VIEWPORT::CORNERSTONE';
if (hasCornerstoneContext) {
cornerstoneTools.setToolActiveForElement(
viewportInfo.enabledElement,
viewportInfo.element,
toolName,
{ mouseButtonMask: 1 }
);
@ -333,6 +409,11 @@ const commandsModule = ({ servicesManager, commandsManager }) => {
storeContexts: [],
options: {},
},
toggleSynchronizer: {
commandFn: actions.toggleSynchronizer,
storeContexts: [],
options: {},
},
removeToolState: {
commandFn: actions.removeToolState,
storeContexts: [],

View File

@ -0,0 +1,3 @@
const id = 'org.ohif.cornerstone';
export default id;

View File

@ -38,12 +38,14 @@ export default {
const onNewImageHandler = jumpData => {
commandsManager.runCommand('jumpToImage', jumpData);
};
const { ToolBarService } = servicesManager;
const { ToolBarService } = servicesManager.services;
return (
<OHIFCornerstoneViewport
{...props}
ToolBarService={ToolBarService}
servicesManager={servicesManager}
commandsManager={commandsManager}
onNewImage={onNewImageHandler}
/>
);

View File

@ -465,6 +465,9 @@ const _connectToolsToMeasurementService = (
}
);
// on display sets added, check if there are any measurements in measurement service that need to be
// put into cornerstone tools tooldata
const enabledElement = evt.detail.element;
const completedEvt = csTools.EVENTS.MEASUREMENT_COMPLETED;
const updatedEvt = csTools.EVENTS.MEASUREMENT_MODIFIED;
@ -480,9 +483,13 @@ const _connectToolsToMeasurementService = (
const _connectMeasurementServiceToTools = (
MeasurementService,
measurementSource
measurementSource,
dataSource
) => {
const { MEASUREMENT_REMOVED } = MeasurementService.EVENTS;
const {
MEASUREMENT_REMOVED,
RAW_MEASUREMENT_ADDED,
} = MeasurementService.EVENTS;
const sourceId = measurementSource.id;
// TODO: This is an unsafe delete
@ -495,6 +502,86 @@ const _connectMeasurementServiceToTools = (
//
// Could potentially use "source" from event to determine tool type and skip some
// iterations?
const {
POLYLINE,
ELLIPSE,
POINT,
BIDIRECTIONAL,
} = MeasurementService.VALUE_TYPES;
// TODO -> I get why this was attemped, but its not nearly flexible enough.
// A single measurement may have an ellipse + a bidirectional measurement, for instances.
// You can't define a bidirectional tool as a single type..
// OHIF-230
const TOOL_TYPE_TO_VALUE_TYPE = {
Length: POLYLINE,
EllipticalRoi: ELLIPSE,
Bidirectional: BIDIRECTIONAL,
ArrowAnnotate: POINT,
};
const VALUE_TYPE_TO_TOOL_TYPE = {
[POLYLINE]: 'Length',
[ELLIPSE]: 'EllipticalRoi',
[BIDIRECTIONAL]: 'Bidirectional',
[POINT]: 'ArrowAnnotate',
};
MeasurementService.subscribe(
RAW_MEASUREMENT_ADDED,
({ source, measurement, data, dataSource }) => {
const {
referenceStudyUID: StudyInstanceUID,
referenceSeriesUID: SeriesInstanceUID,
SOPInstanceUID,
} = measurement;
let toolType;
try {
toolType = VALUE_TYPE_TO_TOOL_TYPE[measurement.type];
} catch {
throw Error('Cannot add tool to cornerstone tools');
}
let imageId;
if (data.imageId) {
// handle dicom json launch, since we cannot create image id from
// instance UIDs, each tool should embed the instance metadata
// TODO: handle multi instance case
imageId = data.imageId;
} else {
// handle general case of dicom web
const instance = {
StudyInstanceUID,
SeriesInstanceUID,
SOPInstanceUID,
};
// TODO: handle multi frame
imageId = dataSource.getImageIdsForInstance({ instance });
}
const toolState = cornerstoneTools.globalImageIdSpecificToolStateManager.saveToolState();
if (toolState[imageId] === undefined) {
toolState[imageId] = {};
}
const imageIdToolState = toolState[imageId];
// If we don't have tool state for this type of tool, add an empty object
if (imageIdToolState[toolType] === undefined) {
imageIdToolState[toolType] = {
data: [],
};
}
const toolData = imageIdToolState[toolType];
toolData.data.push(data);
}
);
MeasurementService.subscribe(
MEASUREMENT_REMOVED,
({ source, measurement: removedMeasurementId }) => {

View File

@ -10,7 +10,7 @@ const state = {
* @return void
*/
const setEnabledElement = (viewportIndex, element, context) => {
const targetContext = context || DEFAULT_CONTEXT;
const targetContext = context || state.DEFAULT_CONTEXT;
state.enabledElements[viewportIndex] = {
element,

View 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 };

View File

@ -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) {

View File

@ -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);

View File

@ -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') {

View File

@ -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);
};
/**

View File

@ -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();

View File

@ -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() {

View File

@ -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
);
}

View File

@ -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(

View File

@ -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 };
});
}
}
});
}

View File

@ -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?

View File

@ -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} />

View File

@ -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 {

View File

@ -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,
},
];
}

View 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;

View File

@ -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,

View File

@ -32,7 +32,7 @@
"@ohif/core": "^0.50.0",
"@ohif/ui": "^0.50.0",
"cornerstone-core": "^2.3.0",
"cornerstone-math": "^0.1.8",
"cornerstone-math": "^0.1.9",
"cornerstone-tools": "5.1.2",
"cornerstone-wado-image-loader": "^3.1.2",
"dcmjs": "0.16.1",
@ -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",

View File

@ -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"

View File

@ -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
}),
});

View File

@ -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);
}

View File

@ -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,

View File

@ -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;

View File

@ -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}
/>
);

View File

@ -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());

View File

@ -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

View File

@ -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?

View File

@ -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;

View File

@ -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],
};

View File

@ -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",

View File

@ -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"

View File

@ -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,
};
}

View File

@ -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';

View File

@ -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

View File

@ -215,6 +215,9 @@ describe('ExtensionManager.js', () => {
getDataSourcesModule: () => {
return [{}];
},
getHangingProtocolModule: () => {
return [{}];
},
getContextModule: () => {
return [{}];
}

View File

@ -7,4 +7,5 @@ export default {
VIEWPORT: 'viewportModule',
CONTEXT: 'contextModule',
LAYOUT_TEMPLATE: 'layoutTemplateModule',
HANGING_PROTOCOL: 'hangingProtocolModule',
};

View File

@ -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 };

View File

@ -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();

View 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,
},
},
};

View File

@ -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(

View File

@ -1,4 +1,5 @@
const EVENTS = {
STUDY_ADDED: 'event::dicomMetadataStore:studyAdded',
INSTANCES_ADDED: 'event::dicomMetadataStore:instancesAdded',
SERIES_ADDED: 'event::dicomMetadataStore:seriesAdded',
};

View File

@ -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));
}
}
},
};
}

View File

@ -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);
}

View File

@ -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 };

View File

@ -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 };

View File

@ -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');
// }
// }
// }

View File

@ -1,3 +1,8 @@
import HangingProtocolService from './HangingProtocolService';
export default HangingProtocolService;
export default {
name: 'HangingProtocolService',
create: ({ configuration = {}, commandsManager }) => {
return new HangingProtocolService(commandsManager);
},
};

View File

@ -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 };

View File

@ -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;
}

View File

@ -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 };

View File

@ -0,0 +1,8 @@
// Sorts an array by score
const sortByScore = arr => {
arr.sort((a, b) => {
return b.score - a.score;
});
};
export { sortByScore };

View File

@ -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;

View File

@ -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);
}

View File

@ -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;

View File

@ -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,
};

View File

@ -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;

View File

@ -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;

View File

@ -25,9 +25,6 @@ export {
ViewportGridContext,
ViewportGridProvider,
useViewportGrid,
HangingProtocolContext,
HangingProtocolProvider,
useHangingProtocol,
} from './src/contextProviders';
/** COMPONENTS */

View File

@ -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 = {

View File

@ -143,7 +143,7 @@ const SidePanel = ({
</span>
</Button>
</div>
<childComponent.content/>
<childComponent.content />
</React.Fragment>
) : (
<React.Fragment>{getPanelButtons()}</React.Fragment>

View File

@ -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,
};

View File

@ -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);

View File

@ -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>
);

View File

@ -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,
};
}

View File

@ -41,9 +41,3 @@ export {
ViewportGridProvider,
useViewportGrid,
} from './ViewportGridProvider';
export {
HangingProtocolContext,
HangingProtocolProvider,
useHangingProtocol,
} from './HangingProtocolProvider';

View File

@ -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:

View File

@ -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;"]

View 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
}

View File

@ -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",

View File

@ -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',
};

View File

@ -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',
};

View File

@ -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',
};

View File

@ -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',
};

View 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

View File

@ -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>
);

View File

@ -81,6 +81,7 @@ function appInit(appConfigOrFunc, defaultExtensions) {
// appConfig.modes.push(window.segmentationMode);
}
return {
appConfig,
commandsManager,

View File

@ -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}>

View 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;

View File

@ -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>

View File

@ -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
View File

@ -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"