feat: Add White Labelling, Hanging Protocols, and tons of Extensions / Mode fixes
* modified * new files in existing extensions/libraries/modes * fixed bugs and tests * Clean up * fix: viewport display fixed * Fixed configs for deployment * Removed unnecessary files and functions * Fixed default HP module * Added white labelling * Fixed hash routing * Removed unnecessary routers Co-authored-by: Alireza Sedghi <ar.sedghi@gmail.com>
This commit is contained in:
parent
0db81b30f3
commit
99b8dc4759
2
.gitignore
vendored
2
.gitignore
vendored
@ -14,7 +14,7 @@ coverage/
|
|||||||
# YALC (for Erik)
|
# YALC (for Erik)
|
||||||
.yalc
|
.yalc
|
||||||
yalc.lock
|
yalc.lock
|
||||||
|
*.dcm
|
||||||
# Logging, System files, misc.
|
# Logging, System files, misc.
|
||||||
.idea/
|
.idea/
|
||||||
.npm
|
.npm
|
||||||
|
|||||||
@ -32,7 +32,7 @@
|
|||||||
"@ohif/core": "^0.50.0",
|
"@ohif/core": "^0.50.0",
|
||||||
"@ohif/ui": "^0.50.0",
|
"@ohif/ui": "^0.50.0",
|
||||||
"cornerstone-core": "^2.3.0",
|
"cornerstone-core": "^2.3.0",
|
||||||
"cornerstone-math": "^0.1.8",
|
"cornerstone-math": "^0.1.9",
|
||||||
"cornerstone-tools": "5.1.2",
|
"cornerstone-tools": "5.1.2",
|
||||||
"cornerstone-wado-image-loader": "^3.1.2",
|
"cornerstone-wado-image-loader": "^3.1.2",
|
||||||
"dcmjs": "0.16.1",
|
"dcmjs": "0.16.1",
|
||||||
@ -47,6 +47,6 @@
|
|||||||
"@babel/runtime": "7.7.6",
|
"@babel/runtime": "7.7.6",
|
||||||
"lodash.merge": "^4.6.2",
|
"lodash.merge": "^4.6.2",
|
||||||
"lodash.debounce": "4.0.8",
|
"lodash.debounce": "4.0.8",
|
||||||
"react-cornerstone-viewport": "4.0.2"
|
"react-cornerstone-viewport": "4.0.5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,107 +1,75 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { useEffect, useState, useRef } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import cornerstone from 'cornerstone-core';
|
||||||
|
import cornerstoneTools from 'cornerstone-tools';
|
||||||
import CornerstoneViewport from 'react-cornerstone-viewport';
|
import CornerstoneViewport from 'react-cornerstone-viewport';
|
||||||
import OHIF from '@ohif/core';
|
import OHIF from '@ohif/core';
|
||||||
import csTools from 'cornerstone-tools';
|
import ViewportLoadingIndicator from './ViewportLoadingIndicator';
|
||||||
import PropTypes from 'prop-types';
|
import setCornerstoneMeasurementActive from './_shared/setCornerstoneMeasurementActive';
|
||||||
import debounce from 'lodash.debounce';
|
import ViewportOverlay from './ViewportOverlay';
|
||||||
import getTools from './utils/getTools.js';
|
|
||||||
import setActiveAndPassiveToolsForElement from './utils/setActiveAndPassiveToolsForElement';
|
|
||||||
|
|
||||||
import { setEnabledElement } from './state';
|
import { useCine, useViewportGrid } from '@ohif/ui';
|
||||||
|
|
||||||
|
const scrollToIndex = cornerstoneTools.importInternal('util/scrollToIndex');
|
||||||
|
|
||||||
const { StackManager } = OHIF.utils;
|
const { StackManager } = OHIF.utils;
|
||||||
|
|
||||||
class OHIFCornerstoneViewport extends Component {
|
function OHIFCornerstoneViewport({
|
||||||
state = {
|
children,
|
||||||
viewportData: null,
|
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 = {
|
const isMounted = useRef(false);
|
||||||
customProps: {},
|
const stageChangedRef = useRef(false);
|
||||||
};
|
|
||||||
|
|
||||||
static propTypes = {
|
const onNewImage = (element, callback) => {
|
||||||
displaySet: PropTypes.object,
|
const handler = () => {
|
||||||
viewportIndex: PropTypes.number,
|
element.removeEventListener(cornerstone.EVENTS.IMAGE_RENDERED, handler);
|
||||||
dataSource: PropTypes.object,
|
callback(element, ToolBarService);
|
||||||
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,
|
|
||||||
};
|
};
|
||||||
|
element.addEventListener(cornerstone.EVENTS.IMAGE_RENDERED, handler);
|
||||||
return viewportData;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
setStateFromProps() {
|
useEffect(() => {
|
||||||
const { displaySet } = this.props;
|
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 {
|
const {
|
||||||
StudyInstanceUID,
|
StudyInstanceUID,
|
||||||
displaySetInstanceUID,
|
displaySetInstanceUID,
|
||||||
@ -118,111 +86,267 @@ class OHIFCornerstoneViewport extends Component {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.getViewportData(displaySet).then(viewportData => {
|
_getViewportData(dataSource, displaySet).then(data => {
|
||||||
this.setState({
|
if (isMounted.current) setViewportData(data);
|
||||||
viewportData,
|
});
|
||||||
|
}, [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() {
|
// We have...
|
||||||
this.setStateFromProps();
|
// 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 cine = cines[viewportIndex];
|
||||||
const { displaySet } = this.props;
|
const isPlaying = (cine && cine.isPlaying) || false;
|
||||||
const prevDisplaySet = prevProps.displaySet;
|
const frameRate = (cine && cine.frameRate) || 24;
|
||||||
|
|
||||||
if (
|
return (
|
||||||
displaySet.displaySetInstanceUID !==
|
<div className="relative flex flex-row w-full h-full overflow-hidden">
|
||||||
prevDisplaySet.displaySetInstanceUID ||
|
<CornerstoneViewport
|
||||||
displaySet.SOPInstanceUID !== prevDisplaySet.SOPInstanceUID ||
|
onElementEnabled={onElementEnabled}
|
||||||
displaySet.imageIndex !== prevDisplaySet.imageIndex
|
viewportIndex={viewportIndex}
|
||||||
) {
|
imageIds={imageIds}
|
||||||
this.setStateFromProps();
|
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() {
|
OHIFCornerstoneViewport.propTypes = {
|
||||||
let childrenWithProps = null;
|
displaySet: PropTypes.object,
|
||||||
|
viewportIndex: PropTypes.number,
|
||||||
|
dataSource: PropTypes.object,
|
||||||
|
children: PropTypes.node,
|
||||||
|
customProps: PropTypes.object,
|
||||||
|
ToolBarService: PropTypes.object,
|
||||||
|
};
|
||||||
|
|
||||||
if (!this.state.viewportData) {
|
OHIFCornerstoneViewport.defaultProps = {
|
||||||
return null;
|
customProps: {},
|
||||||
}
|
};
|
||||||
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;
|
|
||||||
|
|
||||||
// TODO: Does it make more sense to use Context?
|
const _viewportLabels = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I'];
|
||||||
if (this.props.children && this.props.children.length) {
|
|
||||||
childrenWithProps = this.props.children.map((child, index) => {
|
function _getCornerstoneStack(displaySet, dataSource) {
|
||||||
return (
|
// Get stack from Stack Manager
|
||||||
child &&
|
const storedStack = StackManager.findOrCreateStack(displaySet, dataSource);
|
||||||
React.cloneElement(child, {
|
|
||||||
viewportIndex: this.props.viewportIndex,
|
// Clone the stack here so we don't mutate it
|
||||||
key: index,
|
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(
|
// Jump to measurement consumed, remove.
|
||||||
({ currentImageIdIndex, sopInstanceUid }) => {
|
MeasurementService.removeJumpToMeasurement(viewportIndex);
|
||||||
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}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
41
extensions/cornerstone/src/ViewportLoadingIndicator.js
Normal file
41
extensions/cornerstone/src/ViewportLoadingIndicator.js
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
const ViewportLoadingIndicator = ({ error }) => {
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="bg-black h-full w-full absolute opacity-50"></div>
|
||||||
|
<div className="text-primary-light text-xl font-thin">
|
||||||
|
<h4>Error Loading Image</h4>
|
||||||
|
<p>An error has occurred.</p>
|
||||||
|
<p>{error.message}</p>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="bg-black h-full w-full absolute opacity-50"></div>
|
||||||
|
<div className="absolute transparent w-full h-full flex items-center justify-center">
|
||||||
|
<p className="text-primary-light text-xl font-thin">
|
||||||
|
Loading...
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
ViewportLoadingIndicator.propTypes = {
|
||||||
|
percentComplete: PropTypes.number,
|
||||||
|
error: PropTypes.object,
|
||||||
|
};
|
||||||
|
|
||||||
|
ViewportLoadingIndicator.defaultProps = {
|
||||||
|
percentComplete: 0,
|
||||||
|
error: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ViewportLoadingIndicator;
|
||||||
83
extensions/cornerstone/src/ViewportOverlay.js
Normal file
83
extensions/cornerstone/src/ViewportOverlay.js
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import cornerstone from 'cornerstone-core';
|
||||||
|
import classnames from 'classnames';
|
||||||
|
|
||||||
|
const ViewportOverlay = ({
|
||||||
|
imageId,
|
||||||
|
scale,
|
||||||
|
windowWidth,
|
||||||
|
windowCenter,
|
||||||
|
imageIndex,
|
||||||
|
stackSize,
|
||||||
|
activeTools,
|
||||||
|
}) => {
|
||||||
|
const topLeft = 'top-viewport left-viewport';
|
||||||
|
const topRight = 'top-viewport right-viewport-scrollbar';
|
||||||
|
const bottomRight = 'bottom-viewport right-viewport-scrollbar';
|
||||||
|
const bottomLeft = 'bottom-viewport left-viewport';
|
||||||
|
const overlay = 'absolute pointer-events-none';
|
||||||
|
|
||||||
|
const isZoomActive = activeTools.includes('Zoom');
|
||||||
|
const isWwwcActive = activeTools.includes('Wwwc');
|
||||||
|
|
||||||
|
if (!imageId) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: this component should be presentational only. Right now it has a weird dependency on Cornerstone
|
||||||
|
const generalImageModule =
|
||||||
|
cornerstone.metaData.get('generalImageModule', imageId) || {};
|
||||||
|
const { instanceNumber } = generalImageModule;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="text-primary-light">
|
||||||
|
<div data-cy={"viewport-overlay-top-left"} className={classnames(overlay, topLeft)}>
|
||||||
|
{isZoomActive && (
|
||||||
|
<div className="flex flex-row">
|
||||||
|
<span className="mr-1">Zoom:</span>
|
||||||
|
<span className="font-thin">{scale.toFixed(2)}x</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{isWwwcActive && (
|
||||||
|
<div className="flex flex-row">
|
||||||
|
<span className="mr-1">W:</span>
|
||||||
|
<span className="ml-1 mr-2 font-thin">
|
||||||
|
{windowWidth.toFixed(0)}
|
||||||
|
</span>
|
||||||
|
<span className="mr-1">L:</span>
|
||||||
|
<span className="ml-1 font-thin">{windowCenter.toFixed(0)}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div data-cy={"viewport-overlay-top-right"} className={classnames(overlay, topRight)}>
|
||||||
|
{stackSize > 1 && (
|
||||||
|
<div className="flex flex-row">
|
||||||
|
<span className="mr-1">I:</span>
|
||||||
|
<span className="font-thin">
|
||||||
|
{`${instanceNumber} (${imageIndex}/${stackSize})`}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div data-cy={"viewport-overlay-bottom-right"} className={classnames(overlay, bottomRight)}></div>
|
||||||
|
<div data-cy={"viewport-overlay-bottom-left"} className={classnames(overlay, bottomLeft)}></div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
ViewportOverlay.propTypes = {
|
||||||
|
scale: PropTypes.number.isRequired,
|
||||||
|
windowWidth: PropTypes.number.isRequired,
|
||||||
|
windowCenter: PropTypes.number.isRequired,
|
||||||
|
imageId: PropTypes.string.isRequired,
|
||||||
|
imageIndex: PropTypes.number.isRequired,
|
||||||
|
stackSize: PropTypes.number.isRequired,
|
||||||
|
activeTools: PropTypes.arrayOf(PropTypes.string),
|
||||||
|
};
|
||||||
|
|
||||||
|
ViewportOverlay.defaultProps = {
|
||||||
|
activeTools: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ViewportOverlay;
|
||||||
36
extensions/cornerstone/src/_shared/getTools.js
Normal file
36
extensions/cornerstone/src/_shared/getTools.js
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import csTools from 'cornerstone-tools';
|
||||||
|
|
||||||
|
const toolsGroupedByType = {
|
||||||
|
touch: [csTools.PanMultiTouchTool, csTools.ZoomTouchPinchTool],
|
||||||
|
annotations: [
|
||||||
|
csTools.ArrowAnnotateTool,
|
||||||
|
csTools.BidirectionalTool,
|
||||||
|
csTools.LengthTool,
|
||||||
|
csTools.AngleTool,
|
||||||
|
csTools.FreehandRoiTool,
|
||||||
|
csTools.EllipticalRoiTool,
|
||||||
|
csTools.DragProbeTool,
|
||||||
|
csTools.RectangleRoiTool,
|
||||||
|
],
|
||||||
|
other: [
|
||||||
|
csTools.PanTool,
|
||||||
|
csTools.ZoomTool,
|
||||||
|
csTools.WwwcTool,
|
||||||
|
csTools.WwwcRegionTool,
|
||||||
|
csTools.MagnifyTool,
|
||||||
|
csTools.StackScrollTool,
|
||||||
|
csTools.StackScrollMouseWheelTool,
|
||||||
|
csTools.OverlayTool,
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function getTools() {
|
||||||
|
const tools = [];
|
||||||
|
Object.keys(toolsGroupedByType).forEach(toolsGroup =>
|
||||||
|
tools.push(...toolsGroupedByType[toolsGroup])
|
||||||
|
);
|
||||||
|
|
||||||
|
return tools;
|
||||||
|
}
|
||||||
|
|
||||||
|
export { toolsGroupedByType };
|
||||||
@ -0,0 +1,21 @@
|
|||||||
|
import csTools from 'cornerstone-tools';
|
||||||
|
|
||||||
|
export default function _setActiveAndPassiveToolsForElement(element, tools) {
|
||||||
|
const BaseAnnotationTool = csTools.importInternal('base/BaseAnnotationTool');
|
||||||
|
|
||||||
|
tools.forEach(tool => {
|
||||||
|
if (tool.prototype instanceof BaseAnnotationTool) {
|
||||||
|
// BaseAnnotationTool would likely come from csTools lib exports
|
||||||
|
const toolName = new tool().name;
|
||||||
|
csTools.setToolPassiveForElement(element, toolName); // there may be a better place to determine name; may not be on uninstantiated class
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
csTools.setToolActiveForElement(element, 'Pan', { mouseButtonMask: 4 });
|
||||||
|
csTools.setToolActiveForElement(element, 'Zoom', { mouseButtonMask: 2 });
|
||||||
|
csTools.setToolActiveForElement(element, 'Wwwc', { mouseButtonMask: 1 });
|
||||||
|
csTools.setToolActiveForElement(element, 'StackScrollMouseWheel', {}); // TODO: Empty options should not be required
|
||||||
|
csTools.setToolActiveForElement(element, 'PanMultiTouch', { pointers: 2 }); // TODO: Better error if no options
|
||||||
|
csTools.setToolActiveForElement(element, 'ZoomTouchPinch', {});
|
||||||
|
csTools.setToolEnabledForElement(element, 'Overlay', {});
|
||||||
|
}
|
||||||
@ -0,0 +1,39 @@
|
|||||||
|
import cornerstone from 'cornerstone-core';
|
||||||
|
import cornerstoneTools from 'cornerstone-tools';
|
||||||
|
|
||||||
|
const { globalImageIdSpecificToolStateManager } = cornerstoneTools;
|
||||||
|
|
||||||
|
export default function setCornerstoneMeasurementActive(measurement) {
|
||||||
|
const { id } = measurement;
|
||||||
|
|
||||||
|
const toolState = globalImageIdSpecificToolStateManager.saveToolState();
|
||||||
|
|
||||||
|
Object.keys(toolState).forEach(imageId => {
|
||||||
|
const imageIdSpecificToolState = toolState[imageId];
|
||||||
|
|
||||||
|
Object.keys(imageIdSpecificToolState).forEach(toolType => {
|
||||||
|
const toolSpecificToolState = imageIdSpecificToolState[toolType];
|
||||||
|
|
||||||
|
const toolSpecificToolData = toolSpecificToolState.data;
|
||||||
|
|
||||||
|
if (toolSpecificToolData && toolSpecificToolData.length) {
|
||||||
|
toolSpecificToolData.forEach(data => {
|
||||||
|
data.active = data.id === id ? true : false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const enabledElements = cornerstoneTools.store.state.enabledElements;
|
||||||
|
|
||||||
|
enabledElements.forEach(element => {
|
||||||
|
try {
|
||||||
|
cornerstone.updateImage(element);
|
||||||
|
} catch (ex) {
|
||||||
|
// https://github.com/cornerstonejs/cornerstone/blob/master/src/updateImage.js#L16
|
||||||
|
// This fails if enabledElement.image is undefined and we have no layers
|
||||||
|
// Instead of throwing, it should _probably_ do nothing.
|
||||||
|
// We'll just swallow the exception
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
@ -9,6 +9,30 @@ const scroll = cornerstoneTools.import('util/scroll');
|
|||||||
|
|
||||||
const { studyMetadataManager } = OHIF.utils;
|
const { 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 commandsModule = ({ servicesManager, commandsManager }) => {
|
||||||
const { ViewportGridService } = servicesManager.services;
|
const { ViewportGridService } = servicesManager.services;
|
||||||
|
|
||||||
@ -70,8 +94,60 @@ const commandsModule = ({ servicesManager, commandsManager }) => {
|
|||||||
cornerstone.reset(enabledElement);
|
cornerstone.reset(enabledElement);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
invertViewport: () => {
|
toggleSynchronizer: ({ toggledState }) => {
|
||||||
const enabledElement = _getActiveViewportsEnabledElement();
|
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) {
|
if (enabledElement) {
|
||||||
let viewport = cornerstone.getViewport(enabledElement);
|
let viewport = cornerstone.getViewport(enabledElement);
|
||||||
@ -111,11 +187,11 @@ const commandsModule = ({ servicesManager, commandsManager }) => {
|
|||||||
|
|
||||||
const viewportInfo = getEnabledElement(i);
|
const viewportInfo = getEnabledElement(i);
|
||||||
const hasCornerstoneContext =
|
const hasCornerstoneContext =
|
||||||
viewportInfo.context == 'ACTIVE_VIEWPORT::CORNERSTONE';
|
viewportInfo.context === 'ACTIVE_VIEWPORT::CORNERSTONE';
|
||||||
|
|
||||||
if (hasCornerstoneContext) {
|
if (hasCornerstoneContext) {
|
||||||
cornerstoneTools.setToolActiveForElement(
|
cornerstoneTools.setToolActiveForElement(
|
||||||
viewportInfo.enabledElement,
|
viewportInfo.element,
|
||||||
toolName,
|
toolName,
|
||||||
{ mouseButtonMask: 1 }
|
{ mouseButtonMask: 1 }
|
||||||
);
|
);
|
||||||
@ -333,6 +409,11 @@ const commandsModule = ({ servicesManager, commandsManager }) => {
|
|||||||
storeContexts: [],
|
storeContexts: [],
|
||||||
options: {},
|
options: {},
|
||||||
},
|
},
|
||||||
|
toggleSynchronizer: {
|
||||||
|
commandFn: actions.toggleSynchronizer,
|
||||||
|
storeContexts: [],
|
||||||
|
options: {},
|
||||||
|
},
|
||||||
removeToolState: {
|
removeToolState: {
|
||||||
commandFn: actions.removeToolState,
|
commandFn: actions.removeToolState,
|
||||||
storeContexts: [],
|
storeContexts: [],
|
||||||
|
|||||||
3
extensions/cornerstone/src/id.js
Normal file
3
extensions/cornerstone/src/id.js
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
const id = 'org.ohif.cornerstone';
|
||||||
|
|
||||||
|
export default id;
|
||||||
@ -38,12 +38,14 @@ export default {
|
|||||||
const onNewImageHandler = jumpData => {
|
const onNewImageHandler = jumpData => {
|
||||||
commandsManager.runCommand('jumpToImage', jumpData);
|
commandsManager.runCommand('jumpToImage', jumpData);
|
||||||
};
|
};
|
||||||
const { ToolBarService } = servicesManager;
|
const { ToolBarService } = servicesManager.services;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<OHIFCornerstoneViewport
|
<OHIFCornerstoneViewport
|
||||||
{...props}
|
{...props}
|
||||||
ToolBarService={ToolBarService}
|
ToolBarService={ToolBarService}
|
||||||
|
servicesManager={servicesManager}
|
||||||
|
commandsManager={commandsManager}
|
||||||
onNewImage={onNewImageHandler}
|
onNewImage={onNewImageHandler}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -465,6 +465,9 @@ const _connectToolsToMeasurementService = (
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// on display sets added, check if there are any measurements in measurement service that need to be
|
||||||
|
// put into cornerstone tools tooldata
|
||||||
|
|
||||||
const enabledElement = evt.detail.element;
|
const enabledElement = evt.detail.element;
|
||||||
const completedEvt = csTools.EVENTS.MEASUREMENT_COMPLETED;
|
const completedEvt = csTools.EVENTS.MEASUREMENT_COMPLETED;
|
||||||
const updatedEvt = csTools.EVENTS.MEASUREMENT_MODIFIED;
|
const updatedEvt = csTools.EVENTS.MEASUREMENT_MODIFIED;
|
||||||
@ -480,9 +483,13 @@ const _connectToolsToMeasurementService = (
|
|||||||
|
|
||||||
const _connectMeasurementServiceToTools = (
|
const _connectMeasurementServiceToTools = (
|
||||||
MeasurementService,
|
MeasurementService,
|
||||||
measurementSource
|
measurementSource,
|
||||||
|
dataSource
|
||||||
) => {
|
) => {
|
||||||
const { MEASUREMENT_REMOVED } = MeasurementService.EVENTS;
|
const {
|
||||||
|
MEASUREMENT_REMOVED,
|
||||||
|
RAW_MEASUREMENT_ADDED,
|
||||||
|
} = MeasurementService.EVENTS;
|
||||||
const sourceId = measurementSource.id;
|
const sourceId = measurementSource.id;
|
||||||
|
|
||||||
// TODO: This is an unsafe delete
|
// 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
|
// Could potentially use "source" from event to determine tool type and skip some
|
||||||
// iterations?
|
// 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(
|
MeasurementService.subscribe(
|
||||||
MEASUREMENT_REMOVED,
|
MEASUREMENT_REMOVED,
|
||||||
({ source, measurement: removedMeasurementId }) => {
|
({ source, measurement: removedMeasurementId }) => {
|
||||||
|
|||||||
@ -10,7 +10,7 @@ const state = {
|
|||||||
* @return void
|
* @return void
|
||||||
*/
|
*/
|
||||||
const setEnabledElement = (viewportIndex, element, context) => {
|
const setEnabledElement = (viewportIndex, element, context) => {
|
||||||
const targetContext = context || DEFAULT_CONTEXT;
|
const targetContext = context || state.DEFAULT_CONTEXT;
|
||||||
|
|
||||||
state.enabledElements[viewportIndex] = {
|
state.enabledElements[viewportIndex] = {
|
||||||
element,
|
element,
|
||||||
|
|||||||
245
extensions/default/src/DicomJSONDataSource/index.js
Normal file
245
extensions/default/src/DicomJSONDataSource/index.js
Normal file
@ -0,0 +1,245 @@
|
|||||||
|
import { DicomMetadataStore, IWebApiDataSource } from '@ohif/core';
|
||||||
|
import OHIF from '@ohif/core';
|
||||||
|
|
||||||
|
import getImageId from '../DicomWebDataSource/utils/getImageId';
|
||||||
|
|
||||||
|
const metadataProvider = OHIF.classes.metadataProvider;
|
||||||
|
|
||||||
|
const mappings = {
|
||||||
|
studyInstanceUid: 'StudyInstanceUID',
|
||||||
|
patientId: 'PatientID',
|
||||||
|
};
|
||||||
|
|
||||||
|
let _store = {
|
||||||
|
urls: [],
|
||||||
|
// {
|
||||||
|
// url: url1
|
||||||
|
// studies: [Study1, Study2], // if multiple studies
|
||||||
|
// }
|
||||||
|
// {
|
||||||
|
// url: url2
|
||||||
|
// studies: [Study1],
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
};
|
||||||
|
|
||||||
|
const getMetaDataByURL = url => {
|
||||||
|
return _store.urls.find(metaData => metaData.url === url);
|
||||||
|
};
|
||||||
|
|
||||||
|
const findStudies = (key, value) => {
|
||||||
|
let studies = [];
|
||||||
|
_store.urls.map(metaData => {
|
||||||
|
metaData.studies.map(aStudy => {
|
||||||
|
if (aStudy[key] === value) {
|
||||||
|
studies.push(aStudy);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return studies;
|
||||||
|
};
|
||||||
|
|
||||||
|
function createDicomJSONApi(dicomJsonConfig) {
|
||||||
|
const { name } = dicomJsonConfig;
|
||||||
|
|
||||||
|
const implementation = {
|
||||||
|
parseRouteParams: async ({ params, query, url }) => {
|
||||||
|
if (!url) url = query.get('url');
|
||||||
|
let metaData = getMetaDataByURL(url);
|
||||||
|
|
||||||
|
// if we have already cached the data from this specific url
|
||||||
|
// We are only handling one StudyInstanceUID to run; however,
|
||||||
|
// all studies for patientID will be put in the correct tab
|
||||||
|
if (metaData) {
|
||||||
|
return metaData.studies.map(aStudy => {
|
||||||
|
return aStudy.StudyInstanceUID;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(url);
|
||||||
|
let data = await response.json();
|
||||||
|
|
||||||
|
const studyInstanceUIDs = data.studies.map(
|
||||||
|
study => study.StudyInstanceUID
|
||||||
|
);
|
||||||
|
|
||||||
|
let StudyInstanceUID;
|
||||||
|
let SeriesInstanceUID;
|
||||||
|
data.studies.forEach(study => {
|
||||||
|
StudyInstanceUID = study.StudyInstanceUID;
|
||||||
|
|
||||||
|
study.series.forEach(series => {
|
||||||
|
SeriesInstanceUID = series.SeriesInstanceUID;
|
||||||
|
|
||||||
|
series.instances.forEach(instance => {
|
||||||
|
const { url: imageId, metadata: naturalizedDicom } = instance;
|
||||||
|
|
||||||
|
// Add imageId specific mapping to this data as the URL isn't necessarliy WADO-URI.
|
||||||
|
metadataProvider.addImageIdToUIDs(imageId, {
|
||||||
|
StudyInstanceUID,
|
||||||
|
SeriesInstanceUID,
|
||||||
|
SOPInstanceUID: naturalizedDicom.SOPInstanceUID,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
_store.urls.push({
|
||||||
|
url,
|
||||||
|
studies: [...data.studies],
|
||||||
|
});
|
||||||
|
|
||||||
|
return studyInstanceUIDs;
|
||||||
|
},
|
||||||
|
query: {
|
||||||
|
studies: {
|
||||||
|
mapParams: () => {},
|
||||||
|
search: async param => {
|
||||||
|
const [key, value] = Object.entries(param)[0];
|
||||||
|
const mappedParam = mappings[key];
|
||||||
|
|
||||||
|
const studies = findStudies(mappedParam, value);
|
||||||
|
|
||||||
|
return studies.map(aStudy => {
|
||||||
|
return {
|
||||||
|
accession: aStudy.AccessionNumber,
|
||||||
|
date: aStudy.StudyDate,
|
||||||
|
description: aStudy.StudyDescription,
|
||||||
|
instances: aStudy.NumInstances,
|
||||||
|
modalities: aStudy.Modalities,
|
||||||
|
mrn: aStudy.PatientID,
|
||||||
|
patientName: aStudy.PatientName,
|
||||||
|
studyInstanceUid: aStudy.StudyInstanceUID,
|
||||||
|
NumInstances: aStudy.NumInstances,
|
||||||
|
time: aStudy.StudyTime,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
processResults: () => {
|
||||||
|
console.debug(' DICOMJson QUERY processResults');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
series: {
|
||||||
|
// mapParams: mapParams.bind(),
|
||||||
|
search: () => {
|
||||||
|
console.debug(' DICOMJson QUERY SERIES SEARCH');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
instances: {
|
||||||
|
search: () => {
|
||||||
|
console.debug(' DICOMJson QUERY instances SEARCH');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
retrieve: {
|
||||||
|
series: {
|
||||||
|
metaData: () => {
|
||||||
|
console.debug(' DICOMJson retrieve series metadata');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
store: {
|
||||||
|
dicom: () => {
|
||||||
|
console.debug(' DICOMJson store dicom');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
retrieveSeriesMetadata: ({
|
||||||
|
StudyInstanceUID,
|
||||||
|
madeInClient = false,
|
||||||
|
customSort,
|
||||||
|
} = {}) => {
|
||||||
|
if (!StudyInstanceUID) {
|
||||||
|
throw new Error(
|
||||||
|
'Unable to query for SeriesMetadata without StudyInstanceUID'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const study = findStudies('StudyInstanceUID', StudyInstanceUID)[0];
|
||||||
|
let series;
|
||||||
|
|
||||||
|
if (customSort) {
|
||||||
|
series = customSort(study.series);
|
||||||
|
} else {
|
||||||
|
series = study.series;
|
||||||
|
}
|
||||||
|
|
||||||
|
const seriesSummaryMetadata = series.map(series => {
|
||||||
|
const seriesSummary = {
|
||||||
|
StudyInstanceUID: study.StudyInstanceUID,
|
||||||
|
...series,
|
||||||
|
};
|
||||||
|
delete seriesSummary.instances;
|
||||||
|
return seriesSummary;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Async load series, store as retrieved
|
||||||
|
function storeInstances(naturalizedInstances) {
|
||||||
|
DicomMetadataStore.addInstances(naturalizedInstances, madeInClient);
|
||||||
|
}
|
||||||
|
|
||||||
|
DicomMetadataStore.addSeriesMetadata(seriesSummaryMetadata, madeInClient);
|
||||||
|
|
||||||
|
function setSuccessFlag() {
|
||||||
|
const study = DicomMetadataStore.getStudy(
|
||||||
|
StudyInstanceUID,
|
||||||
|
madeInClient
|
||||||
|
);
|
||||||
|
study.isLoaded = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const numberOfSeries = series.length;
|
||||||
|
series.forEach((series, index) => {
|
||||||
|
const instances = series.instances.map(instance => {
|
||||||
|
const obj = {
|
||||||
|
...instance.metadata,
|
||||||
|
url: instance.url,
|
||||||
|
imageId: instance.url,
|
||||||
|
...series,
|
||||||
|
};
|
||||||
|
delete obj.instances;
|
||||||
|
return obj;
|
||||||
|
});
|
||||||
|
storeInstances(instances);
|
||||||
|
if (index === numberOfSeries - 1) setSuccessFlag();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
getImageIdsForDisplaySet(displaySet) {
|
||||||
|
const images = displaySet.images;
|
||||||
|
const imageIds = [];
|
||||||
|
|
||||||
|
if (!images) {
|
||||||
|
return imageIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
displaySet.images.forEach(instance => {
|
||||||
|
const NumberOfFrames = instance.NumberOfFrames;
|
||||||
|
|
||||||
|
if (NumberOfFrames > 1) {
|
||||||
|
for (let i = 0; i < NumberOfFrames; i++) {
|
||||||
|
const imageId = getImageId({
|
||||||
|
instance,
|
||||||
|
frame: i,
|
||||||
|
config: dicomJsonConfig,
|
||||||
|
});
|
||||||
|
imageIds.push(imageId);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const imageId = getImageId({ instance, config: dicomJsonConfig });
|
||||||
|
imageIds.push(imageId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return imageIds;
|
||||||
|
},
|
||||||
|
getImageIdsForInstance({ instance, frame }) {
|
||||||
|
const imageIds = getImageId({
|
||||||
|
instance,
|
||||||
|
frame,
|
||||||
|
});
|
||||||
|
return imageIds;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return IWebApiDataSource.create(implementation);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { createDicomJSONApi };
|
||||||
@ -63,10 +63,22 @@ function createDicomWebApi(dicomWebConfig) {
|
|||||||
const wadoDicomWebClient = new api.DICOMwebClient(wadoConfig);
|
const wadoDicomWebClient = new api.DICOMwebClient(wadoConfig);
|
||||||
|
|
||||||
const implementation = {
|
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: {
|
query: {
|
||||||
studies: {
|
studies: {
|
||||||
mapParams: mapParams.bind(),
|
mapParams: mapParams.bind(),
|
||||||
search: async function (origParams) {
|
search: async function(origParams) {
|
||||||
const { studyInstanceUid, seriesInstanceUid, ...mappedParams } =
|
const { studyInstanceUid, seriesInstanceUid, ...mappedParams } =
|
||||||
mapParams(origParams, {
|
mapParams(origParams, {
|
||||||
supportsFuzzyMatching,
|
supportsFuzzyMatching,
|
||||||
@ -86,7 +98,7 @@ function createDicomWebApi(dicomWebConfig) {
|
|||||||
},
|
},
|
||||||
series: {
|
series: {
|
||||||
// mapParams: mapParams.bind(),
|
// mapParams: mapParams.bind(),
|
||||||
search: async function (studyInstanceUid) {
|
search: async function(studyInstanceUid) {
|
||||||
const results = await seriesInStudy(
|
const results = await seriesInStudy(
|
||||||
qidoDicomWebClient,
|
qidoDicomWebClient,
|
||||||
studyInstanceUid
|
studyInstanceUid
|
||||||
@ -129,7 +141,6 @@ function createDicomWebApi(dicomWebConfig) {
|
|||||||
DicomMetadataStore.addInstances(naturalizedInstances);
|
DicomMetadataStore.addInstances(naturalizedInstances);
|
||||||
callback(naturalizedInstances);
|
callback(naturalizedInstances);
|
||||||
};
|
};
|
||||||
|
|
||||||
const studyPromises = StudyInstanceUIDs.map(StudyInstanceUID =>
|
const studyPromises = StudyInstanceUIDs.map(StudyInstanceUID =>
|
||||||
retrieveStudyMetadata(
|
retrieveStudyMetadata(
|
||||||
wadoDicomWebClient,
|
wadoDicomWebClient,
|
||||||
@ -177,7 +188,14 @@ function createDicomWebApi(dicomWebConfig) {
|
|||||||
await wadoDicomWebClient.storeInstances(options);
|
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) {
|
if (!StudyInstanceUID) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
'Unable to query for SeriesMetadata without StudyInstanceUID'
|
'Unable to query for SeriesMetadata without StudyInstanceUID'
|
||||||
@ -191,21 +209,34 @@ function createDicomWebApi(dicomWebConfig) {
|
|||||||
} = await retrieveStudyMetadata(
|
} = await retrieveStudyMetadata(
|
||||||
wadoDicomWebClient,
|
wadoDicomWebClient,
|
||||||
StudyInstanceUID,
|
StudyInstanceUID,
|
||||||
enableStudyLazyLoad
|
enableStudyLazyLoad,
|
||||||
|
filters,
|
||||||
|
sortCriteria,
|
||||||
|
sortFunction
|
||||||
);
|
);
|
||||||
|
|
||||||
// Async load series, store as retrieved
|
// Async load series, store as retrieved
|
||||||
function storeInstances(instances) {
|
function storeInstances(instances) {
|
||||||
const naturalizedInstances = instances.map(naturalizeDataset);
|
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;
|
const instances = await seriesPromise;
|
||||||
storeInstances(instances);
|
storeInstances(instances);
|
||||||
|
if (index === numberOfSeries - 1) setSuccessFlag();
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
deleteStudyMetadataPromise,
|
deleteStudyMetadataPromise,
|
||||||
@ -222,21 +253,28 @@ function createDicomWebApi(dicomWebConfig) {
|
|||||||
|
|
||||||
if (NumberOfFrames > 1) {
|
if (NumberOfFrames > 1) {
|
||||||
for (let i = 0; i < NumberOfFrames; i++) {
|
for (let i = 0; i < NumberOfFrames; i++) {
|
||||||
const imageId = getImageId({
|
const imageId = this.getImageIdsForInstance({
|
||||||
instance,
|
instance,
|
||||||
frame: i,
|
frame: i,
|
||||||
config: dicomWebConfig,
|
|
||||||
});
|
});
|
||||||
imageIds.push(imageId);
|
imageIds.push(imageId);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const imageId = getImageId({ instance, config: dicomWebConfig });
|
const imageId = this.getImageIdsForInstance({ instance });
|
||||||
imageIds.push(imageId);
|
imageIds.push(imageId);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return imageIds;
|
return imageIds;
|
||||||
},
|
},
|
||||||
|
getImageIdsForInstance({ instance, frame }) {
|
||||||
|
const imageIds = getImageId({
|
||||||
|
instance,
|
||||||
|
frame,
|
||||||
|
config: dicomWebConfig,
|
||||||
|
});
|
||||||
|
return imageIds;
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
if (supportsReject) {
|
if (supportsReject) {
|
||||||
|
|||||||
@ -19,7 +19,9 @@ export function retrieveStudyMetadata(
|
|||||||
dicomWebClient,
|
dicomWebClient,
|
||||||
StudyInstanceUID,
|
StudyInstanceUID,
|
||||||
enableStudyLazyLoad,
|
enableStudyLazyLoad,
|
||||||
filters
|
filters,
|
||||||
|
sortCriteria,
|
||||||
|
sortFunction
|
||||||
) {
|
) {
|
||||||
// @TODO: Whenever a study metadata request has failed, its related promise will be rejected once and for all
|
// @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
|
// 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,
|
dicomWebClient,
|
||||||
StudyInstanceUID,
|
StudyInstanceUID,
|
||||||
enableStudyLazyLoad,
|
enableStudyLazyLoad,
|
||||||
filters
|
filters,
|
||||||
|
sortCriteria,
|
||||||
|
sortFunction
|
||||||
).then(function(data) {
|
).then(function(data) {
|
||||||
resolve(data);
|
resolve(data);
|
||||||
}, reject);
|
}, reject);
|
||||||
|
|||||||
@ -34,6 +34,10 @@ export default function getImageId({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (instance.url) {
|
||||||
|
return instance.url;
|
||||||
|
}
|
||||||
|
|
||||||
const renderingAttr = thumbnail ? 'thumbnailRendering' : 'imageRendering';
|
const renderingAttr = thumbnail ? 'thumbnailRendering' : 'imageRendering';
|
||||||
|
|
||||||
if (!config[renderingAttr] || config[renderingAttr] === 'wadouri') {
|
if (!config[renderingAttr] || config[renderingAttr] === 'wadouri') {
|
||||||
|
|||||||
@ -43,9 +43,11 @@ const sortingCriteria = {
|
|||||||
*/
|
*/
|
||||||
const sortStudySeries = (
|
const sortStudySeries = (
|
||||||
series,
|
series,
|
||||||
seriesSortingCriteria = seriesSortCriteria.default
|
seriesSortingCriteria = seriesSortCriteria.default,
|
||||||
|
sortFunction
|
||||||
) => {
|
) => {
|
||||||
return series.sort(seriesSortingCriteria);
|
if (typeof sortFunction === 'function') return sortFunction(series);
|
||||||
|
else return series.sort(seriesSortingCriteria);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -15,7 +15,9 @@ async function RetrieveMetadata(
|
|||||||
dicomWebClient,
|
dicomWebClient,
|
||||||
studyInstanceUid,
|
studyInstanceUid,
|
||||||
enableStudyLazyLoad,
|
enableStudyLazyLoad,
|
||||||
filters = {}
|
filters = {},
|
||||||
|
sortCriteria,
|
||||||
|
sortFunction
|
||||||
) {
|
) {
|
||||||
// const RetrieveMetadataLoader =
|
// const RetrieveMetadataLoader =
|
||||||
// enableStudyLazyLoad !== false
|
// enableStudyLazyLoad !== false
|
||||||
@ -27,7 +29,9 @@ async function RetrieveMetadata(
|
|||||||
const retrieveMetadataLoader = new RetrieveMetadataLoader(
|
const retrieveMetadataLoader = new RetrieveMetadataLoader(
|
||||||
dicomWebClient,
|
dicomWebClient,
|
||||||
studyInstanceUid,
|
studyInstanceUid,
|
||||||
filters
|
filters,
|
||||||
|
sortCriteria,
|
||||||
|
sortFunction
|
||||||
);
|
);
|
||||||
const { preLoadData, promises } = await retrieveMetadataLoader.execLoad();
|
const { preLoadData, promises } = await retrieveMetadataLoader.execLoad();
|
||||||
|
|
||||||
|
|||||||
@ -12,11 +12,20 @@ export default class RetrieveMetadataLoader {
|
|||||||
* @param {Array} studyInstanceUID Study instance ui to be retrieved
|
* @param {Array} studyInstanceUID Study instance ui to be retrieved
|
||||||
* @param {Object} [filters] - Object containing filters to be applied on retrieve metadata process
|
* @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 {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.client = client;
|
||||||
this.studyInstanceUID = studyInstanceUID;
|
this.studyInstanceUID = studyInstanceUID;
|
||||||
this.filters = filters;
|
this.filters = filters;
|
||||||
|
this.sortCriteria = sortCriteria;
|
||||||
|
this.sortFunction = sortFunction;
|
||||||
}
|
}
|
||||||
|
|
||||||
async execLoad() {
|
async execLoad() {
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import dcmjs from 'dcmjs';
|
|||||||
import { sortStudySeries, sortingCriteria } from '../utils/sortStudy';
|
import { sortStudySeries, sortingCriteria } from '../utils/sortStudy';
|
||||||
import RetrieveMetadataLoader from './retrieveMetadataLoader';
|
import RetrieveMetadataLoader from './retrieveMetadataLoader';
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates an immutable series loader object which loads each series sequentially using the iterator interface
|
* 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
|
* @param {DICOMWebClient} dicomWebClient The DICOMWebClient instance to be used for series load
|
||||||
@ -62,13 +63,17 @@ export default class RetrieveMetadataLoaderAsync extends RetrieveMetadataLoader
|
|||||||
async preLoad() {
|
async preLoad() {
|
||||||
const preLoaders = this.getPreLoaders();
|
const preLoaders = this.getPreLoaders();
|
||||||
const result = await this.runLoaders(preLoaders);
|
const result = await this.runLoaders(preLoaders);
|
||||||
|
const sortCriteria = this.sortCriteria;
|
||||||
|
const sortFunction = this.sortFunction;
|
||||||
|
|
||||||
const { naturalizeDataset } = dcmjs.data.DicomMetaDictionary;
|
const { naturalizeDataset } = dcmjs.data.DicomMetaDictionary;
|
||||||
const naturalized = result.map(naturalizeDataset);
|
const naturalized = result.map(naturalizeDataset);
|
||||||
|
|
||||||
return sortStudySeries(
|
return sortStudySeries(
|
||||||
naturalized,
|
naturalized,
|
||||||
sortingCriteria.seriesSortCriteria.seriesInfoSortingCriteria
|
sortCriteria ||
|
||||||
|
sortingCriteria.seriesSortCriteria.seriesInfoSortingCriteria,
|
||||||
|
sortFunction
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -21,12 +21,13 @@ export default function PanelMeasurementTable({
|
|||||||
|
|
||||||
// ~~ Subscription
|
// ~~ Subscription
|
||||||
const added = MeasurementService.EVENTS.MEASUREMENT_ADDED;
|
const added = MeasurementService.EVENTS.MEASUREMENT_ADDED;
|
||||||
|
const addedRaw = MeasurementService.EVENTS.RAW_MEASUREMENT_ADDED;
|
||||||
const updated = MeasurementService.EVENTS.MEASUREMENT_UPDATED;
|
const updated = MeasurementService.EVENTS.MEASUREMENT_UPDATED;
|
||||||
const removed = MeasurementService.EVENTS.MEASUREMENT_REMOVED;
|
const removed = MeasurementService.EVENTS.MEASUREMENT_REMOVED;
|
||||||
const cleared = MeasurementService.EVENTS.MEASUREMENTS_CLEARED;
|
const cleared = MeasurementService.EVENTS.MEASUREMENTS_CLEARED;
|
||||||
const subscriptions = [];
|
const subscriptions = [];
|
||||||
|
|
||||||
[added, updated, removed, cleared].forEach(evt => {
|
[added, addedRaw, updated, removed, cleared].forEach(evt => {
|
||||||
subscriptions.push(
|
subscriptions.push(
|
||||||
MeasurementService.subscribe(evt, () => {
|
MeasurementService.subscribe(evt, () => {
|
||||||
debouncedSetDisplayMeasurements(
|
debouncedSetDisplayMeasurements(
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { StudyBrowser, useImageViewer } from '@ohif/ui';
|
import { StudyBrowser, useImageViewer } from '@ohif/ui';
|
||||||
import { utils } from '@ohif/core';
|
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
|
// 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.
|
// doesn't have to have such an intense shape. This works well enough for now.
|
||||||
// Tabs --> Studies --> DisplaySets --> Thumbnails
|
// Tabs --> Studies --> DisplaySets --> Thumbnails
|
||||||
const [{ StudyInstanceUIDs }, dispatch] = useImageViewer();
|
const { StudyInstanceUIDs } = useImageViewer();
|
||||||
const [activeTabName, setActiveTabName] = useState('primary');
|
const [activeTabName, setActiveTabName] = useState('primary');
|
||||||
const [expandedStudyInstanceUIDs, setExpandedStudyInstanceUIDs] = useState([
|
const [expandedStudyInstanceUIDs, setExpandedStudyInstanceUIDs] = useState([
|
||||||
...StudyInstanceUIDs,
|
...StudyInstanceUIDs,
|
||||||
@ -27,6 +27,7 @@ function PanelStudyBrowser({
|
|||||||
const [studyDisplayList, setStudyDisplayList] = useState([]);
|
const [studyDisplayList, setStudyDisplayList] = useState([]);
|
||||||
const [displaySets, setDisplaySets] = useState([]);
|
const [displaySets, setDisplaySets] = useState([]);
|
||||||
const [thumbnailImageSrcMap, setThumbnailImageSrcMap] = useState({});
|
const [thumbnailImageSrcMap, setThumbnailImageSrcMap] = useState({});
|
||||||
|
const isMounted = useRef(true);
|
||||||
|
|
||||||
// ~~ studyDisplayList
|
// ~~ studyDisplayList
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -47,8 +48,9 @@ function PanelStudyBrowser({
|
|||||||
// displaySets: []
|
// displaySets: []
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
if (isMounted.current) {
|
||||||
setStudyDisplayList(actuallyMappedStudies);
|
setStudyDisplayList(actuallyMappedStudies);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
StudyInstanceUIDs.forEach(sid => fetchStudiesForPatient(sid));
|
StudyInstanceUIDs.forEach(sid => fetchStudiesForPatient(sid));
|
||||||
@ -72,11 +74,16 @@ function PanelStudyBrowser({
|
|||||||
newImageSrcEntry[dSet.displaySetInstanceUID] = await getImageSrc(
|
newImageSrcEntry[dSet.displaySetInstanceUID] = await getImageSrc(
|
||||||
imageId
|
imageId
|
||||||
);
|
);
|
||||||
setThumbnailImageSrcMap(prevState => {
|
if (isMounted.current) {
|
||||||
return { ...prevState, ...newImageSrcEntry };
|
setThumbnailImageSrcMap(prevState => {
|
||||||
});
|
return { ...prevState, ...newImageSrcEntry };
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
return () => {
|
||||||
|
isMounted.current = false;
|
||||||
|
};
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@ -112,11 +119,14 @@ function PanelStudyBrowser({
|
|||||||
if (imageId) {
|
if (imageId) {
|
||||||
// When the image arrives, render it and store the result in the thumbnailImgSrcMap
|
// When the image arrives, render it and store the result in the thumbnailImgSrcMap
|
||||||
newImageSrcEntry[dSet.displaySetInstanceUID] = await getImageSrc(
|
newImageSrcEntry[dSet.displaySetInstanceUID] = await getImageSrc(
|
||||||
imageId
|
imageId,
|
||||||
|
dSet.initialViewport
|
||||||
);
|
);
|
||||||
setThumbnailImageSrcMap(prevState => {
|
if (isMounted.current) {
|
||||||
return { ...prevState, ...newImageSrcEntry };
|
setThumbnailImageSrcMap(prevState => {
|
||||||
});
|
return { ...prevState, ...newImageSrcEntry };
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,6 +2,7 @@ async function getStudiesForPatientByStudyInstanceUID(
|
|||||||
dataSource,
|
dataSource,
|
||||||
StudyInstanceUID
|
StudyInstanceUID
|
||||||
) {
|
) {
|
||||||
|
if (StudyInstanceUID === undefined) return;
|
||||||
// TODO: The `DicomMetadataStore` should short-circuit both of these requests
|
// TODO: The `DicomMetadataStore` should short-circuit both of these requests
|
||||||
// Data _could_ be here from route query, or if using JSON data source
|
// 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?
|
// We could also force this to "await" these values being available in the DICOMStore?
|
||||||
|
|||||||
@ -10,6 +10,8 @@ import {
|
|||||||
useModal,
|
useModal,
|
||||||
} from '@ohif/ui';
|
} from '@ohif/ui';
|
||||||
|
|
||||||
|
import { useAppConfig } from '@state';
|
||||||
|
|
||||||
function Toolbar({ servicesManager }) {
|
function Toolbar({ servicesManager }) {
|
||||||
const { ToolBarService } = servicesManager.services;
|
const { ToolBarService } = servicesManager.services;
|
||||||
const [toolbarButtons, setToolbarButtons] = useState([]);
|
const [toolbarButtons, setToolbarButtons] = useState([]);
|
||||||
@ -45,7 +47,10 @@ function Toolbar({ servicesManager }) {
|
|||||||
// isActive if:
|
// isActive if:
|
||||||
// - id is primary?
|
// - id is primary?
|
||||||
// - id is in list of "toggled on"?
|
// - 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 filter list for splitButton, and set primary based on most recently clicked
|
||||||
// Also need to kill the radioGroup button's magic logic
|
// Also need to kill the radioGroup button's magic logic
|
||||||
// Everything should be reactive off these props, so commands can inform ToolbarService
|
// Everything should be reactive off these props, so commands can inform ToolbarService
|
||||||
@ -58,6 +63,7 @@ function Toolbar({ servicesManager }) {
|
|||||||
id={id}
|
id={id}
|
||||||
{...componentProps}
|
{...componentProps}
|
||||||
bState={buttonState}
|
bState={buttonState}
|
||||||
|
isActive={isActive}
|
||||||
onInteraction={args => ToolBarService.recordInteraction(args)}
|
onInteraction={args => ToolBarService.recordInteraction(args)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@ -78,6 +84,8 @@ function ViewerLayout({
|
|||||||
viewports,
|
viewports,
|
||||||
ViewportGridComp,
|
ViewportGridComp,
|
||||||
}) {
|
}) {
|
||||||
|
const [appConfig] = useAppConfig();
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { show, hide } = useModal();
|
const { show, hide } = useModal();
|
||||||
|
|
||||||
@ -154,7 +162,7 @@ function ViewerLayout({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Header menuOptions={menuOptions}>
|
<Header menuOptions={menuOptions} WhiteLabeling={appConfig.whiteLabeling} >
|
||||||
<ErrorBoundary context="Primary Toolbar">
|
<ErrorBoundary context="Primary Toolbar">
|
||||||
<div className="relative flex justify-center">
|
<div className="relative flex justify-center">
|
||||||
<Toolbar servicesManager={servicesManager} />
|
<Toolbar servicesManager={servicesManager} />
|
||||||
|
|||||||
@ -1,5 +1,11 @@
|
|||||||
const commandsModule = ({ servicesManager, commandsManager }) => {
|
const commandsModule = ({ servicesManager, commandsManager }) => {
|
||||||
const { MeasurementService, ViewportGridService, ToolBarService, CineService } = servicesManager.services;
|
const {
|
||||||
|
MeasurementService,
|
||||||
|
ViewportGridService,
|
||||||
|
ToolBarService,
|
||||||
|
HangingProtocolService,
|
||||||
|
CineService,
|
||||||
|
} = servicesManager.services;
|
||||||
|
|
||||||
const actions = {
|
const actions = {
|
||||||
clearMeasurements: () => {
|
clearMeasurements: () => {
|
||||||
@ -10,7 +16,16 @@ const commandsModule = ({ servicesManager, commandsManager }) => {
|
|||||||
const { isCineEnabled } = CineService.getState();
|
const { isCineEnabled } = CineService.getState();
|
||||||
CineService.setIsCineEnabled(!isCineEnabled);
|
CineService.setIsCineEnabled(!isCineEnabled);
|
||||||
ToolBarService.setButton('Cine', { props: { isActive: !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: [],
|
storeContexts: [],
|
||||||
options: {},
|
options: {},
|
||||||
},
|
},
|
||||||
|
nextStage: {
|
||||||
|
commandFn: actions.nextStage,
|
||||||
|
storeContexts: [],
|
||||||
|
options: {},
|
||||||
|
},
|
||||||
|
previousStage: {
|
||||||
|
commandFn: actions.previousStage,
|
||||||
|
storeContexts: [],
|
||||||
|
options: {},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@ -3,6 +3,7 @@
|
|||||||
// TODO: Use existing DICOMWeb configuration (previously, appConfig, to configure instance)
|
// TODO: Use existing DICOMWeb configuration (previously, appConfig, to configure instance)
|
||||||
|
|
||||||
import { createDicomWebApi } from './DicomWebDataSource/index.js';
|
import { createDicomWebApi } from './DicomWebDataSource/index.js';
|
||||||
|
import { createDicomJSONApi } from './DicomJSONDataSource/index.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
@ -14,6 +15,11 @@ function getDataSourcesModule() {
|
|||||||
type: 'webApi',
|
type: 'webApi',
|
||||||
createDataSource: createDicomWebApi,
|
createDataSource: createDicomWebApi,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'dicomjson',
|
||||||
|
type: 'jsonApi',
|
||||||
|
createDataSource: createDicomJSONApi,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
188
extensions/default/src/getHangingProtocolModule.js
Normal file
188
extensions/default/src/getHangingProtocolModule.js
Normal file
@ -0,0 +1,188 @@
|
|||||||
|
const hangingProtocolName = 'petCT';
|
||||||
|
|
||||||
|
const petCTProtocol = {
|
||||||
|
id: 'PET/CT',
|
||||||
|
locked: true,
|
||||||
|
hasUpdatedPriorsInformation: false,
|
||||||
|
name: 'PET/CT',
|
||||||
|
createdDate: '2021-02-23T18:32:42.849Z',
|
||||||
|
modifiedDate: '2021-02-23T18:32:42.849Z',
|
||||||
|
availableTo: {},
|
||||||
|
editableBy: {},
|
||||||
|
protocolMatchingRules: [
|
||||||
|
{
|
||||||
|
id: 'wauZK2QNEfDPwcAQo',
|
||||||
|
weight: 1,
|
||||||
|
attribute: 'StudyInstanceUID',
|
||||||
|
constraint: {
|
||||||
|
equals: {
|
||||||
|
value: '1.3.6.1.4.1.25403.345050719074.3824.20170125112931.11',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
stages: [
|
||||||
|
{
|
||||||
|
id: 'hYbmMy3b7pz7GLiaT',
|
||||||
|
name: 'oneByTwo',
|
||||||
|
viewportStructure: {
|
||||||
|
type: 'grid',
|
||||||
|
properties: {
|
||||||
|
rows: 2,
|
||||||
|
columns: 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
viewports: [
|
||||||
|
{
|
||||||
|
viewportSettings: [
|
||||||
|
{
|
||||||
|
options: {
|
||||||
|
voi: {
|
||||||
|
windowWidth: 500,
|
||||||
|
windowCenter: 500,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
commandName: '',
|
||||||
|
// Type can be viewport or prop
|
||||||
|
// viewport: It is most suited for settings that
|
||||||
|
// should be applied before the first render
|
||||||
|
// prop: It is the type of command that can be applied
|
||||||
|
// after the image render, such as tool activations.
|
||||||
|
type: 'viewport',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
imageMatchingRules: [],
|
||||||
|
seriesMatchingRules: [
|
||||||
|
{
|
||||||
|
id: 'vSjk7NCYjtdS3XZAw',
|
||||||
|
weight: 1,
|
||||||
|
attribute: 'SeriesNumber',
|
||||||
|
constraint: {
|
||||||
|
equals: {
|
||||||
|
value: "4",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
studyMatchingRules: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
viewportSettings: [
|
||||||
|
{
|
||||||
|
options: {
|
||||||
|
invert: true,
|
||||||
|
},
|
||||||
|
commandName: '',
|
||||||
|
type: 'viewport',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
imageMatchingRules: [
|
||||||
|
|
||||||
|
],
|
||||||
|
seriesMatchingRules: [
|
||||||
|
{
|
||||||
|
id: 'GPEYqFLv2dwzCM322',
|
||||||
|
weight: 1,
|
||||||
|
attribute: 'SeriesNumber',
|
||||||
|
constraint: {
|
||||||
|
equals: {
|
||||||
|
value: "1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
studyMatchingRules: [
|
||||||
|
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
viewportSettings: [
|
||||||
|
{
|
||||||
|
options: {
|
||||||
|
invert: true,
|
||||||
|
},
|
||||||
|
commandName: '',
|
||||||
|
type: 'viewport',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
imageMatchingRules: [
|
||||||
|
|
||||||
|
],
|
||||||
|
seriesMatchingRules: [
|
||||||
|
{
|
||||||
|
id: 'GPEYqFLv2dwzCM322',
|
||||||
|
weight: 1,
|
||||||
|
attribute: 'SeriesNumber',
|
||||||
|
constraint: {
|
||||||
|
equals: {
|
||||||
|
value: "6",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
studyMatchingRules: [
|
||||||
|
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
viewportSettings: [
|
||||||
|
{
|
||||||
|
options: {
|
||||||
|
invert: true,
|
||||||
|
},
|
||||||
|
commandName: '',
|
||||||
|
type: 'viewport',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
imageMatchingRules: [
|
||||||
|
|
||||||
|
],
|
||||||
|
seriesMatchingRules: [
|
||||||
|
{
|
||||||
|
id: 'GPEYqFLv2dwzCM322',
|
||||||
|
weight: 1,
|
||||||
|
attribute: 'SeriesDescription',
|
||||||
|
constraint: {
|
||||||
|
contains: {
|
||||||
|
value: "Corrected",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'GPEYqFLv2dwzCM322',
|
||||||
|
weight: 2,
|
||||||
|
attribute: 'SeriesDescription',
|
||||||
|
constraint: {
|
||||||
|
doesNotContain: {
|
||||||
|
value: "Uncorrected",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
studyMatchingRules: [
|
||||||
|
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
createdDate: '2021-02-23T18:32:42.850Z',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
numberOfPriorsReferenced: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
function getHangingProtocolModule() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: hangingProtocolName,
|
||||||
|
protocols: [petCTProtocol],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
export default getHangingProtocolModule;
|
||||||
@ -3,6 +3,7 @@ import getDataSourcesModule from './getDataSourcesModule.js';
|
|||||||
import getLayoutTemplateModule from './getLayoutTemplateModule.js';
|
import getLayoutTemplateModule from './getLayoutTemplateModule.js';
|
||||||
import getPanelModule from './getPanelModule.js';
|
import getPanelModule from './getPanelModule.js';
|
||||||
import getSopClassHandlerModule from './getSopClassHandlerModule.js';
|
import getSopClassHandlerModule from './getSopClassHandlerModule.js';
|
||||||
|
import getHangingProtocolModule from './getHangingProtocolModule.js';
|
||||||
import getToolbarModule from './getToolbarModule.js';
|
import getToolbarModule from './getToolbarModule.js';
|
||||||
import commandsModule from './commandsModule';
|
import commandsModule from './commandsModule';
|
||||||
import id from './id';
|
import id from './id';
|
||||||
@ -14,6 +15,7 @@ export default {
|
|||||||
id,
|
id,
|
||||||
getContextModule,
|
getContextModule,
|
||||||
getDataSourcesModule,
|
getDataSourcesModule,
|
||||||
|
getHangingProtocolModule,
|
||||||
getLayoutTemplateModule,
|
getLayoutTemplateModule,
|
||||||
getPanelModule,
|
getPanelModule,
|
||||||
getSopClassHandlerModule,
|
getSopClassHandlerModule,
|
||||||
|
|||||||
@ -32,7 +32,7 @@
|
|||||||
"@ohif/core": "^0.50.0",
|
"@ohif/core": "^0.50.0",
|
||||||
"@ohif/ui": "^0.50.0",
|
"@ohif/ui": "^0.50.0",
|
||||||
"cornerstone-core": "^2.3.0",
|
"cornerstone-core": "^2.3.0",
|
||||||
"cornerstone-math": "^0.1.8",
|
"cornerstone-math": "^0.1.9",
|
||||||
"cornerstone-tools": "5.1.2",
|
"cornerstone-tools": "5.1.2",
|
||||||
"cornerstone-wado-image-loader": "^3.1.2",
|
"cornerstone-wado-image-loader": "^3.1.2",
|
||||||
"dcmjs": "0.16.1",
|
"dcmjs": "0.16.1",
|
||||||
@ -40,7 +40,7 @@
|
|||||||
"hammerjs": "^2.0.8",
|
"hammerjs": "^2.0.8",
|
||||||
"prop-types": "^15.6.2",
|
"prop-types": "^15.6.2",
|
||||||
"react": "^16.11.0",
|
"react": "^16.11.0",
|
||||||
"react-cornerstone-viewport": "4.0.2"
|
"react-cornerstone-viewport": "4.0.5"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "7.7.6",
|
"@babel/runtime": "7.7.6",
|
||||||
|
|||||||
@ -34,7 +34,7 @@
|
|||||||
"dcmjs": "0.16.1",
|
"dcmjs": "0.16.1",
|
||||||
"prop-types": "^15.6.2",
|
"prop-types": "^15.6.2",
|
||||||
"react": "^16.13.1",
|
"react": "^16.13.1",
|
||||||
"react-cornerstone-viewport": "^4.0.4",
|
"react-cornerstone-viewport": "^4.0.5",
|
||||||
"react-dom": "^16.13.1",
|
"react-dom": "^16.13.1",
|
||||||
"webpack": "^4.0.0",
|
"webpack": "^4.0.0",
|
||||||
"webpack-merge": "^5.7.3"
|
"webpack-merge": "^5.7.3"
|
||||||
|
|||||||
@ -12,7 +12,6 @@ import promptTrackNewSeries from './promptTrackNewSeries';
|
|||||||
import promptTrackNewStudy from './promptTrackNewStudy';
|
import promptTrackNewStudy from './promptTrackNewStudy';
|
||||||
import promptSaveReport from './promptSaveReport';
|
import promptSaveReport from './promptSaveReport';
|
||||||
import promptHydrateStructuredReport from './promptHydrateStructuredReport';
|
import promptHydrateStructuredReport from './promptHydrateStructuredReport';
|
||||||
import hydrateStructuredReport from './_hydrateStructuredReport.js';
|
|
||||||
|
|
||||||
const TrackedMeasurementsContext = React.createContext();
|
const TrackedMeasurementsContext = React.createContext();
|
||||||
TrackedMeasurementsContext.displayName = 'TrackedMeasurementsContext';
|
TrackedMeasurementsContext.displayName = 'TrackedMeasurementsContext';
|
||||||
@ -99,6 +98,7 @@ function TrackedMeasurementsContextProvider(
|
|||||||
}),
|
}),
|
||||||
promptHydrateStructuredReport: promptHydrateStructuredReport.bind(null, {
|
promptHydrateStructuredReport: promptHydrateStructuredReport.bind(null, {
|
||||||
servicesManager,
|
servicesManager,
|
||||||
|
extensionManager
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import cornerstoneTools from 'cornerstone-tools';
|
import cornerstoneTools from 'cornerstone-tools';
|
||||||
import OHIF, { DicomMetadataStore } from '@ohif/core';
|
import OHIF, { DicomMetadataStore } from '@ohif/core';
|
||||||
import getLabelFromDCMJSImportedToolData from './utils/getLabelFromDCMJSImportedToolData';
|
import getLabelFromDCMJSImportedToolData from './utils/getLabelFromDCMJSImportedToolData';
|
||||||
import getToolStateToCornerstoneMeasurementSchema from './getToolStateToCornerstoneMeasurementSchema';
|
import getCornerstoneToolStateToMeasurementSchema from './getCornerstoneToolStateToMeasurementSchema';
|
||||||
import { adapters } from 'dcmjs';
|
import { adapters } from 'dcmjs';
|
||||||
|
|
||||||
const { guid } = OHIF.utils;
|
const { guid } = OHIF.utils;
|
||||||
@ -15,6 +15,7 @@ export default function _hydrateStructuredReport(
|
|||||||
{ servicesManager, extensionManager },
|
{ servicesManager, extensionManager },
|
||||||
displaySetInstanceUID
|
displaySetInstanceUID
|
||||||
) {
|
) {
|
||||||
|
const dataSource = extensionManager.getActiveDataSource()[0];
|
||||||
const { MeasurementService, DisplaySetService } = servicesManager.services;
|
const { MeasurementService, DisplaySetService } = servicesManager.services;
|
||||||
|
|
||||||
const displaySet = DisplaySetService.getDisplaySetByUID(
|
const displaySet = DisplaySetService.getDisplaySetByUID(
|
||||||
@ -114,14 +115,23 @@ export default function _hydrateStructuredReport(
|
|||||||
|
|
||||||
data.id = guid();
|
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
|
// Let the measurement service know we added to toolState
|
||||||
const toMeasurementSchema = getToolStateToCornerstoneMeasurementSchema(
|
const toMeasurementSchema = getCornerstoneToolStateToMeasurementSchema(
|
||||||
toolType,
|
toolType,
|
||||||
MeasurementService,
|
MeasurementService,
|
||||||
DisplaySetService,
|
DisplaySetService,
|
||||||
imageId
|
SOPInstanceUID,
|
||||||
|
FrameOfReferenceUID,
|
||||||
|
SeriesInstanceUID,
|
||||||
|
StudyInstanceUID
|
||||||
);
|
);
|
||||||
|
|
||||||
const source = MeasurementService.getSource('CornerstoneTools', '4');
|
const source = MeasurementService.getSource('CornerstoneTools', '4');
|
||||||
@ -132,7 +142,8 @@ export default function _hydrateStructuredReport(
|
|||||||
source,
|
source,
|
||||||
toolType,
|
toolType,
|
||||||
data,
|
data,
|
||||||
toMeasurementSchema
|
toMeasurementSchema,
|
||||||
|
dataSource
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!imageIds.includes(imageId)) {
|
if (!imageIds.includes(imageId)) {
|
||||||
@ -148,24 +159,3 @@ export default function _hydrateStructuredReport(
|
|||||||
SeriesInstanceUIDs,
|
SeriesInstanceUIDs,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function _addToolDataToCornerstoneTools(data, toolType, imageId) {
|
|
||||||
const toolState = globalImageIdSpecificToolStateManager.saveToolState();
|
|
||||||
|
|
||||||
if (toolState[imageId] === undefined) {
|
|
||||||
toolState[imageId] = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
const imageIdToolState = toolState[imageId];
|
|
||||||
|
|
||||||
// If we don't have tool state for this type of tool, add an empty object
|
|
||||||
if (imageIdToolState[toolType] === undefined) {
|
|
||||||
imageIdToolState[toolType] = {
|
|
||||||
data: [],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const toolData = imageIdToolState[toolType];
|
|
||||||
|
|
||||||
toolData.data.push(data);
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1,8 +1,11 @@
|
|||||||
export default function getToolStateToCornerstoneMeasurementSchema(
|
export default function getCornerstoneToolStateToMeasurementSchema(
|
||||||
toolType,
|
toolType,
|
||||||
MeasurementService,
|
MeasurementService,
|
||||||
DisplaySetService,
|
DisplaySetService,
|
||||||
imageId
|
SOPInstanceUID,
|
||||||
|
FrameOfReferenceUID,
|
||||||
|
SeriesInstanceUID,
|
||||||
|
StudyInstanceUID
|
||||||
) {
|
) {
|
||||||
const _getValueTypeFromToolType = toolType => {
|
const _getValueTypeFromToolType = toolType => {
|
||||||
const {
|
const {
|
||||||
@ -31,7 +34,10 @@ export default function getToolStateToCornerstoneMeasurementSchema(
|
|||||||
return measurementData =>
|
return measurementData =>
|
||||||
Length(
|
Length(
|
||||||
measurementData,
|
measurementData,
|
||||||
imageId,
|
SOPInstanceUID,
|
||||||
|
FrameOfReferenceUID,
|
||||||
|
SeriesInstanceUID,
|
||||||
|
StudyInstanceUID,
|
||||||
DisplaySetService,
|
DisplaySetService,
|
||||||
_getValueTypeFromToolType
|
_getValueTypeFromToolType
|
||||||
);
|
);
|
||||||
@ -39,7 +45,10 @@ export default function getToolStateToCornerstoneMeasurementSchema(
|
|||||||
return measurementData =>
|
return measurementData =>
|
||||||
Bidirectional(
|
Bidirectional(
|
||||||
measurementData,
|
measurementData,
|
||||||
imageId,
|
SOPInstanceUID,
|
||||||
|
FrameOfReferenceUID,
|
||||||
|
SeriesInstanceUID,
|
||||||
|
StudyInstanceUID,
|
||||||
DisplaySetService,
|
DisplaySetService,
|
||||||
_getValueTypeFromToolType
|
_getValueTypeFromToolType
|
||||||
);
|
);
|
||||||
@ -47,7 +56,10 @@ export default function getToolStateToCornerstoneMeasurementSchema(
|
|||||||
return measurementData =>
|
return measurementData =>
|
||||||
EllipticalRoi(
|
EllipticalRoi(
|
||||||
measurementData,
|
measurementData,
|
||||||
imageId,
|
SOPInstanceUID,
|
||||||
|
FrameOfReferenceUID,
|
||||||
|
SeriesInstanceUID,
|
||||||
|
StudyInstanceUID,
|
||||||
DisplaySetService,
|
DisplaySetService,
|
||||||
_getValueTypeFromToolType
|
_getValueTypeFromToolType
|
||||||
);
|
);
|
||||||
@ -55,7 +67,10 @@ export default function getToolStateToCornerstoneMeasurementSchema(
|
|||||||
return measurementData =>
|
return measurementData =>
|
||||||
ArrowAnnotate(
|
ArrowAnnotate(
|
||||||
measurementData,
|
measurementData,
|
||||||
imageId,
|
SOPInstanceUID,
|
||||||
|
FrameOfReferenceUID,
|
||||||
|
SeriesInstanceUID,
|
||||||
|
StudyInstanceUID,
|
||||||
DisplaySetService,
|
DisplaySetService,
|
||||||
_getValueTypeFromToolType
|
_getValueTypeFromToolType
|
||||||
);
|
);
|
||||||
@ -64,18 +79,14 @@ export default function getToolStateToCornerstoneMeasurementSchema(
|
|||||||
|
|
||||||
function Length(
|
function Length(
|
||||||
measurementData,
|
measurementData,
|
||||||
imageId,
|
SOPInstanceUID,
|
||||||
|
FrameOfReferenceUID,
|
||||||
|
SeriesInstanceUID,
|
||||||
|
StudyInstanceUID,
|
||||||
DisplaySetService,
|
DisplaySetService,
|
||||||
_getValueTypeFromToolType
|
_getValueTypeFromToolType
|
||||||
) {
|
) {
|
||||||
const tool = measurementData.toolType || measurementData.toolName;
|
const tool = measurementData.toolType || measurementData.toolName;
|
||||||
const instance = cornerstone.metaData.get('instance', imageId);
|
|
||||||
const {
|
|
||||||
SOPInstanceUID,
|
|
||||||
FrameOfReferenceUID,
|
|
||||||
SeriesInstanceUID,
|
|
||||||
StudyInstanceUID,
|
|
||||||
} = instance;
|
|
||||||
|
|
||||||
const displaySetInstanceUID = _getDisplaySetInstanceUID(
|
const displaySetInstanceUID = _getDisplaySetInstanceUID(
|
||||||
DisplaySetService,
|
DisplaySetService,
|
||||||
@ -113,18 +124,15 @@ function Length(
|
|||||||
|
|
||||||
function Bidirectional(
|
function Bidirectional(
|
||||||
measurementData,
|
measurementData,
|
||||||
imageId,
|
SOPInstanceUID,
|
||||||
|
FrameOfReferenceUID,
|
||||||
|
SeriesInstanceUID,
|
||||||
|
StudyInstanceUID,
|
||||||
DisplaySetService,
|
DisplaySetService,
|
||||||
_getValueTypeFromToolType
|
_getValueTypeFromToolType
|
||||||
) {
|
) {
|
||||||
const tool = measurementData.toolType || measurementData.toolName;
|
const tool = measurementData.toolType || measurementData.toolName;
|
||||||
const instance = cornerstone.metaData.get('instance', imageId);
|
|
||||||
const {
|
|
||||||
SOPInstanceUID,
|
|
||||||
FrameOfReferenceUID,
|
|
||||||
SeriesInstanceUID,
|
|
||||||
StudyInstanceUID,
|
|
||||||
} = instance;
|
|
||||||
const displaySetInstanceUID = _getDisplaySetInstanceUID(
|
const displaySetInstanceUID = _getDisplaySetInstanceUID(
|
||||||
DisplaySetService,
|
DisplaySetService,
|
||||||
SeriesInstanceUID,
|
SeriesInstanceUID,
|
||||||
@ -155,18 +163,14 @@ function Bidirectional(
|
|||||||
|
|
||||||
function EllipticalRoi(
|
function EllipticalRoi(
|
||||||
measurementData,
|
measurementData,
|
||||||
imageId,
|
SOPInstanceUID,
|
||||||
|
FrameOfReferenceUID,
|
||||||
|
SeriesInstanceUID,
|
||||||
|
StudyInstanceUID,
|
||||||
DisplaySetService,
|
DisplaySetService,
|
||||||
_getValueTypeFromToolType
|
_getValueTypeFromToolType
|
||||||
) {
|
) {
|
||||||
const tool = measurementData.toolType || measurementData.toolName;
|
const tool = measurementData.toolType || measurementData.toolName;
|
||||||
const instance = cornerstone.metaData.get('instance', imageId);
|
|
||||||
const {
|
|
||||||
SOPInstanceUID,
|
|
||||||
FrameOfReferenceUID,
|
|
||||||
SeriesInstanceUID,
|
|
||||||
StudyInstanceUID,
|
|
||||||
} = instance;
|
|
||||||
|
|
||||||
const displaySetInstanceUID = _getDisplaySetInstanceUID(
|
const displaySetInstanceUID = _getDisplaySetInstanceUID(
|
||||||
DisplaySetService,
|
DisplaySetService,
|
||||||
@ -222,18 +226,14 @@ function EllipticalRoi(
|
|||||||
|
|
||||||
function ArrowAnnotate(
|
function ArrowAnnotate(
|
||||||
measurementData,
|
measurementData,
|
||||||
imageId,
|
SOPInstanceUID,
|
||||||
|
FrameOfReferenceUID,
|
||||||
|
SeriesInstanceUID,
|
||||||
|
StudyInstanceUID,
|
||||||
DisplaySetService,
|
DisplaySetService,
|
||||||
_getValueTypeFromToolType
|
_getValueTypeFromToolType
|
||||||
) {
|
) {
|
||||||
const tool = measurementData.toolType || measurementData.toolName;
|
const tool = measurementData.toolType || measurementData.toolName;
|
||||||
const instance = cornerstone.metaData.get('instance', imageId);
|
|
||||||
const {
|
|
||||||
SOPInstanceUID,
|
|
||||||
FrameOfReferenceUID,
|
|
||||||
SeriesInstanceUID,
|
|
||||||
StudyInstanceUID,
|
|
||||||
} = instance;
|
|
||||||
|
|
||||||
const displaySetInstanceUID = _getDisplaySetInstanceUID(
|
const displaySetInstanceUID = _getDisplaySetInstanceUID(
|
||||||
DisplaySetService,
|
DisplaySetService,
|
||||||
@ -6,7 +6,7 @@ const RESPONSE = {
|
|||||||
SET_STUDY_AND_SERIES: 3,
|
SET_STUDY_AND_SERIES: 3,
|
||||||
};
|
};
|
||||||
|
|
||||||
function promptUser({ servicesManager }, ctx, evt) {
|
function promptUser({ servicesManager, extensionManager }, ctx, evt) {
|
||||||
const { UIViewportDialogService } = servicesManager.services;
|
const { UIViewportDialogService } = servicesManager.services;
|
||||||
const { viewportIndex, StudyInstanceUID, SeriesInstanceUID } = evt;
|
const { viewportIndex, StudyInstanceUID, SeriesInstanceUID } = evt;
|
||||||
|
|
||||||
|
|||||||
@ -12,12 +12,13 @@ const OHIFCornerstoneViewport = props => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
function getViewportModule({ servicesManager, commandsManager }) {
|
function getViewportModule({ servicesManager, commandsManager, extensionManager }) {
|
||||||
const ExtendedOHIFCornerstoneTrackingViewport = props => {
|
const ExtendedOHIFCornerstoneTrackingViewport = props => {
|
||||||
return (
|
return (
|
||||||
<OHIFCornerstoneViewport
|
<OHIFCornerstoneViewport
|
||||||
servicesManager={servicesManager}
|
servicesManager={servicesManager}
|
||||||
commandsManager={commandsManager}
|
commandsManager={commandsManager}
|
||||||
|
extensionManager={extensionManager}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -114,15 +114,16 @@ function PanelMeasurementTableTracking({ servicesManager, extensionManager }) {
|
|||||||
// Are we exposing the right API for measurementService?
|
// Are we exposing the right API for measurementService?
|
||||||
// This watches for ALL MeasurementService changes. It updates a timestamp,
|
// This watches for ALL MeasurementService changes. It updates a timestamp,
|
||||||
// which is debounced. After a brief period of inactivity, this triggers
|
// 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(() => {
|
useEffect(() => {
|
||||||
const added = MeasurementService.EVENTS.MEASUREMENT_ADDED;
|
const added = MeasurementService.EVENTS.MEASUREMENT_ADDED;
|
||||||
|
const addedRaw = MeasurementService.EVENTS.RAW_MEASUREMENT_ADDED;
|
||||||
const updated = MeasurementService.EVENTS.MEASUREMENT_UPDATED;
|
const updated = MeasurementService.EVENTS.MEASUREMENT_UPDATED;
|
||||||
const removed = MeasurementService.EVENTS.MEASUREMENT_REMOVED;
|
const removed = MeasurementService.EVENTS.MEASUREMENT_REMOVED;
|
||||||
const cleared = MeasurementService.EVENTS.MEASUREMENTS_CLEARED;
|
const cleared = MeasurementService.EVENTS.MEASUREMENTS_CLEARED;
|
||||||
const subscriptions = [];
|
const subscriptions = [];
|
||||||
|
|
||||||
[added, updated, removed, cleared].forEach(evt => {
|
[added, addedRaw, updated, removed, cleared].forEach(evt => {
|
||||||
subscriptions.push(
|
subscriptions.push(
|
||||||
MeasurementService.subscribe(evt, () => {
|
MeasurementService.subscribe(evt, () => {
|
||||||
setMeasurementsUpdated(Date.now().toString());
|
setMeasurementsUpdated(Date.now().toString());
|
||||||
|
|||||||
@ -28,7 +28,7 @@ function PanelStudyBrowserTracking({
|
|||||||
// Normally you nest the components so the tree isn't so deep, and the data
|
// 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.
|
// doesn't have to have such an intense shape. This works well enough for now.
|
||||||
// Tabs --> Studies --> DisplaySets --> Thumbnails
|
// Tabs --> Studies --> DisplaySets --> Thumbnails
|
||||||
const [{ StudyInstanceUIDs }, dispatchImageViewer] = useImageViewer();
|
const { StudyInstanceUIDs } = useImageViewer();
|
||||||
const [
|
const [
|
||||||
{ activeViewportIndex, viewports, numCols, numRows },
|
{ activeViewportIndex, viewports, numCols, numRows },
|
||||||
viewportGridService,
|
viewportGridService,
|
||||||
@ -58,27 +58,34 @@ function PanelStudyBrowserTracking({
|
|||||||
|
|
||||||
const isSingleViewport = numCols === 1 && numRows === 1;
|
const isSingleViewport = numCols === 1 && numRows === 1;
|
||||||
|
|
||||||
// TODO: Should this be somewhere else? Feels more like a mode "lifecycle" setup/destroy?
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const { unsubscribe } = MeasurementService.subscribe(
|
const added = MeasurementService.EVENTS.MEASUREMENT_ADDED;
|
||||||
MeasurementService.EVENTS.MEASUREMENT_ADDED,
|
const addedRaw = MeasurementService.EVENTS.RAW_MEASUREMENT_ADDED;
|
||||||
({ source, measurement }) => {
|
const subscriptions = [];
|
||||||
const {
|
|
||||||
referenceSeriesUID: SeriesInstanceUID,
|
|
||||||
referenceStudyUID: StudyInstanceUID,
|
|
||||||
} = measurement;
|
|
||||||
|
|
||||||
sendTrackedMeasurementsEvent('SET_DIRTY', { SeriesInstanceUID });
|
[added, addedRaw].forEach(evt => {
|
||||||
sendTrackedMeasurementsEvent('TRACK_SERIES', {
|
subscriptions.push(
|
||||||
viewportIndex: activeViewportIndex,
|
MeasurementService.subscribe(evt, ({ source, measurement }) => {
|
||||||
StudyInstanceUID,
|
const {
|
||||||
SeriesInstanceUID,
|
referenceSeriesUID: SeriesInstanceUID,
|
||||||
});
|
referenceStudyUID: StudyInstanceUID,
|
||||||
}
|
} = measurement;
|
||||||
);
|
|
||||||
|
|
||||||
return unsubscribe;
|
sendTrackedMeasurementsEvent('SET_DIRTY', { SeriesInstanceUID });
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
sendTrackedMeasurementsEvent('TRACK_SERIES', {
|
||||||
|
viewportIndex: activeViewportIndex,
|
||||||
|
StudyInstanceUID,
|
||||||
|
SeriesInstanceUID,
|
||||||
|
});
|
||||||
|
}).unsubscribe
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
subscriptions.forEach(unsub => {
|
||||||
|
unsub();
|
||||||
|
});
|
||||||
|
};
|
||||||
}, [MeasurementService, activeViewportIndex, sendTrackedMeasurementsEvent]);
|
}, [MeasurementService, activeViewportIndex, sendTrackedMeasurementsEvent]);
|
||||||
|
|
||||||
const { trackedStudy, trackedSeries } = trackedMeasurements.context;
|
const { trackedStudy, trackedSeries } = trackedMeasurements.context;
|
||||||
@ -89,7 +96,6 @@ function PanelStudyBrowserTracking({
|
|||||||
async function fetchStudiesForPatient(StudyInstanceUID) {
|
async function fetchStudiesForPatient(StudyInstanceUID) {
|
||||||
const qidoStudiesForPatient =
|
const qidoStudiesForPatient =
|
||||||
(await getStudiesForPatientByStudyInstanceUID(StudyInstanceUID)) || [];
|
(await getStudiesForPatientByStudyInstanceUID(StudyInstanceUID)) || [];
|
||||||
|
|
||||||
// TODO: This should be "naturalized DICOM JSON" studies
|
// TODO: This should be "naturalized DICOM JSON" studies
|
||||||
const mappedStudies = _mapDataSourceStudies(qidoStudiesForPatient);
|
const mappedStudies = _mapDataSourceStudies(qidoStudiesForPatient);
|
||||||
const actuallyMappedStudies = mappedStudies.map(qidoStudy => {
|
const actuallyMappedStudies = mappedStudies.map(qidoStudy => {
|
||||||
@ -182,6 +188,7 @@ function PanelStudyBrowserTracking({
|
|||||||
|
|
||||||
const imageIds = dataSource.getImageIdsForDisplaySet(displaySet);
|
const imageIds = dataSource.getImageIdsForDisplaySet(displaySet);
|
||||||
const imageId = imageIds[Math.floor(imageIds.length / 2)];
|
const imageId = imageIds[Math.floor(imageIds.length / 2)];
|
||||||
|
|
||||||
// TODO: Is it okay that imageIds are not returned here for SR displaysets?
|
// TODO: Is it okay that imageIds are not returned here for SR displaysets?
|
||||||
if (imageId) {
|
if (imageId) {
|
||||||
// When the image arrives, render it and store the result in the thumbnailImgSrcMap
|
// When the image arrives, render it and store the result in the thumbnailImgSrcMap
|
||||||
|
|||||||
@ -2,6 +2,7 @@ async function getStudiesForPatientByStudyInstanceUID(
|
|||||||
dataSource,
|
dataSource,
|
||||||
StudyInstanceUID
|
StudyInstanceUID
|
||||||
) {
|
) {
|
||||||
|
if (StudyInstanceUID === undefined) return;
|
||||||
// TODO: The `DicomMetadataStore` should short-circuit both of these requests
|
// TODO: The `DicomMetadataStore` should short-circuit both of these requests
|
||||||
// Data _could_ be here from route query, or if using JSON data source
|
// 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?
|
// We could also force this to "await" these values being available in the DICOMStore?
|
||||||
|
|||||||
@ -1,8 +1,7 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState, useRef } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import cornerstone from 'cornerstone-core';
|
import cornerstone from 'cornerstone-core';
|
||||||
import cornerstoneTools from 'cornerstone-tools';
|
import cornerstoneTools from 'cornerstone-tools';
|
||||||
import CornerstoneViewport from 'react-cornerstone-viewport';
|
|
||||||
import OHIF, { utils } from '@ohif/core';
|
import OHIF, { utils } from '@ohif/core';
|
||||||
import {
|
import {
|
||||||
Notification,
|
Notification,
|
||||||
@ -12,14 +11,10 @@ import {
|
|||||||
useViewportDialog,
|
useViewportDialog,
|
||||||
} from '@ohif/ui';
|
} from '@ohif/ui';
|
||||||
import { useTrackedMeasurements } from './../getContextModule';
|
import { useTrackedMeasurements } from './../getContextModule';
|
||||||
|
|
||||||
import ViewportOverlay from './ViewportOverlay';
|
|
||||||
import ViewportLoadingIndicator from './ViewportLoadingIndicator';
|
|
||||||
import setCornerstoneMeasurementActive from '../_shared/setCornerstoneMeasurementActive';
|
import setCornerstoneMeasurementActive from '../_shared/setCornerstoneMeasurementActive';
|
||||||
import setActiveAndPassiveToolsForElement from '../_shared/setActiveAndPassiveToolsForElement';
|
import setActiveAndPassiveToolsForElement from '../_shared/setActiveAndPassiveToolsForElement';
|
||||||
import getTools from '../_shared/getTools';
|
import getTools from '../_shared/getTools';
|
||||||
|
|
||||||
const scrollToIndex = cornerstoneTools.importInternal('util/scrollToIndex');
|
|
||||||
const { formatDate } = utils;
|
const { formatDate } = utils;
|
||||||
|
|
||||||
// TODO -> Get this list from the list of tracked measurements.
|
// TODO -> Get this list from the list of tracked measurements.
|
||||||
@ -40,19 +35,19 @@ const BaseAnnotationTool = cornerstoneTools.importInternal(
|
|||||||
|
|
||||||
const { StackManager } = OHIF.utils;
|
const { StackManager } = OHIF.utils;
|
||||||
|
|
||||||
function TrackedCornerstoneViewport({
|
function TrackedCornerstoneViewport(props) {
|
||||||
children,
|
|
||||||
dataSource,
|
|
||||||
displaySet,
|
|
||||||
viewportIndex,
|
|
||||||
servicesManager,
|
|
||||||
commandsManager,
|
|
||||||
}) {
|
|
||||||
const {
|
const {
|
||||||
ToolBarService,
|
children,
|
||||||
DisplaySetService,
|
dataSource,
|
||||||
MeasurementService,
|
displaySet,
|
||||||
} = servicesManager.services;
|
viewportIndex,
|
||||||
|
servicesManager,
|
||||||
|
extensionManager,
|
||||||
|
commandsManager,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const { ToolBarService } = servicesManager.services;
|
||||||
|
|
||||||
const [trackedMeasurements] = useTrackedMeasurements();
|
const [trackedMeasurements] = useTrackedMeasurements();
|
||||||
const [
|
const [
|
||||||
{ activeViewportIndex, viewports },
|
{ activeViewportIndex, viewports },
|
||||||
@ -60,73 +55,9 @@ function TrackedCornerstoneViewport({
|
|||||||
] = useViewportGrid();
|
] = useViewportGrid();
|
||||||
const [{ isCineEnabled, cines }, cineService] = useCine();
|
const [{ isCineEnabled, cines }, cineService] = useCine();
|
||||||
const [viewportDialogState, viewportDialogApi] = useViewportDialog();
|
const [viewportDialogState, viewportDialogApi] = useViewportDialog();
|
||||||
const [viewportData, setViewportData] = useState(null);
|
|
||||||
const [element, setElement] = useState(null);
|
|
||||||
const [isTracked, setIsTracked] = useState(false);
|
const [isTracked, setIsTracked] = useState(false);
|
||||||
const [trackedMeasurementId, setTrackedMeasurementId] = useState(null);
|
const [trackedMeasurementId, setTrackedMeasurementId] = useState(null);
|
||||||
|
const [element, setElement] = 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 onElementEnabled = evt => {
|
const onElementEnabled = evt => {
|
||||||
const eventData = evt.detail;
|
const eventData = evt.detail;
|
||||||
@ -191,51 +122,33 @@ function TrackedCornerstoneViewport({
|
|||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const {
|
if (!element) {
|
||||||
StudyInstanceUID,
|
|
||||||
displaySetInstanceUID,
|
|
||||||
sopClassUids,
|
|
||||||
} = displaySet;
|
|
||||||
|
|
||||||
if (!StudyInstanceUID || !displaySetInstanceUID) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const allTools = cornerstoneTools.store.state.tools;
|
||||||
|
const toolsForElement = allTools.filter(tool => tool.element === element);
|
||||||
|
|
||||||
if (sopClassUids && sopClassUids.length > 1) {
|
toolsForElement.forEach(tool => {
|
||||||
console.warn(
|
if (
|
||||||
'More than one SOPClassUID in the same series is not yet supported.'
|
tool instanceof ArrowAnnotateTool ||
|
||||||
);
|
tool instanceof BidirectionalTool ||
|
||||||
}
|
tool instanceof EllipticalRoiTool ||
|
||||||
|
tool instanceof LengthTool
|
||||||
|
) {
|
||||||
|
const configuration = tool.configuration;
|
||||||
|
|
||||||
_getViewportData(dataSource, displaySet).then(setViewportData);
|
configuration.renderDashed = !isTracked;
|
||||||
}, [dataSource, displaySet, viewports, viewportIndex]);
|
|
||||||
|
|
||||||
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
tool.configuration = configuration;
|
||||||
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,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
const enabledElement = cornerstone.getEnabledElement(element);
|
||||||
|
|
||||||
|
if (enabledElement.image) {
|
||||||
|
cornerstone.updateImage(element);
|
||||||
|
}
|
||||||
|
}, [isTracked]);
|
||||||
|
|
||||||
// We have...
|
// We have...
|
||||||
// StudyInstanceUid, DisplaySetInstanceUid
|
// 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 cine = cines[viewportIndex];
|
||||||
const isPlaying = (cine && cine.isPlaying) || false;
|
const isPlaying = (cine && cine.isPlaying) || false;
|
||||||
const frameRate = (cine && cine.frameRate) || 24;
|
const frameRate = (cine && cine.frameRate) || 24;
|
||||||
@ -341,10 +267,12 @@ function TrackedCornerstoneViewport({
|
|||||||
patientSex: PatientSex || '',
|
patientSex: PatientSex || '',
|
||||||
patientAge: PatientAge || '',
|
patientAge: PatientAge || '',
|
||||||
MRN: PatientID || '',
|
MRN: PatientID || '',
|
||||||
thickness: SliceThickness ? `${SliceThickness.toFixed(2)}mm` : '',
|
thickness: SliceThickness
|
||||||
|
? `${parseFloat(SliceThickness).toFixed(2)}mm`
|
||||||
|
: '',
|
||||||
spacing:
|
spacing:
|
||||||
SpacingBetweenSlices !== undefined
|
SpacingBetweenSlices !== undefined
|
||||||
? `${SpacingBetweenSlices.toFixed(2)}mm`
|
? `${parseFloat(SpacingBetweenSlices).toFixed(2)}mm`
|
||||||
: '',
|
: '',
|
||||||
scanner: ManufacturerModelName || '',
|
scanner: ManufacturerModelName || '',
|
||||||
},
|
},
|
||||||
@ -362,29 +290,7 @@ function TrackedCornerstoneViewport({
|
|||||||
/>
|
/>
|
||||||
{/* TODO: Viewport interface to accept stack or layers of content like this? */}
|
{/* TODO: Viewport interface to accept stack or layers of content like this? */}
|
||||||
<div className="relative flex flex-row w-full h-full overflow-hidden">
|
<div className="relative flex flex-row w-full h-full overflow-hidden">
|
||||||
<CornerstoneViewport
|
{renderViewport()}
|
||||||
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()}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<div className="absolute w-full">
|
<div className="absolute w-full">
|
||||||
{viewportDialogState.viewportIndex === viewportIndex && (
|
{viewportDialogState.viewportIndex === viewportIndex && (
|
||||||
<Notification
|
<Notification
|
||||||
@ -397,7 +303,6 @@ function TrackedCornerstoneViewport({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{childrenWithProps}
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
@ -417,54 +322,6 @@ TrackedCornerstoneViewport.defaultProps = {
|
|||||||
|
|
||||||
const _viewportLabels = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I'];
|
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(
|
function _getNextMeasurementId(
|
||||||
direction,
|
direction,
|
||||||
servicesManager,
|
servicesManager,
|
||||||
@ -518,112 +375,4 @@ function _getNextMeasurementId(
|
|||||||
return newTrackedMeasurementId;
|
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;
|
export default TrackedCornerstoneViewport;
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import { hotkeys } from '@ohif/core';
|
|||||||
const ohif = {
|
const ohif = {
|
||||||
layout: 'org.ohif.default.layoutTemplateModule.viewerLayout',
|
layout: 'org.ohif.default.layoutTemplateModule.viewerLayout',
|
||||||
sopClassHandler: 'org.ohif.default.sopClassHandlerModule.stack',
|
sopClassHandler: 'org.ohif.default.sopClassHandlerModule.stack',
|
||||||
|
hangingProtocols: 'org.ohif.default.hangingProtocolModule.default',
|
||||||
};
|
};
|
||||||
|
|
||||||
const tracked = {
|
const tracked = {
|
||||||
@ -17,6 +18,7 @@ const dicomsr = {
|
|||||||
viewport: 'org.ohif.dicom-sr.viewportModule.dicom-sr',
|
viewport: 'org.ohif.dicom-sr.viewportModule.dicom-sr',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
export default function mode({ modeConfiguration }) {
|
export default function mode({ modeConfiguration }) {
|
||||||
return {
|
return {
|
||||||
// TODO: We're using this as a route segment
|
// TODO: We're using this as a route segment
|
||||||
@ -40,6 +42,18 @@ export default function mode({ modeConfiguration }) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
ToolBarService.recordInteraction(interaction);
|
ToolBarService.recordInteraction(interaction);
|
||||||
|
|
||||||
|
ToolBarService.init(extensionManager);
|
||||||
|
ToolBarService.addButtons(toolbarButtons);
|
||||||
|
ToolBarService.createButtonSection('primary', [
|
||||||
|
'MeasurementTools',
|
||||||
|
'Zoom',
|
||||||
|
'WindowLevel',
|
||||||
|
'Pan',
|
||||||
|
'Capture',
|
||||||
|
'Layout',
|
||||||
|
'MoreTools',
|
||||||
|
]);
|
||||||
},
|
},
|
||||||
onModeExit: () => {},
|
onModeExit: () => {},
|
||||||
validationTags: {
|
validationTags: {
|
||||||
@ -53,21 +67,10 @@ export default function mode({ modeConfiguration }) {
|
|||||||
routes: [
|
routes: [
|
||||||
{
|
{
|
||||||
path: 'longitudinal',
|
path: 'longitudinal',
|
||||||
init: ({ servicesManager, extensionManager }) => {
|
/*init: ({ servicesManager, extensionManager }) => {
|
||||||
const { ToolBarService } = servicesManager.services;
|
//defaultViewerRouteInit
|
||||||
ToolBarService.init(extensionManager);
|
},*/
|
||||||
ToolBarService.addButtons(toolbarButtons);
|
layoutTemplate: ({ location, servicesManager }) => {
|
||||||
ToolBarService.createButtonSection('primary', [
|
|
||||||
'MeasurementTools',
|
|
||||||
'Zoom',
|
|
||||||
'WindowLevel',
|
|
||||||
'Pan',
|
|
||||||
'Capture',
|
|
||||||
'Layout',
|
|
||||||
'MoreTools',
|
|
||||||
]);
|
|
||||||
},
|
|
||||||
layoutTemplate: ({ routeProps }) => {
|
|
||||||
return {
|
return {
|
||||||
id: ohif.layout,
|
id: ohif.layout,
|
||||||
props: {
|
props: {
|
||||||
@ -95,6 +98,7 @@ export default function mode({ modeConfiguration }) {
|
|||||||
'org.ohif.measurement-tracking',
|
'org.ohif.measurement-tracking',
|
||||||
'org.ohif.dicom-sr',
|
'org.ohif.dicom-sr',
|
||||||
],
|
],
|
||||||
|
hangingProtocols: [ohif.hangingProtocols],
|
||||||
sopClassHandlers: [ohif.sopClassHandler, dicomsr.sopClassHandler],
|
sopClassHandlers: [ohif.sopClassHandler, dicomsr.sopClassHandler],
|
||||||
hotkeys: [...hotkeys.defaults.hotkeyBindings],
|
hotkeys: [...hotkeys.defaults.hotkeyBindings],
|
||||||
};
|
};
|
||||||
|
|||||||
@ -74,7 +74,7 @@
|
|||||||
"cross-env": "^5.2.0",
|
"cross-env": "^5.2.0",
|
||||||
"css-loader": "^3.2.0",
|
"css-loader": "^3.2.0",
|
||||||
"dotenv": "^8.1.0",
|
"dotenv": "^8.1.0",
|
||||||
"eslint": "5.16.0",
|
"eslint": "6.8.0",
|
||||||
"eslint-config-prettier": "^6.4.0",
|
"eslint-config-prettier": "^6.4.0",
|
||||||
"eslint-config-react-app": "^5.2.0",
|
"eslint-config-react-app": "^5.2.0",
|
||||||
"eslint-plugin-flowtype": "2.x",
|
"eslint-plugin-flowtype": "2.x",
|
||||||
@ -84,7 +84,7 @@
|
|||||||
"eslint-plugin-prettier": "^3.1.1",
|
"eslint-plugin-prettier": "^3.1.1",
|
||||||
"eslint-plugin-promise": "^4.2.1",
|
"eslint-plugin-promise": "^4.2.1",
|
||||||
"eslint-plugin-react": "7.x",
|
"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",
|
"extract-css-chunks-webpack-plugin": "^4.5.4",
|
||||||
"html-webpack-plugin": "^3.2.0",
|
"html-webpack-plugin": "^3.2.0",
|
||||||
"husky": "^3.0.0",
|
"husky": "^3.0.0",
|
||||||
|
|||||||
@ -45,7 +45,8 @@
|
|||||||
"mousetrap": "^1.6.3",
|
"mousetrap": "^1.6.3",
|
||||||
"query-string": "^6.14.0",
|
"query-string": "^6.14.0",
|
||||||
"object-hash": "2.1.1",
|
"object-hash": "2.1.1",
|
||||||
"cornerstone-math": "0.1.9"
|
"cornerstone-math": "0.1.9",
|
||||||
|
"validate.js": "^0.12.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"webpack-merge": "5.7.3"
|
"webpack-merge": "5.7.3"
|
||||||
|
|||||||
@ -17,9 +17,11 @@ function create({
|
|||||||
retrieve,
|
retrieve,
|
||||||
store,
|
store,
|
||||||
reject,
|
reject,
|
||||||
|
parseRouteParams,
|
||||||
retrieveSeriesMetadata,
|
retrieveSeriesMetadata,
|
||||||
deleteStudyMetadataPromise,
|
deleteStudyMetadataPromise,
|
||||||
getImageIdsForDisplaySet,
|
getImageIdsForDisplaySet,
|
||||||
|
getImageIdsForInstance,
|
||||||
}) {
|
}) {
|
||||||
const defaultQuery = {
|
const defaultQuery = {
|
||||||
studies: {
|
studies: {
|
||||||
@ -56,15 +58,16 @@ function create({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const defaultReject = {};
|
const defaultReject = {};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
query: query || defaultQuery,
|
query: query || defaultQuery,
|
||||||
retrieve: retrieve || defaultRetrieve,
|
retrieve: retrieve || defaultRetrieve,
|
||||||
reject: reject || defaultReject,
|
reject: reject || defaultReject,
|
||||||
store: store || defaultStore,
|
store: store || defaultStore,
|
||||||
getImageIdsForDisplaySet,
|
parseRouteParams,
|
||||||
retrieveSeriesMetadata,
|
retrieveSeriesMetadata,
|
||||||
deleteStudyMetadataPromise,
|
deleteStudyMetadataPromise,
|
||||||
|
getImageIdsForDisplaySet,
|
||||||
|
getImageIdsForInstance,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -189,7 +189,7 @@ class MetadataProvider {
|
|||||||
metadata = {
|
metadata = {
|
||||||
modality: instance.Modality,
|
modality: instance.Modality,
|
||||||
seriesInstanceUID: instance.SeriesInstanceUID,
|
seriesInstanceUID: instance.SeriesInstanceUID,
|
||||||
seriesNumber: instance.SeriesNumber,
|
seriesNumber: getNumberValues(instance.SeriesNumber),
|
||||||
studyInstanceUID: instance.StudyInstanceUID,
|
studyInstanceUID: instance.StudyInstanceUID,
|
||||||
seriesDate,
|
seriesDate,
|
||||||
seriesTime,
|
seriesTime,
|
||||||
@ -197,9 +197,9 @@ class MetadataProvider {
|
|||||||
break;
|
break;
|
||||||
case WADO_IMAGE_LOADER_TAGS.PATIENT_STUDY_MODULE:
|
case WADO_IMAGE_LOADER_TAGS.PATIENT_STUDY_MODULE:
|
||||||
metadata = {
|
metadata = {
|
||||||
patientAge: instance.PatientAge,
|
patientAge: getNumberValues(instance.PatientAge),
|
||||||
patientSize: instance.PatientSize,
|
patientSize: getNumberValues(instance.PatientSize),
|
||||||
patientWeight: instance.PatientWeight,
|
patientWeight: getNumberValues(instance.PatientWeight),
|
||||||
};
|
};
|
||||||
break;
|
break;
|
||||||
case WADO_IMAGE_LOADER_TAGS.IMAGE_PLANE_MODULE:
|
case WADO_IMAGE_LOADER_TAGS.IMAGE_PLANE_MODULE:
|
||||||
@ -228,51 +228,53 @@ class MetadataProvider {
|
|||||||
|
|
||||||
metadata = {
|
metadata = {
|
||||||
frameOfReferenceUID: instance.FrameOfReferenceUID,
|
frameOfReferenceUID: instance.FrameOfReferenceUID,
|
||||||
rows: instance.Rows,
|
rows: getNumberValues(instance.Rows),
|
||||||
columns: instance.Columns,
|
columns: getNumberValues(instance.Columns),
|
||||||
imageOrientationPatient: ImageOrientationPatient,
|
imageOrientationPatient: getNumberValues(ImageOrientationPatient),
|
||||||
rowCosines,
|
rowCosines: getNumberValues(rowCosines),
|
||||||
columnCosines,
|
columnCosines: getNumberValues(columnCosines),
|
||||||
imagePositionPatient: instance.ImagePositionPatient,
|
imagePositionPatient: getNumberValues(instance.ImagePositionPatient),
|
||||||
sliceThickness: instance.SliceThickness,
|
sliceThickness: getNumberValues(instance.SliceThickness),
|
||||||
sliceLocation: instance.SliceLocation,
|
sliceLocation: getNumberValues(instance.SliceLocation),
|
||||||
pixelSpacing: PixelSpacing,
|
pixelSpacing: getNumberValues(PixelSpacing),
|
||||||
rowPixelSpacing,
|
rowPixelSpacing: getNumberValues(rowPixelSpacing),
|
||||||
columnPixelSpacing,
|
columnPixelSpacing: getNumberValues(columnPixelSpacing),
|
||||||
};
|
};
|
||||||
break;
|
break;
|
||||||
case WADO_IMAGE_LOADER_TAGS.IMAGE_PIXEL_MODULE:
|
case WADO_IMAGE_LOADER_TAGS.IMAGE_PIXEL_MODULE:
|
||||||
metadata = {
|
metadata = {
|
||||||
samplesPerPixel: instance.SamplesPerPixel,
|
samplesPerPixel: getNumberValues(instance.SamplesPerPixel),
|
||||||
photometricInterpretation: instance.PhotometricInterpretation,
|
photometricInterpretation: getNumberValues(instance.PhotometricInterpretation),
|
||||||
rows: instance.Rows,
|
rows: getNumberValues(instance.Rows),
|
||||||
columns: instance.Columns,
|
columns: getNumberValues(instance.Columns),
|
||||||
bitsAllocated: instance.BitsAllocated,
|
bitsAllocated: getNumberValues(instance.BitsAllocated),
|
||||||
bitsStored: instance.BitsStored,
|
bitsStored: getNumberValues(instance.BitsStored),
|
||||||
highBit: instance.HighBit,
|
highBit: getNumberValues(instance.HighBit),
|
||||||
pixelRepresentation: instance.PixelRepresentation,
|
pixelRepresentation: getNumberValues(instance.PixelRepresentation),
|
||||||
planarConfiguration: instance.PlanarConfiguration,
|
planarConfiguration: getNumberValues(instance.PlanarConfiguration),
|
||||||
pixelAspectRatio: instance.PixelAspectRatio,
|
pixelAspectRatio: getNumberValues(instance.PixelAspectRatio),
|
||||||
smallestPixelValue: instance.SmallestPixelValue,
|
smallestPixelValue: getNumberValues(instance.SmallestPixelValue),
|
||||||
largestPixelValue: instance.LargestPixelValue,
|
largestPixelValue: getNumberValues(instance.LargestPixelValue),
|
||||||
redPaletteColorLookupTableDescriptor:
|
redPaletteColorLookupTableDescriptor:
|
||||||
instance.RedPaletteColorLookupTableDescriptor,
|
getNumberValues(instance.RedPaletteColorLookupTableDescriptor),
|
||||||
greenPaletteColorLookupTableDescriptor:
|
greenPaletteColorLookupTableDescriptor:
|
||||||
instance.GreenPaletteColorLookupTableDescriptor,
|
getNumberValues(instance.GreenPaletteColorLookupTableDescriptor),
|
||||||
bluePaletteColorLookupTableDescriptor:
|
bluePaletteColorLookupTableDescriptor:
|
||||||
instance.BluePaletteColorLookupTableDescriptor,
|
getNumberValues(instance.BluePaletteColorLookupTableDescriptor),
|
||||||
redPaletteColorLookupTableData:
|
redPaletteColorLookupTableData:
|
||||||
instance.RedPaletteColorLookupTableData,
|
getNumberValues(instance.RedPaletteColorLookupTableData),
|
||||||
greenPaletteColorLookupTableData:
|
greenPaletteColorLookupTableData:
|
||||||
instance.GreenPaletteColorLookupTableData,
|
getNumberValues(instance.GreenPaletteColorLookupTableData),
|
||||||
bluePaletteColorLookupTableData:
|
bluePaletteColorLookupTableData:
|
||||||
instance.BluePaletteColorLookupTableData,
|
getNumberValues(instance.BluePaletteColorLookupTableData),
|
||||||
};
|
};
|
||||||
|
|
||||||
break;
|
break;
|
||||||
case WADO_IMAGE_LOADER_TAGS.VOI_LUT_MODULE:
|
case WADO_IMAGE_LOADER_TAGS.VOI_LUT_MODULE:
|
||||||
const { WindowCenter, WindowWidth } = instance;
|
const { WindowCenter, WindowWidth } = instance;
|
||||||
|
if (WindowCenter === undefined || WindowWidth === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const windowCenter = Array.isArray(WindowCenter)
|
const windowCenter = Array.isArray(WindowCenter)
|
||||||
? WindowCenter
|
? WindowCenter
|
||||||
: [WindowCenter];
|
: [WindowCenter];
|
||||||
@ -281,15 +283,20 @@ class MetadataProvider {
|
|||||||
: [WindowWidth];
|
: [WindowWidth];
|
||||||
|
|
||||||
metadata = {
|
metadata = {
|
||||||
windowCenter,
|
windowCenter: getNumberValues(windowCenter),
|
||||||
windowWidth,
|
windowWidth: getNumberValues(windowWidth),
|
||||||
};
|
};
|
||||||
|
|
||||||
break;
|
break;
|
||||||
case WADO_IMAGE_LOADER_TAGS.MODALITY_LUT_MODULE:
|
case WADO_IMAGE_LOADER_TAGS.MODALITY_LUT_MODULE:
|
||||||
|
const { RescaleIntercept, RescaleSlope } = instance;
|
||||||
|
if (RescaleIntercept === undefined || RescaleSlope === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
metadata = {
|
metadata = {
|
||||||
rescaleIntercept: instance.RescaleIntercept,
|
rescaleIntercept: getNumberValues(instance.RescaleIntercept),
|
||||||
rescaleSlope: instance.RescaleSlope,
|
rescaleSlope: getNumberValues(instance.RescaleSlope),
|
||||||
rescaleType: instance.RescaleType,
|
rescaleType: instance.RescaleType,
|
||||||
};
|
};
|
||||||
break;
|
break;
|
||||||
@ -401,7 +408,7 @@ class MetadataProvider {
|
|||||||
case WADO_IMAGE_LOADER_TAGS.GENERAL_IMAGE_MODULE:
|
case WADO_IMAGE_LOADER_TAGS.GENERAL_IMAGE_MODULE:
|
||||||
metadata = {
|
metadata = {
|
||||||
sopInstanceUid: instance.SOPInstanceUID,
|
sopInstanceUid: instance.SOPInstanceUID,
|
||||||
instanceNumber: instance.InstanceNumber,
|
instanceNumber: getNumberValues(instance.InstanceNumber),
|
||||||
lossyImageCompression: instance.LossyImageCompression,
|
lossyImageCompression: instance.LossyImageCompression,
|
||||||
lossyImageCompressionRatio: instance.LossyImageCompressionRatio,
|
lossyImageCompressionRatio: instance.LossyImageCompressionRatio,
|
||||||
lossyImageCompressionMethod: instance.LossyImageCompressionMethod,
|
lossyImageCompressionMethod: instance.LossyImageCompressionMethod,
|
||||||
@ -457,6 +464,31 @@ class MetadataProvider {
|
|||||||
|
|
||||||
const metadataProvider = new 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;
|
export default metadataProvider;
|
||||||
|
|
||||||
const WADO_IMAGE_LOADER_TAGS = {
|
const WADO_IMAGE_LOADER_TAGS = {
|
||||||
@ -479,3 +511,4 @@ const WADO_IMAGE_LOADER_TAGS = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const INSTANCE = 'instance';
|
const INSTANCE = 'instance';
|
||||||
|
const DICOMWEB = 'dicomweb';
|
||||||
|
|||||||
@ -2,7 +2,12 @@ import MODULE_TYPES from './MODULE_TYPES.js';
|
|||||||
import log from './../log.js';
|
import log from './../log.js';
|
||||||
|
|
||||||
export default class ExtensionManager {
|
export default class ExtensionManager {
|
||||||
constructor({ commandsManager, servicesManager, hotkeysManager, appConfig = {} }) {
|
constructor({
|
||||||
|
commandsManager,
|
||||||
|
servicesManager,
|
||||||
|
hotkeysManager,
|
||||||
|
appConfig = {},
|
||||||
|
}) {
|
||||||
this.modules = {};
|
this.modules = {};
|
||||||
this.registeredExtensionIds = [];
|
this.registeredExtensionIds = [];
|
||||||
this.moduleTypeNames = Object.values(MODULE_TYPES);
|
this.moduleTypeNames = Object.values(MODULE_TYPES);
|
||||||
@ -52,7 +57,7 @@ export default class ExtensionManager {
|
|||||||
onModeEnter({
|
onModeEnter({
|
||||||
servicesManager: _servicesManager,
|
servicesManager: _servicesManager,
|
||||||
commandsManager: _commandsManager,
|
commandsManager: _commandsManager,
|
||||||
hotkeysManager: _hotkeysManager
|
hotkeysManager: _hotkeysManager,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -115,9 +120,7 @@ export default class ExtensionManager {
|
|||||||
*/
|
*/
|
||||||
registerExtension = (extension, configuration = {}, dataSources = []) => {
|
registerExtension = (extension, configuration = {}, dataSources = []) => {
|
||||||
if (!extension) {
|
if (!extension) {
|
||||||
throw new Error(
|
throw new Error('Attempting to register a null/undefined extension.');
|
||||||
'Attempting to register a null/undefined extension.'
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let extensionId = extension.id;
|
let extensionId = extension.id;
|
||||||
@ -191,6 +194,13 @@ export default class ExtensionManager {
|
|||||||
] = element;
|
] = element;
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
|
case MODULE_TYPES.HANGING_PROTOCOL:
|
||||||
|
extensionModule.forEach(element => {
|
||||||
|
this.modulesMap[
|
||||||
|
`${extensionId}.${moduleType}.${element.name}`
|
||||||
|
] = element;
|
||||||
|
});
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
throw new Error(`Module type invalid: ${moduleType}`);
|
throw new Error(`Module type invalid: ${moduleType}`);
|
||||||
}
|
}
|
||||||
@ -224,6 +234,10 @@ export default class ExtensionManager {
|
|||||||
return this.dataSourceMap[this.activeDataSource];
|
return this.dataSourceMap[this.activeDataSource];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
getDataSource = () => {
|
||||||
|
return this.dataSourceMap[this.activeDataSource];
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @private
|
* @private
|
||||||
* @param {string} moduleType
|
* @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
|
* @private
|
||||||
|
|||||||
@ -215,6 +215,9 @@ describe('ExtensionManager.js', () => {
|
|||||||
getDataSourcesModule: () => {
|
getDataSourcesModule: () => {
|
||||||
return [{}];
|
return [{}];
|
||||||
},
|
},
|
||||||
|
getHangingProtocolModule: () => {
|
||||||
|
return [{}];
|
||||||
|
},
|
||||||
getContextModule: () => {
|
getContextModule: () => {
|
||||||
return [{}];
|
return [{}];
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,4 +7,5 @@ export default {
|
|||||||
VIEWPORT: 'viewportModule',
|
VIEWPORT: 'viewportModule',
|
||||||
CONTEXT: 'contextModule',
|
CONTEXT: 'contextModule',
|
||||||
LAYOUT_TEMPLATE: 'layoutTemplateModule',
|
LAYOUT_TEMPLATE: 'layoutTemplateModule',
|
||||||
|
HANGING_PROTOCOL: 'hangingProtocolModule',
|
||||||
};
|
};
|
||||||
|
|||||||
@ -25,6 +25,7 @@ import {
|
|||||||
MeasurementService,
|
MeasurementService,
|
||||||
ViewportGridService,
|
ViewportGridService,
|
||||||
HangingProtocolService,
|
HangingProtocolService,
|
||||||
|
pubSubServiceInterface,
|
||||||
} from './services';
|
} from './services';
|
||||||
|
|
||||||
import IWebApiDataSource from './DataSources/IWebApiDataSource';
|
import IWebApiDataSource from './DataSources/IWebApiDataSource';
|
||||||
@ -67,6 +68,7 @@ const OHIF = {
|
|||||||
HangingProtocolService,
|
HangingProtocolService,
|
||||||
IWebApiDataSource,
|
IWebApiDataSource,
|
||||||
DicomMetadataStore,
|
DicomMetadataStore,
|
||||||
|
pubSubServiceInterface,
|
||||||
};
|
};
|
||||||
|
|
||||||
export {
|
export {
|
||||||
@ -101,6 +103,7 @@ export {
|
|||||||
HangingProtocolService,
|
HangingProtocolService,
|
||||||
IWebApiDataSource,
|
IWebApiDataSource,
|
||||||
DicomMetadataStore,
|
DicomMetadataStore,
|
||||||
|
pubSubServiceInterface,
|
||||||
};
|
};
|
||||||
|
|
||||||
export { OHIF };
|
export { OHIF };
|
||||||
|
|||||||
@ -10,21 +10,6 @@ describe('Top level exports', () => {
|
|||||||
'HotkeysManager',
|
'HotkeysManager',
|
||||||
'ServicesManager',
|
'ServicesManager',
|
||||||
//
|
//
|
||||||
'DicomMetadataStore',
|
|
||||||
//
|
|
||||||
'CineService',
|
|
||||||
'DisplaySetService',
|
|
||||||
'HangingProtocolService',
|
|
||||||
'ToolBarService',
|
|
||||||
'UINotificationService',
|
|
||||||
'UIModalService',
|
|
||||||
'UIDialogService',
|
|
||||||
'UIViewportDialogService',
|
|
||||||
'MeasurementService',
|
|
||||||
'ViewportGridService',
|
|
||||||
//
|
|
||||||
'IWebApiDataSource',
|
|
||||||
//
|
|
||||||
'defaults',
|
'defaults',
|
||||||
'utils',
|
'utils',
|
||||||
'hotkeys',
|
'hotkeys',
|
||||||
@ -37,7 +22,21 @@ describe('Top level exports', () => {
|
|||||||
'log',
|
'log',
|
||||||
'DICOMWeb',
|
'DICOMWeb',
|
||||||
'DICOMSR',
|
'DICOMSR',
|
||||||
'OHIF', //
|
'OHIF',
|
||||||
|
//
|
||||||
|
'CineService',
|
||||||
|
'UIDialogService',
|
||||||
|
'UIModalService',
|
||||||
|
'UINotificationService',
|
||||||
|
'UIViewportDialogService',
|
||||||
|
'DisplaySetService',
|
||||||
|
'MeasurementService',
|
||||||
|
'ToolBarService',
|
||||||
|
'ViewportGridService',
|
||||||
|
'HangingProtocolService',
|
||||||
|
'IWebApiDataSource',
|
||||||
|
'DicomMetadataStore',
|
||||||
|
'pubSubServiceInterface'
|
||||||
].sort();
|
].sort();
|
||||||
|
|
||||||
const exports = Object.keys(OHIF).sort();
|
const exports = Object.keys(OHIF).sort();
|
||||||
|
|||||||
24
platform/core/src/measurements/tools/polygonRoi.js
Normal file
24
platform/core/src/measurements/tools/polygonRoi.js
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
const displayFunction = data => {
|
||||||
|
let meanValue = '';
|
||||||
|
const { cachedStats } = data;
|
||||||
|
if (cachedStats && cachedStats.mean && !isNaN(cachedStats.mean)) {
|
||||||
|
meanValue = cachedStats.mean.toFixed(2) + ' HU';
|
||||||
|
}
|
||||||
|
return meanValue;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const polygonRoi = {
|
||||||
|
id: 'PolygonRoi',
|
||||||
|
name: 'Polygon',
|
||||||
|
toolGroup: 'allTools',
|
||||||
|
cornerstoneToolType: 'FreehandRoiTool',
|
||||||
|
options: {
|
||||||
|
measurementTable: {
|
||||||
|
displayFunction,
|
||||||
|
},
|
||||||
|
caseProgress: {
|
||||||
|
include: true,
|
||||||
|
evaluate: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
@ -54,6 +54,18 @@ function _getInstance(StudyInstanceUID, SeriesInstanceUID, SOPInstanceUID) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function _getInstanceFromImageId(imageId) {
|
||||||
|
for (let study of _model.studies) {
|
||||||
|
for (let series of study.series) {
|
||||||
|
for (let instance of series.instances) {
|
||||||
|
if (instance.imageId === imageId) {
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const BaseImplementation = {
|
const BaseImplementation = {
|
||||||
EVENTS,
|
EVENTS,
|
||||||
listeners: {},
|
listeners: {},
|
||||||
@ -82,7 +94,7 @@ const BaseImplementation = {
|
|||||||
madeInClient,
|
madeInClient,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
addSeriesMetadata(seriesSummaryMetadata) {
|
addSeriesMetadata(seriesSummaryMetadata, madeInClient = false) {
|
||||||
const { StudyInstanceUID } = seriesSummaryMetadata[0];
|
const { StudyInstanceUID } = seriesSummaryMetadata[0];
|
||||||
let study = _getStudy(StudyInstanceUID);
|
let study = _getStudy(StudyInstanceUID);
|
||||||
if (!study) {
|
if (!study) {
|
||||||
@ -98,6 +110,7 @@ const BaseImplementation = {
|
|||||||
|
|
||||||
this._broadcastEvent(EVENTS.SERIES_ADDED, {
|
this._broadcastEvent(EVENTS.SERIES_ADDED, {
|
||||||
StudyInstanceUID,
|
StudyInstanceUID,
|
||||||
|
madeInClient,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
addStudy(study) {
|
addStudy(study) {
|
||||||
@ -124,6 +137,7 @@ const BaseImplementation = {
|
|||||||
getStudy: _getStudy,
|
getStudy: _getStudy,
|
||||||
getSeries: _getSeries,
|
getSeries: _getSeries,
|
||||||
getInstance: _getInstance,
|
getInstance: _getInstance,
|
||||||
|
getInstanceFromImageId: _getInstanceFromImageId,
|
||||||
};
|
};
|
||||||
|
|
||||||
const DicomMetadataStore = Object.assign(
|
const DicomMetadataStore = Object.assign(
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
const EVENTS = {
|
const EVENTS = {
|
||||||
|
STUDY_ADDED: 'event::dicomMetadataStore:studyAdded',
|
||||||
INSTANCES_ADDED: 'event::dicomMetadataStore:instancesAdded',
|
INSTANCES_ADDED: 'event::dicomMetadataStore:instancesAdded',
|
||||||
SERIES_ADDED: 'event::dicomMetadataStore:seriesAdded',
|
SERIES_ADDED: 'event::dicomMetadataStore:seriesAdded',
|
||||||
};
|
};
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import createSeriesMetadata from './createSeriesMetadata';
|
|||||||
function createStudyMetadata(StudyInstanceUID) {
|
function createStudyMetadata(StudyInstanceUID) {
|
||||||
return {
|
return {
|
||||||
StudyInstanceUID,
|
StudyInstanceUID,
|
||||||
|
isLoaded: false,
|
||||||
series: [],
|
series: [],
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
@ -24,7 +25,7 @@ function createStudyMetadata(StudyInstanceUID) {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
setSeriesMetadata: function (SeriesInstanceUID, seriesMetadata) {
|
setSeriesMetadata: function(SeriesInstanceUID, seriesMetadata) {
|
||||||
let existingSeries = this.series.find(
|
let existingSeries = this.series.find(
|
||||||
s => s.SeriesInstanceUID === SeriesInstanceUID
|
s => s.SeriesInstanceUID === SeriesInstanceUID
|
||||||
);
|
);
|
||||||
@ -34,7 +35,7 @@ function createStudyMetadata(StudyInstanceUID) {
|
|||||||
} else {
|
} else {
|
||||||
this.series.push(Object.assign({ instances: [] }, seriesMetadata));
|
this.series.push(Object.assign({ instances: [] }, seriesMetadata));
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -96,10 +96,13 @@ export default class DisplaySetService {
|
|||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param {*} input
|
* @param {*} input
|
||||||
* @param {*} param1
|
* @param {*} param1: settings: initialViewportSettings by HP or callbacks after rendering
|
||||||
* @returns {string[]} - added displaySetInstanceUIDs
|
* @returns {string[]} - added displaySetInstanceUIDs
|
||||||
*/
|
*/
|
||||||
makeDisplaySets = (input, { batch = false, madeInClient = false } = {}) => {
|
makeDisplaySets = (
|
||||||
|
input,
|
||||||
|
{ batch = false, madeInClient = false, settings = {} } = {}
|
||||||
|
) => {
|
||||||
if (!input || !input.length) {
|
if (!input || !input.length) {
|
||||||
throw new Error('No instances were provided.');
|
throw new Error('No instances were provided.');
|
||||||
}
|
}
|
||||||
@ -116,12 +119,15 @@ export default class DisplaySetService {
|
|||||||
if (batch) {
|
if (batch) {
|
||||||
for (let i = 0; i < input.length; i++) {
|
for (let i = 0; i < input.length; i++) {
|
||||||
const instances = input[i];
|
const instances = input[i];
|
||||||
const displaySets = this.makeDisplaySetForInstances(instances);
|
const displaySets = this.makeDisplaySetForInstances(
|
||||||
|
instances,
|
||||||
|
settings
|
||||||
|
);
|
||||||
|
|
||||||
displaySetsAdded = [...displaySetsAdded, displaySets];
|
displaySetsAdded = [...displaySetsAdded, displaySets];
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const displaySets = this.makeDisplaySetForInstances(input);
|
const displaySets = this.makeDisplaySetForInstances(input, settings);
|
||||||
|
|
||||||
displaySetsAdded = displaySets;
|
displaySetsAdded = displaySets;
|
||||||
}
|
}
|
||||||
@ -145,7 +151,7 @@ export default class DisplaySetService {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
makeDisplaySetForInstances(instances) {
|
makeDisplaySetForInstances(instances, settings) {
|
||||||
const instance = instances[0];
|
const instance = instances[0];
|
||||||
|
|
||||||
const existingDisplaySets =
|
const existingDisplaySets =
|
||||||
@ -168,6 +174,13 @@ export default class DisplaySetService {
|
|||||||
} else {
|
} else {
|
||||||
displaySets = handler.getDisplaySetsFromSeries(instances);
|
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._addDisplaySetsToCache(displaySets);
|
||||||
this._addActiveDisplaySets(displaySets);
|
this._addActiveDisplaySets(displaySets);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,96 @@
|
|||||||
|
import validate from './lib/validator';
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Match a Metadata instance against rules using Validate.js for validation.
|
||||||
|
* @param {InstanceMetadata} metadataInstance Metadata instance object
|
||||||
|
* @param {Array} rules Array of MatchingRules instances (StudyMatchingRule|SeriesMatchingRule|ImageMatchingRule) for the match
|
||||||
|
* @return {Object} Matching Object with score and details (which rule passed or failed)
|
||||||
|
*/
|
||||||
|
const match = (metadataInstance, rules, customAttributeRetrievalCallbacks) => {
|
||||||
|
const options = {
|
||||||
|
format: 'grouped',
|
||||||
|
};
|
||||||
|
|
||||||
|
const details = {
|
||||||
|
passed: [],
|
||||||
|
failed: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
let requiredFailed = false;
|
||||||
|
let score = 0;
|
||||||
|
|
||||||
|
rules.forEach(rule => {
|
||||||
|
const attribute = rule.attribute;
|
||||||
|
|
||||||
|
// Do not use the custom attribute from the metadataInstance since it is subject to change
|
||||||
|
if (customAttributeRetrievalCallbacks.hasOwnProperty(attribute)) {
|
||||||
|
const customAttribute = customAttributeRetrievalCallbacks[attribute];
|
||||||
|
metadataInstance[attribute] = customAttribute.callback(metadataInstance);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format the constraint as required by Validate.js
|
||||||
|
const testConstraint = {
|
||||||
|
[attribute]: rule.constraint,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create a single attribute object to be validated, since metadataInstance is an
|
||||||
|
// instance of Metadata (StudyMetadata, SeriesMetadata or InstanceMetadata)
|
||||||
|
const attributeValue = metadataInstance[attribute];
|
||||||
|
const attributeMap = {
|
||||||
|
[attribute]: attributeValue,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Use Validate.js to evaluate the constraints on the specified metadataInstance
|
||||||
|
let errorMessages;
|
||||||
|
try {
|
||||||
|
errorMessages = validate(attributeMap, testConstraint, [options]);
|
||||||
|
} catch (e) {
|
||||||
|
errorMessages = ['Something went wrong during validation.', e];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!errorMessages) {
|
||||||
|
// If no errorMessages were returned, then validation passed.
|
||||||
|
|
||||||
|
// Add the rule's weight to the total score
|
||||||
|
score += parseInt(rule.weight, 10);
|
||||||
|
|
||||||
|
// Log that this rule passed in the matching details object
|
||||||
|
details.passed.push({
|
||||||
|
rule,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// If errorMessages were present, then validation failed
|
||||||
|
|
||||||
|
// If the rule that failed validation was Required, then
|
||||||
|
// mark that a required Rule has failed
|
||||||
|
if (rule.required) {
|
||||||
|
requiredFailed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log that this rule failed in the matching details object
|
||||||
|
// and include any error messages
|
||||||
|
details.failed.push({
|
||||||
|
rule,
|
||||||
|
errorMessages,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// If a required Rule has failed Validation, set the matching score to zero
|
||||||
|
if (requiredFailed) {
|
||||||
|
score = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
score,
|
||||||
|
details,
|
||||||
|
requiredFailed,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const HPMatcher = {
|
||||||
|
match,
|
||||||
|
};
|
||||||
|
|
||||||
|
export { HPMatcher };
|
||||||
@ -1,74 +1,476 @@
|
|||||||
const name = 'HangingProtocolService';
|
import { cloneDeep } from 'lodash';
|
||||||
|
import pubSubServiceInterface from '../_shared/pubSubServiceInterface';
|
||||||
|
import sortBy from '../../utils/sortBy.js';
|
||||||
|
import ProtocolEngine from './ProtocolEngine';
|
||||||
|
|
||||||
const publicAPI = {
|
const EVENTS = {
|
||||||
name,
|
STAGE_CHANGE: 'event::hanging_protocol_stage_change',
|
||||||
getState: _getState,
|
NEW_LAYOUT: 'event::hanging_protocol_new_layout',
|
||||||
setHangingProtocol: _setHangingProtocol,
|
|
||||||
setHangingProtocolAppliedForViewport: _setHangingProtocolAppliedForViewport,
|
|
||||||
setServiceImplementation,
|
|
||||||
reset: _reset,
|
|
||||||
set: _set,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const serviceImplementation = {
|
const VIEWPORT_SETTING_TYPES = {
|
||||||
_getState: () => console.warn('getState() NOT IMPLEMENTED'),
|
PROPS: 'props',
|
||||||
_setHangingProtocol: () =>
|
VIEWPORT: 'viewport',
|
||||||
console.warn('_setHangingProtocol() NOT IMPLEMENTED'),
|
|
||||||
_setHangingProtocolAppliedForViewport: () =>
|
|
||||||
console.warn('_setHangingProtocolAppliedForViewport() NOT IMPLEMENTED'),
|
|
||||||
_reset: () => console.warn('reset() NOT IMPLEMENTED'),
|
|
||||||
_set: () => console.warn('set() NOT IMPLEMENTED'),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
function _getState() {
|
class HangingProtocolService {
|
||||||
return serviceImplementation._getState();
|
constructor(commandsManager) {
|
||||||
}
|
this._commandsManager = commandsManager;
|
||||||
|
this.protocols = [];
|
||||||
function _setHangingProtocol(hangingProtocol) {
|
this.ProtocolEngine = undefined;
|
||||||
return serviceImplementation._setHangingProtocol(hangingProtocol);
|
this.protocol = undefined;
|
||||||
}
|
this.stage = undefined;
|
||||||
|
this.matchDetails = [];
|
||||||
function _setHangingProtocolAppliedForViewport(hpAlreadyApplied) {
|
this.hpAlreadyApplied = [];
|
||||||
return serviceImplementation._setHangingProtocolAppliedForViewport(
|
this.studies = [];
|
||||||
hpAlreadyApplied
|
this.customViewportSettings = [];
|
||||||
);
|
this.customAttributeRetrievalCallbacks = {};
|
||||||
}
|
this.listeners = {};
|
||||||
|
Object.defineProperty(this, 'EVENTS', {
|
||||||
function _set(state) {
|
value: EVENTS,
|
||||||
return serviceImplementation._set(state);
|
writable: false,
|
||||||
}
|
enumerable: true,
|
||||||
|
configurable: false,
|
||||||
function _reset() {
|
});
|
||||||
return serviceImplementation._reset({});
|
Object.assign(this, pubSubServiceInterface);
|
||||||
}
|
|
||||||
|
|
||||||
function setServiceImplementation({
|
|
||||||
getState: getStateImplementation,
|
|
||||||
setHangingProtocol: setHangingProtocolImplementation,
|
|
||||||
setHangingProtocolAppliedForViewport: setHangingProtocolAppliedForViewportImplementation,
|
|
||||||
reset: resetImplementation,
|
|
||||||
set: setImplementation,
|
|
||||||
}) {
|
|
||||||
if (getStateImplementation) {
|
|
||||||
serviceImplementation._getState = getStateImplementation;
|
|
||||||
}
|
}
|
||||||
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 {
|
export default HangingProtocolService;
|
||||||
name,
|
export { EVENTS };
|
||||||
create: ({ configuration = {} }) => {
|
|
||||||
return publicAPI;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|||||||
@ -0,0 +1,789 @@
|
|||||||
|
import { HPMatcher } from './HPMatcher.js';
|
||||||
|
import { sortByScore } from './lib/sortByScore';
|
||||||
|
|
||||||
|
const deafultProtocol = {
|
||||||
|
id: 'defaultProtocol',
|
||||||
|
locked: true,
|
||||||
|
hasUpdatedPriorsInformation: false,
|
||||||
|
name: 'Default',
|
||||||
|
createdDate: '2021-02-23T19:22:08.894Z',
|
||||||
|
modifiedDate: '2021-02-23T19:22:08.894Z',
|
||||||
|
availableTo: {},
|
||||||
|
editableBy: {},
|
||||||
|
protocolMatchingRules: [],
|
||||||
|
stages: [
|
||||||
|
{
|
||||||
|
id: 'nwzau7jDkEkL8djfr',
|
||||||
|
name: 'oneByOne',
|
||||||
|
viewportStructure: {
|
||||||
|
type: 'grid',
|
||||||
|
properties: {
|
||||||
|
rows: 1,
|
||||||
|
columns: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
viewports: [
|
||||||
|
{
|
||||||
|
viewportSettings: [],
|
||||||
|
imageMatchingRules: [],
|
||||||
|
seriesMatchingRules: [],
|
||||||
|
studyMatchingRules: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
createdDate: '2021-02-23T19:22:08.894Z',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
numberOfPriorsReferenced: -1,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default class ProtocolEngine {
|
||||||
|
constructor(protocols, customAttributeRetrievalCallbacks) {
|
||||||
|
this.protocols = protocols;
|
||||||
|
this.customAttributeRetrievalCallbacks = customAttributeRetrievalCallbacks;
|
||||||
|
this.matchedProtocols = new Map();
|
||||||
|
this.matchedProtocolScores = {};
|
||||||
|
this.study = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
run(studyMetaData) {
|
||||||
|
this.study = studyMetaData;
|
||||||
|
return this.getBestProtocolMatch();
|
||||||
|
}
|
||||||
|
|
||||||
|
// /**
|
||||||
|
// * Resets the ProtocolEngine to the best match
|
||||||
|
// */
|
||||||
|
// reset() {
|
||||||
|
// const protocol = this.getBestProtocolMatch();
|
||||||
|
|
||||||
|
// this.setHangingProtocol(protocol);
|
||||||
|
// }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the best matched Protocol to the current study or set of studies
|
||||||
|
* @returns {*}
|
||||||
|
*/
|
||||||
|
getBestProtocolMatch() {
|
||||||
|
// Run the matching to populate matchedProtocols Set and Map
|
||||||
|
this.updateProtocolMatches();
|
||||||
|
|
||||||
|
// Retrieve the highest scoring Protocol
|
||||||
|
const bestMatch = this._getHighestScoringProtocol();
|
||||||
|
|
||||||
|
console.log('ProtocolEngine::getBestProtocolMatch bestMatch', bestMatch);
|
||||||
|
|
||||||
|
return bestMatch;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Populates the MatchedProtocols Collection by running the matching procedure
|
||||||
|
*/
|
||||||
|
updateProtocolMatches() {
|
||||||
|
console.log('ProtocolEngine::updateProtocolMatches');
|
||||||
|
|
||||||
|
// Clear all data currently in matchedProtocols
|
||||||
|
this._clearMatchedProtocols();
|
||||||
|
|
||||||
|
// TODO: handle more than one study
|
||||||
|
const study = this.study;
|
||||||
|
const matched = this.findMatchByStudy(study);
|
||||||
|
|
||||||
|
// For each matched protocol, check if it is already in MatchedProtocols
|
||||||
|
matched.forEach(matchedDetail => {
|
||||||
|
const protocol = matchedDetail.protocol;
|
||||||
|
if (!protocol) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If it is not already in the MatchedProtocols Collection, insert it with its score
|
||||||
|
if (!this.matchedProtocols.has(protocol.id)) {
|
||||||
|
console.log(
|
||||||
|
'ProtocolEngine::updateProtocolMatches inserting protocol match',
|
||||||
|
matchedDetail
|
||||||
|
);
|
||||||
|
this.matchedProtocols.set(protocol.id, protocol);
|
||||||
|
this.matchedProtocolScores[protocol.id] = matchedDetail.score;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
findMatch(metaData, rules) {
|
||||||
|
return HPMatcher.match(
|
||||||
|
metaData,
|
||||||
|
rules,
|
||||||
|
this.customAttributeRetrievalCallbacks
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds the best protocols from Protocol Store, matching each protocol matching rules
|
||||||
|
* with the given study. The best protocol are orded by score and returned in an array
|
||||||
|
* @param {Object} study StudyMetadata instance object
|
||||||
|
* @return {Array} Array of match objects or an empty array if no match was found
|
||||||
|
* Each match object has the score of the matching and the matched
|
||||||
|
* protocol
|
||||||
|
*/
|
||||||
|
findMatchByStudy(study) {
|
||||||
|
const matched = [];
|
||||||
|
|
||||||
|
this.protocols.forEach(protocol => {
|
||||||
|
// Clone the protocol's protocolMatchingRules array
|
||||||
|
// We clone it so that we don't accidentally add the
|
||||||
|
// numberOfPriorsReferenced rule to the Protocol itself.
|
||||||
|
let rules = protocol.protocolMatchingRules.slice();
|
||||||
|
if (!rules) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run the matcher and get matching details
|
||||||
|
const matchedDetails = this.findMatch(study, rules);
|
||||||
|
const score = matchedDetails.score;
|
||||||
|
|
||||||
|
// The protocol matched some rule, add it to the matched list
|
||||||
|
if (score > 0) {
|
||||||
|
matched.push({
|
||||||
|
score,
|
||||||
|
protocol,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// If no matches were found, select the default protocol
|
||||||
|
if (!matched.length) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
score: 1,
|
||||||
|
protocol: deafultProtocol,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort the matched list by score
|
||||||
|
sortByScore(matched);
|
||||||
|
|
||||||
|
console.log('ProtocolEngine::findMatchByStudy matched', matched);
|
||||||
|
|
||||||
|
return matched;
|
||||||
|
}
|
||||||
|
|
||||||
|
_clearMatchedProtocols() {
|
||||||
|
this.matchedProtocols.clear();
|
||||||
|
this.matchedProtocolScores = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
_largestKeyByValue(obj) {
|
||||||
|
return Object.keys(obj).reduce((a, b) => (obj[a] > obj[b] ? a : b));
|
||||||
|
}
|
||||||
|
|
||||||
|
_getHighestScoringProtocol() {
|
||||||
|
if (!Object.keys(this.matchedProtocolScores).length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const highestScoringProtocolId = this._largestKeyByValue(
|
||||||
|
this.matchedProtocolScores
|
||||||
|
);
|
||||||
|
return this.matchedProtocols.get(highestScoringProtocolId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resets the ProtocolEngine to the best match
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the current Stage from the current Protocol and stage index
|
||||||
|
*
|
||||||
|
* @returns {*} The Stage model for the currently displayed Stage
|
||||||
|
*/
|
||||||
|
// getCurrentStageModel() {
|
||||||
|
// return this.protocol.stages[this.stage];
|
||||||
|
// }
|
||||||
|
|
||||||
|
// /**
|
||||||
|
// * Get the number of prior studies supplied in the priorStudies map property.
|
||||||
|
// *
|
||||||
|
// * @param {String} studyObjectID The study object ID of the study whose priors are needed
|
||||||
|
// * @returns {number} The number of available prior studies with the same PatientID
|
||||||
|
// */
|
||||||
|
// getNumberOfAvailablePriors(studyObjectID) {
|
||||||
|
// return this.getAvailableStudyPriors(studyObjectID).length;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// /**
|
||||||
|
// * Get the array of prior studies from a specific study.
|
||||||
|
// *
|
||||||
|
// * @param {String} studyObjectID The study object ID of the study whose priors are needed
|
||||||
|
// * @returns {Array} The array of available priors or an empty array
|
||||||
|
// */
|
||||||
|
// getAvailableStudyPriors(studyObjectID) {
|
||||||
|
// const priors = this.priorStudies.get(studyObjectID);
|
||||||
|
|
||||||
|
// return priors instanceof Array ? priors : [];
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // Match images given a list of Studies and a Viewport's image matching reqs
|
||||||
|
// matchImages(viewport, viewportIndex) {
|
||||||
|
// log.trace('ProtocolEngine::matchImages');
|
||||||
|
|
||||||
|
// const {
|
||||||
|
// studyMatchingRules,
|
||||||
|
// seriesMatchingRules,
|
||||||
|
// imageMatchingRules: instanceMatchingRules,
|
||||||
|
// } = viewport;
|
||||||
|
|
||||||
|
// const matchingScores = [];
|
||||||
|
// const currentStudy = this.studies[0]; // @TODO: Should this be: this.studies[this.currentStudy] ???
|
||||||
|
// const firstInstance = currentStudy.getFirstInstance();
|
||||||
|
|
||||||
|
// let highestStudyMatchingScore = 0;
|
||||||
|
// let highestSeriesMatchingScore = 0;
|
||||||
|
|
||||||
|
// // Set custom attribute for study metadata and it's first instance
|
||||||
|
// currentStudy.setCustomAttribute(ABSTRACT_PRIOR_VALUE, 0);
|
||||||
|
// if (firstInstance instanceof InstanceMetadata) {
|
||||||
|
// firstInstance.setCustomAttribute(ABSTRACT_PRIOR_VALUE, 0);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // Only used if study matching rules has abstract prior values defined...
|
||||||
|
// let priorStudies;
|
||||||
|
|
||||||
|
// studyMatchingRules.forEach(rule => {
|
||||||
|
// if (rule.attribute === ABSTRACT_PRIOR_VALUE) {
|
||||||
|
// const validatorType = Object.keys(rule.constraint)[0];
|
||||||
|
// const validator = Object.keys(rule.constraint[validatorType])[0];
|
||||||
|
|
||||||
|
// let abstractPriorValue = rule.constraint[validatorType][validator];
|
||||||
|
// abstractPriorValue = parseInt(abstractPriorValue, 10);
|
||||||
|
// // TODO: Restrict or clarify validators for abstractPriorValue?
|
||||||
|
|
||||||
|
// // No need to call it more than once...
|
||||||
|
// if (!priorStudies) {
|
||||||
|
// priorStudies = this.getAvailableStudyPriors(
|
||||||
|
// currentStudy.getObjectID()
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // TODO: Revisit this later: What about two studies with the same
|
||||||
|
// // study date?
|
||||||
|
|
||||||
|
// let priorStudy;
|
||||||
|
// if (abstractPriorValue === -1) {
|
||||||
|
// priorStudy = priorStudies[priorStudies.length - 1];
|
||||||
|
// } else {
|
||||||
|
// const studyIndex = Math.max(abstractPriorValue - 1, 0);
|
||||||
|
// priorStudy = priorStudies[studyIndex];
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // Invalid data
|
||||||
|
// if (!priorStudy instanceof StudyMetadata) {
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// const priorStudyObjectID = priorStudy.getObjectID();
|
||||||
|
|
||||||
|
// // Check if study metadata is already in studies list
|
||||||
|
// if (
|
||||||
|
// this.studies.find(study => study.getObjectID() === priorStudyObjectID)
|
||||||
|
// ) {
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // Get study metadata if necessary and load study in the viewer (each viewer should provide it's own load study method)
|
||||||
|
// this.studyMetadataSource.loadStudy(priorStudy).then(
|
||||||
|
// studyMetadata => {
|
||||||
|
// // Set the custom attribute abstractPriorValue for the study metadata
|
||||||
|
// studyMetadata.setCustomAttribute(
|
||||||
|
// ABSTRACT_PRIOR_VALUE,
|
||||||
|
// abstractPriorValue
|
||||||
|
// );
|
||||||
|
|
||||||
|
// // Also add custom attribute
|
||||||
|
// const firstInstance = studyMetadata.getFirstInstance();
|
||||||
|
// if (firstInstance instanceof InstanceMetadata) {
|
||||||
|
// firstInstance.setCustomAttribute(
|
||||||
|
// ABSTRACT_PRIOR_VALUE,
|
||||||
|
// abstractPriorValue
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // Insert the new study metadata
|
||||||
|
// this.studies.push(studyMetadata);
|
||||||
|
|
||||||
|
// // Update the viewport to refresh layout manager with new study
|
||||||
|
// this.updateViewports(viewportIndex);
|
||||||
|
// },
|
||||||
|
// error => {
|
||||||
|
// log.warn(error);
|
||||||
|
// throw new OHIFError(
|
||||||
|
// `ProtocolEngine::matchImages could not get study metadata for the Study with the following ObjectID: ${priorStudyObjectID}`
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
// // TODO: Add relative Date / time
|
||||||
|
// });
|
||||||
|
|
||||||
|
// this.studies.forEach(study => {
|
||||||
|
// const studyMatchDetails = HPMatcher.match(
|
||||||
|
// study.getFirstInstance(),
|
||||||
|
// studyMatchingRules
|
||||||
|
// );
|
||||||
|
|
||||||
|
// // Prevent bestMatch from being updated if the matchDetails' required attribute check has failed
|
||||||
|
// if (
|
||||||
|
// studyMatchDetails.requiredFailed === true ||
|
||||||
|
// studyMatchDetails.score < highestStudyMatchingScore
|
||||||
|
// ) {
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// highestStudyMatchingScore = studyMatchDetails.score;
|
||||||
|
|
||||||
|
// study.forEachSeries(series => {
|
||||||
|
// const seriesMatchDetails = HPMatcher.match(
|
||||||
|
// series.getFirstInstance(),
|
||||||
|
// seriesMatchingRules
|
||||||
|
// );
|
||||||
|
|
||||||
|
// // Prevent bestMatch from being updated if the matchDetails' required attribute check has failed
|
||||||
|
// if (
|
||||||
|
// seriesMatchDetails.requiredFailed === true ||
|
||||||
|
// seriesMatchDetails.score < highestSeriesMatchingScore
|
||||||
|
// ) {
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// highestSeriesMatchingScore = seriesMatchDetails.score;
|
||||||
|
|
||||||
|
// series.forEachInstance((instance, index) => {
|
||||||
|
// // This tests to make sure there is actually image data in this instance
|
||||||
|
// // TODO: Change this when we add PDF and MPEG support
|
||||||
|
// // See https://ohiforg.atlassian.net/browse/LT-227
|
||||||
|
// if (
|
||||||
|
// !isImage(instance.getTagValue('SOPClassUID')) &&
|
||||||
|
// !instance.getTagValue('Rows')
|
||||||
|
// ) {
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// const instanceMatchDetails = HPMatcher.match(
|
||||||
|
// instance,
|
||||||
|
// instanceMatchingRules
|
||||||
|
// );
|
||||||
|
|
||||||
|
// // Prevent bestMatch from being updated if the matchDetails' required attribute check has failed
|
||||||
|
// if (instanceMatchDetails.requiredFailed === true) {
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// const matchDetails = {
|
||||||
|
// passed: [],
|
||||||
|
// failed: [],
|
||||||
|
// };
|
||||||
|
|
||||||
|
// matchDetails.passed = matchDetails.passed.concat(
|
||||||
|
// instanceMatchDetails.details.passed
|
||||||
|
// );
|
||||||
|
// matchDetails.passed = matchDetails.passed.concat(
|
||||||
|
// seriesMatchDetails.details.passed
|
||||||
|
// );
|
||||||
|
// matchDetails.passed = matchDetails.passed.concat(
|
||||||
|
// studyMatchDetails.details.passed
|
||||||
|
// );
|
||||||
|
|
||||||
|
// matchDetails.failed = matchDetails.failed.concat(
|
||||||
|
// instanceMatchDetails.details.failed
|
||||||
|
// );
|
||||||
|
// matchDetails.failed = matchDetails.failed.concat(
|
||||||
|
// seriesMatchDetails.details.failed
|
||||||
|
// );
|
||||||
|
// matchDetails.failed = matchDetails.failed.concat(
|
||||||
|
// studyMatchDetails.details.failed
|
||||||
|
// );
|
||||||
|
|
||||||
|
// const totalMatchScore =
|
||||||
|
// instanceMatchDetails.score +
|
||||||
|
// seriesMatchDetails.score +
|
||||||
|
// studyMatchDetails.score;
|
||||||
|
// const currentSOPInstanceUID = instance.getSOPInstanceUID();
|
||||||
|
|
||||||
|
// const imageDetails = {
|
||||||
|
// StudyInstanceUID: study.getStudyInstanceUID(),
|
||||||
|
// SeriesInstanceUID: series.getSeriesInstanceUID(),
|
||||||
|
// SOPInstanceUID: currentSOPInstanceUID,
|
||||||
|
// currentImageIdIndex: index,
|
||||||
|
// matchingScore: totalMatchScore,
|
||||||
|
// matchDetails: matchDetails,
|
||||||
|
// sortingInfo: {
|
||||||
|
// score: totalMatchScore,
|
||||||
|
// study:
|
||||||
|
// instance.getTagValue('StudyDate') +
|
||||||
|
// instance.getTagValue('StudyTime'),
|
||||||
|
// series: parseInt(instance.getTagValue('SeriesNumber')), // TODO: change for seriesDateTime
|
||||||
|
// instance: parseInt(instance.getTagValue('InstanceNumber')), // TODO: change for acquisitionTime
|
||||||
|
// },
|
||||||
|
// };
|
||||||
|
|
||||||
|
// // Find the displaySet
|
||||||
|
// const displaySet = study.findDisplaySet(displaySet =>
|
||||||
|
// displaySet.images.find(
|
||||||
|
// image => image.getSOPInstanceUID() === currentSOPInstanceUID
|
||||||
|
// )
|
||||||
|
// );
|
||||||
|
|
||||||
|
// // If the instance was found, set the displaySet ID
|
||||||
|
// if (displaySet) {
|
||||||
|
// imageDetails.displaySetInstanceUID = displaySet.getUID();
|
||||||
|
// imageDetails.imageId = instance.getImageId();
|
||||||
|
// }
|
||||||
|
|
||||||
|
// matchingScores.push(imageDetails);
|
||||||
|
// });
|
||||||
|
// });
|
||||||
|
// });
|
||||||
|
|
||||||
|
// // Sort the matchingScores
|
||||||
|
// const sortingFunction = sortBy(
|
||||||
|
// {
|
||||||
|
// name: 'score',
|
||||||
|
// reverse: true,
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// name: 'study',
|
||||||
|
// reverse: true,
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// name: 'instance',
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// name: 'series',
|
||||||
|
// }
|
||||||
|
// );
|
||||||
|
// matchingScores.sort((a, b) =>
|
||||||
|
// sortingFunction(a.sortingInfo, b.sortingInfo)
|
||||||
|
// );
|
||||||
|
|
||||||
|
// const bestMatch = matchingScores[0];
|
||||||
|
|
||||||
|
// log.trace('ProtocolEngine::matchImages bestMatch', bestMatch);
|
||||||
|
|
||||||
|
// return {
|
||||||
|
// bestMatch,
|
||||||
|
// matchingScores,
|
||||||
|
// };
|
||||||
|
// }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the current layout
|
||||||
|
*
|
||||||
|
* @param {number} numRows
|
||||||
|
* @param {number} numColumns
|
||||||
|
*/
|
||||||
|
// setLayout(numRows, numColumns) {
|
||||||
|
// if (numRows < 1 && numColumns < 1) {
|
||||||
|
// log.error(`Invalid layout ${numRows} x ${numColumns}`);
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if (typeof this.options.setLayout !== 'function') {
|
||||||
|
// log.error('Hanging Protocol Engine setLayout callback is not defined');
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// let viewports = [];
|
||||||
|
// const numViewports = numRows * numColumns;
|
||||||
|
|
||||||
|
// for (let i = 0; i < numViewports; i++) {
|
||||||
|
// viewports.push({});
|
||||||
|
// }
|
||||||
|
|
||||||
|
// this.options.setLayout({ numRows, numColumns, viewports });
|
||||||
|
// }
|
||||||
|
|
||||||
|
// /**
|
||||||
|
// * Rerenders viewports that are part of the current layout manager
|
||||||
|
// * using the matching rules internal to each viewport.
|
||||||
|
// *
|
||||||
|
// * If this function is provided the index of a viewport, only the specified viewport
|
||||||
|
// * is rerendered.
|
||||||
|
// *
|
||||||
|
// * @param viewportIndex
|
||||||
|
// */
|
||||||
|
// updateViewports(viewportIndex) {
|
||||||
|
// log.trace(
|
||||||
|
// `ProtocolEngine::updateViewports viewportIndex: ${viewportIndex}`
|
||||||
|
// );
|
||||||
|
|
||||||
|
// // Make sure we have an active protocol with a non-empty array of display sets
|
||||||
|
// if (!this.getNumProtocolStages()) {
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // Retrieve the current stage
|
||||||
|
// const stageModel = this.getCurrentStageModel();
|
||||||
|
|
||||||
|
// // If the current stage does not fulfill the requirements to be displayed,
|
||||||
|
// // stop here.
|
||||||
|
// if (
|
||||||
|
// !stageModel ||
|
||||||
|
// !stageModel.viewportStructure ||
|
||||||
|
// !stageModel.viewports ||
|
||||||
|
// !stageModel.viewports.length
|
||||||
|
// ) {
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // Retrieve the layoutTemplate associated with the current display set's viewport structure
|
||||||
|
// // If no such template name exists, stop here.
|
||||||
|
// const layoutTemplateName = stageModel.viewportStructure.getLayoutTemplateName();
|
||||||
|
// if (!layoutTemplateName) {
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // Retrieve the properties associated with the current display set's viewport structure template
|
||||||
|
// // If no such layout properties exist, stop here.
|
||||||
|
// const layoutProps = stageModel.viewportStructure.properties;
|
||||||
|
// if (!layoutProps) {
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // Create an empty array to store the output viewportData
|
||||||
|
// const viewportData = [];
|
||||||
|
|
||||||
|
// // Empty the matchDetails associated with the ProtocolEngine.
|
||||||
|
// // This will be used to store the pass/fail details and score
|
||||||
|
// // for each of the viewport matching procedures
|
||||||
|
// this.matchDetails = [];
|
||||||
|
|
||||||
|
// // Loop through each viewport
|
||||||
|
// stageModel.viewports.forEach((viewport, viewportIndex) => {
|
||||||
|
// const details = this.matchImages(viewport, viewportIndex);
|
||||||
|
|
||||||
|
// this.matchDetails[viewportIndex] = details;
|
||||||
|
|
||||||
|
// // Convert any YES/NO values into true/false for Cornerstone
|
||||||
|
// const cornerstoneViewportParams = {};
|
||||||
|
|
||||||
|
// // Cache viewportSettings keys
|
||||||
|
// const viewportSettingsKeys = Object.keys(viewport.viewportSettings);
|
||||||
|
|
||||||
|
// viewportSettingsKeys.forEach(key => {
|
||||||
|
// let value = viewport.viewportSettings[key];
|
||||||
|
// if (value === 'YES') {
|
||||||
|
// value = true;
|
||||||
|
// } else if (value === 'NO') {
|
||||||
|
// value = false;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// cornerstoneViewportParams[key] = value;
|
||||||
|
// });
|
||||||
|
|
||||||
|
// // imageViewerViewports occasionally needs relevant layout data in order to set
|
||||||
|
// // the element style of the viewport in question
|
||||||
|
// const currentViewportData = {
|
||||||
|
// viewportIndex,
|
||||||
|
// viewport: cornerstoneViewportParams,
|
||||||
|
// ...layoutProps,
|
||||||
|
// };
|
||||||
|
|
||||||
|
// const customSettings = [];
|
||||||
|
// viewportSettingsKeys.forEach(id => {
|
||||||
|
// const setting = CustomViewportSettings[id];
|
||||||
|
// if (!setting) {
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// customSettings.push({
|
||||||
|
// id: id,
|
||||||
|
// value: viewport.viewportSettings[id],
|
||||||
|
// });
|
||||||
|
// });
|
||||||
|
|
||||||
|
// currentViewportData.renderedCallback = element => {
|
||||||
|
// //console.log('renderedCallback for ' + element.id);
|
||||||
|
// customSettings.forEach(customSetting => {
|
||||||
|
// log.trace(
|
||||||
|
// `ProtocolEngine::currentViewportData.renderedCallback Applying custom setting: ${customSetting.id}`
|
||||||
|
// );
|
||||||
|
// log.trace(
|
||||||
|
// `ProtocolEngine::currentViewportData.renderedCallback with value: ${customSetting.value}`
|
||||||
|
// );
|
||||||
|
|
||||||
|
// const setting = CustomViewportSettings[customSetting.id];
|
||||||
|
// setting.callback(element, customSetting.value);
|
||||||
|
// });
|
||||||
|
// };
|
||||||
|
|
||||||
|
// let currentMatch = details.bestMatch;
|
||||||
|
// let currentPosition = 1;
|
||||||
|
// const scoresLength = details.matchingScores.length;
|
||||||
|
// while (
|
||||||
|
// currentPosition < scoresLength &&
|
||||||
|
// viewportData.find(a => a.imageId === currentMatch.imageId)
|
||||||
|
// ) {
|
||||||
|
// currentMatch = details.matchingScores[currentPosition];
|
||||||
|
// currentPosition++;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if (currentMatch && currentMatch.imageId) {
|
||||||
|
// currentViewportData.StudyInstanceUID = currentMatch.StudyInstanceUID;
|
||||||
|
// currentViewportData.SeriesInstanceUID = currentMatch.SeriesInstanceUID;
|
||||||
|
// currentViewportData.SOPInstanceUID = currentMatch.SOPInstanceUID;
|
||||||
|
// currentViewportData.currentImageIdIndex =
|
||||||
|
// currentMatch.currentImageIdIndex;
|
||||||
|
// currentViewportData.displaySetInstanceUID =
|
||||||
|
// currentMatch.displaySetInstanceUID;
|
||||||
|
// currentViewportData.imageId = currentMatch.imageId;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // @TODO Why should we throw an exception when a best match is not found? This was aborting the whole process.
|
||||||
|
// // if (!currentViewportData.displaySetInstanceUID) {
|
||||||
|
// // throw new OHIFError('ProtocolEngine::updateViewports No matching display set found?');
|
||||||
|
// // }
|
||||||
|
|
||||||
|
// viewportData.push(currentViewportData);
|
||||||
|
// });
|
||||||
|
|
||||||
|
// this.setLayout(layoutProps.Rows, layoutProps.Columns);
|
||||||
|
|
||||||
|
// if (typeof this.options.setViewportSpecificData !== 'function') {
|
||||||
|
// log.error(
|
||||||
|
// 'Hanging Protocol Engine setViewportSpecificData callback is not defined'
|
||||||
|
// );
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // If viewportIndex is defined, then update only that viewport
|
||||||
|
// if (viewportIndex !== undefined && viewportData[viewportIndex]) {
|
||||||
|
// this.options.setViewportSpecificData(
|
||||||
|
// viewportIndex,
|
||||||
|
// viewportData[viewportIndex]
|
||||||
|
// );
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // Update all viewports
|
||||||
|
// viewportData.forEach(viewportSpecificData => {
|
||||||
|
// this.options.setViewportSpecificData(
|
||||||
|
// viewportSpecificData.viewportIndex,
|
||||||
|
// viewportSpecificData
|
||||||
|
// );
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
|
||||||
|
// /**
|
||||||
|
// * Sets the current Hanging Protocol to the specified Protocol
|
||||||
|
// * An optional argument can also be used to prevent the updating of the Viewports
|
||||||
|
// *
|
||||||
|
// * @param newProtocol
|
||||||
|
// * @param updateViewports
|
||||||
|
// */
|
||||||
|
// setHangingProtocol(newProtocol, updateViewports = true) {
|
||||||
|
// log.trace('ProtocolEngine::setHangingProtocol newProtocol', newProtocol);
|
||||||
|
// log.trace(
|
||||||
|
// `ProtocolEngine::setHangingProtocol updateViewports = ${updateViewports}`
|
||||||
|
// );
|
||||||
|
|
||||||
|
// // Reset the array of newStageIds
|
||||||
|
// this.newStageIds = [];
|
||||||
|
|
||||||
|
// if (Protocol.prototype.isPrototypeOf(newProtocol)) {
|
||||||
|
// this.protocol = newProtocol;
|
||||||
|
// } else {
|
||||||
|
// this.protocol = new Protocol();
|
||||||
|
// this.protocol.fromObject(newProtocol);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// this.stage = 0;
|
||||||
|
|
||||||
|
// // Update viewports by default
|
||||||
|
// if (updateViewports) {
|
||||||
|
// this.updateViewports();
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// /**
|
||||||
|
// * Check if the next stage is available
|
||||||
|
// * @return {Boolean} True if next stage is available or false otherwise
|
||||||
|
// */
|
||||||
|
// isNextStageAvailable() {
|
||||||
|
// const numberOfStages = this.getNumProtocolStages();
|
||||||
|
|
||||||
|
// return this.stage + 1 < numberOfStages;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// /**
|
||||||
|
// * Check if the previous stage is available
|
||||||
|
// * @return {Boolean} True if previous stage is available or false otherwise
|
||||||
|
// */
|
||||||
|
// isPreviousStageAvailable() {
|
||||||
|
// return this.stage - 1 >= 0;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// /**
|
||||||
|
// * Changes the current stage to a new stage index in the display set sequence.
|
||||||
|
// * It checks if the next stage exists.
|
||||||
|
// *
|
||||||
|
// * @param {Integer} stageAction An integer value specifying wheater next (1) or previous (-1) stage
|
||||||
|
// * @return {Boolean} True if new stage has set or false, otherwise
|
||||||
|
// */
|
||||||
|
// setCurrentProtocolStage(stageAction) {
|
||||||
|
// // Check if previous or next stage is available
|
||||||
|
// if (stageAction === -1 && !this.isPreviousStageAvailable()) {
|
||||||
|
// return false;
|
||||||
|
// } else if (stageAction === 1 && !this.isNextStageAvailable()) {
|
||||||
|
// return false;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // Sets the new stage
|
||||||
|
// this.stage += stageAction;
|
||||||
|
|
||||||
|
// // Log the new stage
|
||||||
|
// log.trace(`ProtocolEngine::setCurrentProtocolStage stage = ${this.stage}`);
|
||||||
|
|
||||||
|
// // Since stage has changed, we need to update the viewports
|
||||||
|
// // and redo matchings
|
||||||
|
// this.updateViewports();
|
||||||
|
|
||||||
|
// // Everything went well
|
||||||
|
// return true;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// /**
|
||||||
|
// * Retrieves the number of Stages in the current Protocol or
|
||||||
|
// * undefined if no protocol or stages are set
|
||||||
|
// */
|
||||||
|
// getNumProtocolStages() {
|
||||||
|
// if (
|
||||||
|
// !this.protocol ||
|
||||||
|
// !this.protocol.stages ||
|
||||||
|
// !this.protocol.stages.length
|
||||||
|
// ) {
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// return this.protocol.stages.length;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// /**
|
||||||
|
// * Switches to the next protocol stage in the display set sequence
|
||||||
|
// */
|
||||||
|
// nextProtocolStage() {
|
||||||
|
// log.trace('ProtocolEngine::nextProtocolStage');
|
||||||
|
|
||||||
|
// if (!this.setCurrentProtocolStage(1)) {
|
||||||
|
// log.trace('ProtocolEngine::nextProtocolStage failed');
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// /**
|
||||||
|
// * Switches to the previous protocol stage in the display set sequence
|
||||||
|
// */
|
||||||
|
// previousProtocolStage() {
|
||||||
|
// log.trace('ProtocolEngine::previousProtocolStage');
|
||||||
|
|
||||||
|
// if (!this.setCurrentProtocolStage(-1)) {
|
||||||
|
// log.trace('ProtocolEngine::previousProtocolStage failed');
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
@ -1,3 +1,8 @@
|
|||||||
import HangingProtocolService from './HangingProtocolService';
|
import HangingProtocolService from './HangingProtocolService';
|
||||||
|
|
||||||
export default HangingProtocolService;
|
export default {
|
||||||
|
name: 'HangingProtocolService',
|
||||||
|
create: ({ configuration = {}, commandsManager }) => {
|
||||||
|
return new HangingProtocolService(commandsManager);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|||||||
@ -0,0 +1,98 @@
|
|||||||
|
const comparators = [
|
||||||
|
{
|
||||||
|
id: 'equals',
|
||||||
|
name: '= (Equals)',
|
||||||
|
validator: 'equals',
|
||||||
|
validatorOption: 'value',
|
||||||
|
description: 'The attribute must equal this value.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'doesNotEqual',
|
||||||
|
name: '!= (Does not equal)',
|
||||||
|
validator: 'doesNotEqual',
|
||||||
|
validatorOption: 'value',
|
||||||
|
description: 'The attribute must not equal this value.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'contains',
|
||||||
|
name: 'Contains',
|
||||||
|
validator: 'contains',
|
||||||
|
validatorOption: 'value',
|
||||||
|
description: 'The attribute must contain this value.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'doesNotContain',
|
||||||
|
name: 'Does not contain',
|
||||||
|
validator: 'doesNotContain',
|
||||||
|
validatorOption: 'value',
|
||||||
|
description: 'The attribute must not contain this value.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'startsWith',
|
||||||
|
name: 'Starts with',
|
||||||
|
validator: 'startsWith',
|
||||||
|
validatorOption: 'value',
|
||||||
|
description: 'The attribute must start with this value.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'endsWith',
|
||||||
|
name: 'Ends with',
|
||||||
|
validator: 'endsWith',
|
||||||
|
validatorOption: 'value',
|
||||||
|
description: 'The attribute must end with this value.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'onlyInteger',
|
||||||
|
name: 'Only Integers',
|
||||||
|
validator: 'numericality',
|
||||||
|
validatorOption: 'onlyInteger',
|
||||||
|
description: "Real numbers won't be allowed.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'greaterThan',
|
||||||
|
name: '> (Greater than)',
|
||||||
|
validator: 'numericality',
|
||||||
|
validatorOption: 'greaterThan',
|
||||||
|
description: 'The attribute has to be greater than this value.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'greaterThanOrEqualTo',
|
||||||
|
name: '>= (Greater than or equal to)',
|
||||||
|
validator: 'numericality',
|
||||||
|
validatorOption: 'greaterThanOrEqualTo',
|
||||||
|
description: 'The attribute has to be at least this value.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'lessThanOrEqualTo',
|
||||||
|
name: '<= (Less than or equal to)',
|
||||||
|
validator: 'numericality',
|
||||||
|
validatorOption: 'lessThanOrEqualTo',
|
||||||
|
description: 'The attribute can be this value at the most.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'lessThan',
|
||||||
|
name: '< (Less than)',
|
||||||
|
validator: 'numericality',
|
||||||
|
validatorOption: 'lessThan',
|
||||||
|
description: 'The attribute has to be less than this value.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'odd',
|
||||||
|
name: 'Odd',
|
||||||
|
validator: 'numericality',
|
||||||
|
validatorOption: 'odd',
|
||||||
|
description: 'The attribute has to be odd.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'even',
|
||||||
|
name: 'Even',
|
||||||
|
validator: 'numericality',
|
||||||
|
validatorOption: 'even',
|
||||||
|
description: 'The attribute has to be even.',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// Immutable object
|
||||||
|
Object.freeze(comparators);
|
||||||
|
|
||||||
|
export { comparators };
|
||||||
@ -0,0 +1,71 @@
|
|||||||
|
const attributeCache = Object.create(null);
|
||||||
|
const REGEXP = /^\([x0-9a-f]+\)/;
|
||||||
|
|
||||||
|
const humanize = text => {
|
||||||
|
let humanized = text.replace(/([A-Z])/g, ' $1'); // insert a space before all caps
|
||||||
|
|
||||||
|
humanized = humanized.replace(/^./, str => {
|
||||||
|
// uppercase the first character
|
||||||
|
return str.toUpperCase();
|
||||||
|
});
|
||||||
|
|
||||||
|
return humanized;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the text of an attribute for a given attribute
|
||||||
|
* @param {String} attributeId The attribute ID
|
||||||
|
* @param {Array} attributes Array of attributes objects with id and text properties
|
||||||
|
* @return {String} If found return the attribute text or an empty string otherwise
|
||||||
|
*/
|
||||||
|
const getAttributeText = (attributeId, attributes) => {
|
||||||
|
// If the attribute is already in the cache, return it
|
||||||
|
if (attributeId in attributeCache) {
|
||||||
|
return attributeCache[attributeId];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the attribute with given attributeId
|
||||||
|
const attribute = attributes.find(attribute => attribute.id === attributeId);
|
||||||
|
|
||||||
|
let attributeText;
|
||||||
|
|
||||||
|
// If attribute was found get its text and save it on the cache
|
||||||
|
if (attribute) {
|
||||||
|
attributeText = attribute.text.replace(REGEXP, '');
|
||||||
|
attributeCache[attributeId] = attributeText;
|
||||||
|
}
|
||||||
|
|
||||||
|
return attributeText || '';
|
||||||
|
};
|
||||||
|
|
||||||
|
function displayConstraint(attributeId, constraint, attributes) {
|
||||||
|
if (!constraint || !attributeId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const validatorType = Object.keys(constraint)[0];
|
||||||
|
if (!validatorType) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const validator = Object.keys(constraint[validatorType])[0];
|
||||||
|
if (!validator) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const value = constraint[validatorType][validator];
|
||||||
|
if (value === void 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let comparator = validator;
|
||||||
|
if (validator === 'value') {
|
||||||
|
comparator = validatorType;
|
||||||
|
}
|
||||||
|
|
||||||
|
const attributeText = getAttributeText(attributeId, attributes);
|
||||||
|
const constraintText =
|
||||||
|
attributeText + ' ' + humanize(comparator).toLowerCase() + ' ' + value;
|
||||||
|
|
||||||
|
return constraintText;
|
||||||
|
}
|
||||||
@ -0,0 +1,32 @@
|
|||||||
|
/**
|
||||||
|
* Removes the first instance of an element from an array, if an equal value exists
|
||||||
|
*
|
||||||
|
* @param array
|
||||||
|
* @param input
|
||||||
|
*
|
||||||
|
* @returns {boolean} Whether or not the element was found and removed
|
||||||
|
*/
|
||||||
|
const removeFromArray = (array, input) => {
|
||||||
|
// If the array is empty, stop here
|
||||||
|
if (!array || !array.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
array.forEach((value, index) => {
|
||||||
|
// TODO: Double check whether or not this deep equality check is necessary
|
||||||
|
//if (_.isEqual(value, input)) {
|
||||||
|
if (value === input) {
|
||||||
|
indexToRemove = index;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (indexToRemove === void 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
array.splice(indexToRemove, 1);
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
export { removeFromArray };
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
// Sorts an array by score
|
||||||
|
const sortByScore = arr => {
|
||||||
|
arr.sort((a, b) => {
|
||||||
|
return b.score - a.score;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export { sortByScore };
|
||||||
@ -0,0 +1,39 @@
|
|||||||
|
import validate from 'validate.js';
|
||||||
|
|
||||||
|
validate.validators.equals = function (value, options, key, attributes) {
|
||||||
|
if (options && value !== options.value) {
|
||||||
|
return key + 'must equal ' + options.value;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
validate.validators.doesNotEqual = function (value, options, key) {
|
||||||
|
if (options && value === options.value) {
|
||||||
|
return key + 'cannot equal ' + options.value;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
validate.validators.contains = function (value, options, key) {
|
||||||
|
if (options && value.indexOf && value.indexOf(options.value) === -1) {
|
||||||
|
return key + 'must contain ' + options.value;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
validate.validators.doesNotContain = function (value, options, key) {
|
||||||
|
if (options && value.indexOf && value.indexOf(options.value) !== -1) {
|
||||||
|
return key + 'cannot contain ' + options.value;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
validate.validators.startsWith = function (value, options, key) {
|
||||||
|
if (options && value.startsWith && !value.startsWith(options.value)) {
|
||||||
|
return key + 'must start with ' + options.value;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
validate.validators.endsWith = function (value, options, key) {
|
||||||
|
if (options && value.endsWith && !value.endsWith(options.value)) {
|
||||||
|
return key + 'must end with ' + options.value;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default validate;
|
||||||
@ -54,6 +54,7 @@ const EVENTS = {
|
|||||||
MEASUREMENT_UPDATED: 'event::measurement_updated',
|
MEASUREMENT_UPDATED: 'event::measurement_updated',
|
||||||
INTERNAL_MEASUREMENT_UPDATED: 'event:internal_measurement_updated',
|
INTERNAL_MEASUREMENT_UPDATED: 'event:internal_measurement_updated',
|
||||||
MEASUREMENT_ADDED: 'event::measurement_added',
|
MEASUREMENT_ADDED: 'event::measurement_added',
|
||||||
|
RAW_MEASUREMENT_ADDED: 'event::raw_measurement_added',
|
||||||
MEASUREMENT_REMOVED: 'event::measurement_removed',
|
MEASUREMENT_REMOVED: 'event::measurement_removed',
|
||||||
MEASUREMENTS_CLEARED: 'event::measurements_cleared',
|
MEASUREMENTS_CLEARED: 'event::measurements_cleared',
|
||||||
JUMP_TO_MEASUREMENT: 'event:jump_to_measurement',
|
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
|
* Converted to/from annotation in the same way. E.g. import serialized data
|
||||||
* Of the same form as the measurement source.
|
* Of the same form as the measurement source.
|
||||||
* @param {MeasurementSource} source The measurement source instance.
|
* @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 {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.
|
* @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)) {
|
if (!this._isValidSource(source)) {
|
||||||
log.warn('Invalid source. Exiting early.');
|
log.warn('Invalid source. Exiting early.');
|
||||||
return;
|
return;
|
||||||
@ -342,7 +349,6 @@ class MeasurementService {
|
|||||||
try {
|
try {
|
||||||
/* Convert measurement */
|
/* Convert measurement */
|
||||||
measurement = toMeasurementSchema(data);
|
measurement = toMeasurementSchema(data);
|
||||||
|
|
||||||
/* Assign measurement source instance */
|
/* Assign measurement source instance */
|
||||||
measurement.source = source;
|
measurement.source = source;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -385,9 +391,11 @@ class MeasurementService {
|
|||||||
} else {
|
} else {
|
||||||
log.info(`Measurement added.`, newMeasurement);
|
log.info(`Measurement added.`, newMeasurement);
|
||||||
this.measurements[internalId] = newMeasurement;
|
this.measurements[internalId] = newMeasurement;
|
||||||
this._broadcastEvent(this.EVENTS.MEASUREMENT_ADDED, {
|
this._broadcastEvent(this.EVENTS.RAW_MEASUREMENT_ADDED, {
|
||||||
source,
|
source,
|
||||||
measurement: newMeasurement,
|
measurement: newMeasurement,
|
||||||
|
data,
|
||||||
|
dataSource,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -501,6 +509,7 @@ class MeasurementService {
|
|||||||
|
|
||||||
clearMeasurements() {
|
clearMeasurements() {
|
||||||
this.measurements = {};
|
this.measurements = {};
|
||||||
|
this._jumpToMeasurementCache = {};
|
||||||
this._broadcastEvent(this.EVENTS.MEASUREMENTS_CLEARED);
|
this._broadcastEvent(this.EVENTS.MEASUREMENTS_CLEARED);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -13,6 +13,7 @@ export default class ToolBarService {
|
|||||||
this.EVENTS = EVENTS;
|
this.EVENTS = EVENTS;
|
||||||
this.listeners = {};
|
this.listeners = {};
|
||||||
this.buttons = {};
|
this.buttons = {};
|
||||||
|
this.unsubscriptions = []; // if tools need to unsubscribe from events
|
||||||
this.buttonSections = {
|
this.buttonSections = {
|
||||||
/**
|
/**
|
||||||
* primary: ['Zoom', 'Wwwc'],
|
* primary: ['Zoom', 'Wwwc'],
|
||||||
@ -39,6 +40,16 @@ export default class ToolBarService {
|
|||||||
this.extensionManager = extensionManager;
|
this.extensionManager = extensionManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
reset() {
|
||||||
|
this.unsubscriptions.forEach(unsub => unsub());
|
||||||
|
this.state = {
|
||||||
|
primaryToolId: 'Wwwc',
|
||||||
|
toggles: {},
|
||||||
|
groups: {},
|
||||||
|
};
|
||||||
|
this.unsubscriptions = [];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param {*} interaction
|
* @param {*} interaction
|
||||||
@ -63,6 +74,7 @@ export default class ToolBarService {
|
|||||||
this.state.toggles[itemId] === undefined
|
this.state.toggles[itemId] === undefined
|
||||||
? true
|
? true
|
||||||
: !this.state.toggles[itemId];
|
: !this.state.toggles[itemId];
|
||||||
|
interaction.commandOptions.toggledState = this.state.toggles[itemId];
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
@ -73,13 +85,21 @@ export default class ToolBarService {
|
|||||||
//
|
//
|
||||||
// NOTE: Should probably just do this for tools as well?
|
// NOTE: Should probably just do this for tools as well?
|
||||||
// But would be nice if we could enforce at least the command name?
|
// But would be nice if we could enforce at least the command name?
|
||||||
|
let unsubscribe;
|
||||||
if (interaction.commandName) {
|
if (interaction.commandName) {
|
||||||
commandsManager.runCommand(
|
unsubscribe = commandsManager.runCommand(
|
||||||
interaction.commandName,
|
interaction.commandName,
|
||||||
interaction.commandOptions
|
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
|
// Track last touched id for each group
|
||||||
if (groupId) {
|
if (groupId) {
|
||||||
this.state.groups[groupId] = itemId;
|
this.state.groups[groupId] = itemId;
|
||||||
|
|||||||
@ -10,6 +10,7 @@ import ToolBarService from './ToolBarService';
|
|||||||
import ViewportGridService from './ViewportGridService';
|
import ViewportGridService from './ViewportGridService';
|
||||||
import CineService from './CineService';
|
import CineService from './CineService';
|
||||||
import HangingProtocolService from './HangingProtocolService';
|
import HangingProtocolService from './HangingProtocolService';
|
||||||
|
import pubSubServiceInterface from './_shared/pubSubServiceInterface';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
MeasurementService,
|
MeasurementService,
|
||||||
@ -23,5 +24,6 @@ export {
|
|||||||
ToolBarService,
|
ToolBarService,
|
||||||
ViewportGridService,
|
ViewportGridService,
|
||||||
HangingProtocolService,
|
HangingProtocolService,
|
||||||
CineService
|
CineService,
|
||||||
|
pubSubServiceInterface,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -22,6 +22,7 @@ function createAndAddStack(
|
|||||||
StudyInstanceUID,
|
StudyInstanceUID,
|
||||||
frameRate,
|
frameRate,
|
||||||
isClip,
|
isClip,
|
||||||
|
initialImageIdIndex,
|
||||||
} = displaySet;
|
} = displaySet;
|
||||||
if (!images) {
|
if (!images) {
|
||||||
return;
|
return;
|
||||||
@ -35,6 +36,7 @@ function createAndAddStack(
|
|||||||
imageIds,
|
imageIds,
|
||||||
frameRate,
|
frameRate,
|
||||||
isClip,
|
isClip,
|
||||||
|
initialImageIdIndex,
|
||||||
};
|
};
|
||||||
|
|
||||||
stackMap[displaySetInstanceUID] = stack;
|
stackMap[displaySetInstanceUID] = stack;
|
||||||
|
|||||||
@ -2,15 +2,16 @@
|
|||||||
* Sorting function
|
* Sorting function
|
||||||
* Sorts an array by seriesDate and seriesNumber if equal
|
* Sorts an array by seriesDate and seriesNumber if equal
|
||||||
*/
|
*/
|
||||||
const sortBySeriesDate = array => array.sort((a, b) => {
|
const sortBySeriesDate = array =>
|
||||||
if (a.seriesNumber !== b.seriesNumber) {
|
array.sort((a, b) => {
|
||||||
return a.seriesNumber - b.seriesNumber;
|
if (a.seriesNumber !== b.seriesNumber) {
|
||||||
}
|
return a.seriesNumber - b.seriesNumber;
|
||||||
|
}
|
||||||
|
|
||||||
const seriesDateA = Date.parse(a.seriesDate);
|
const seriesDateA = Date.parse(a.seriesDate);
|
||||||
const seriesDateB = Date.parse(b.seriesDate);
|
const seriesDateB = Date.parse(b.seriesDate);
|
||||||
|
|
||||||
return seriesDateA - seriesDateB;
|
return seriesDateA - seriesDateB;
|
||||||
});
|
});
|
||||||
|
|
||||||
export default sortBySeriesDate;
|
export default sortBySeriesDate;
|
||||||
|
|||||||
@ -25,9 +25,6 @@ export {
|
|||||||
ViewportGridContext,
|
ViewportGridContext,
|
||||||
ViewportGridProvider,
|
ViewportGridProvider,
|
||||||
useViewportGrid,
|
useViewportGrid,
|
||||||
HangingProtocolContext,
|
|
||||||
HangingProtocolProvider,
|
|
||||||
useHangingProtocol,
|
|
||||||
} from './src/contextProviders';
|
} from './src/contextProviders';
|
||||||
|
|
||||||
/** COMPONENTS */
|
/** COMPONENTS */
|
||||||
|
|||||||
@ -6,7 +6,7 @@ import classNames from 'classnames';
|
|||||||
import { useHistory } from 'react-router-dom';
|
import { useHistory } from 'react-router-dom';
|
||||||
import { NavBar, Svg, Icon, IconButton, Dropdown } from '@ohif/ui';
|
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 { t } = useTranslation();
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
|
|
||||||
@ -18,6 +18,10 @@ function Header({ children, menuOptions, isReturnEnabled, isSticky }) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const CustomLogo = (React) => {
|
||||||
|
return WhiteLabeling.createLogoComponentFn(React)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NavBar className='justify-between border-b-4 border-black' isSticky={isSticky}>
|
<NavBar className='justify-between border-b-4 border-black' isSticky={isSticky}>
|
||||||
<div className="flex justify-between flex-1">
|
<div className="flex justify-between flex-1">
|
||||||
@ -29,7 +33,7 @@ function Header({ children, menuOptions, isReturnEnabled, isSticky }) {
|
|||||||
onClick={onReturnHandler}
|
onClick={onReturnHandler}
|
||||||
>
|
>
|
||||||
{isReturnEnabled && <Icon name="chevron-left" className="w-8 text-primary-active" />}
|
{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>
|
</div>
|
||||||
<div className="flex items-center">{children}</div>
|
<div className="flex items-center">{children}</div>
|
||||||
@ -73,7 +77,8 @@ Header.propTypes = {
|
|||||||
),
|
),
|
||||||
children: PropTypes.oneOfType([PropTypes.node, PropTypes.func]),
|
children: PropTypes.oneOfType([PropTypes.node, PropTypes.func]),
|
||||||
isReturnEnabled: PropTypes.bool,
|
isReturnEnabled: PropTypes.bool,
|
||||||
isSticky: PropTypes.bool
|
isSticky: PropTypes.bool,
|
||||||
|
WhiteLabeling: PropTypes.element,
|
||||||
};
|
};
|
||||||
|
|
||||||
Header.defaultProps = {
|
Header.defaultProps = {
|
||||||
|
|||||||
@ -143,7 +143,7 @@ const SidePanel = ({
|
|||||||
</span>
|
</span>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<childComponent.content/>
|
<childComponent.content />
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
) : (
|
) : (
|
||||||
<React.Fragment>{getPanelButtons()}</React.Fragment>
|
<React.Fragment>{getPanelButtons()}</React.Fragment>
|
||||||
|
|||||||
@ -14,6 +14,7 @@ const classes = {
|
|||||||
const ViewportActionBar = ({
|
const ViewportActionBar = ({
|
||||||
studyData,
|
studyData,
|
||||||
showNavArrows,
|
showNavArrows,
|
||||||
|
showStatus,
|
||||||
showCine,
|
showCine,
|
||||||
cineProps,
|
cineProps,
|
||||||
showPatientInfo: patientInfoVisibility,
|
showPatientInfo: patientInfoVisibility,
|
||||||
@ -255,7 +256,7 @@ const ViewportActionBar = ({
|
|||||||
<div className="flex flex-1 flex-grow mt-2 min-w-48">
|
<div className="flex flex-1 flex-grow mt-2 min-w-48">
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<span className="mr-2 text-white text-large">{label}</span>
|
<span className="mr-2 text-white text-large">{label}</span>
|
||||||
{renderIconStatus()}
|
{showStatus && renderIconStatus()}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col justify-start ml-4">
|
<div className="flex flex-col justify-start ml-4">
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
@ -352,6 +353,7 @@ ViewportActionBar.propTypes = {
|
|||||||
ViewportActionBar.defaultProps = {
|
ViewportActionBar.defaultProps = {
|
||||||
cineProps: {},
|
cineProps: {},
|
||||||
showCine: false,
|
showCine: false,
|
||||||
|
showStatus: true,
|
||||||
showNavArrows: true,
|
showNavArrows: true,
|
||||||
showPatientInfo: false,
|
showPatientInfo: false,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,153 +0,0 @@
|
|||||||
import React, {
|
|
||||||
createContext,
|
|
||||||
useCallback,
|
|
||||||
useContext,
|
|
||||||
useEffect,
|
|
||||||
useReducer,
|
|
||||||
} from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
|
|
||||||
const DEFAULT_STATE = {
|
|
||||||
hangingProtocol: null,
|
|
||||||
hpAlreadyApplied: {},
|
|
||||||
};
|
|
||||||
|
|
||||||
export const HangingProtocolContext = createContext(DEFAULT_STATE);
|
|
||||||
|
|
||||||
export function HangingProtocolProvider({ children, service }) {
|
|
||||||
const hangingProtocolReducer = (state, action) => {
|
|
||||||
switch (action.type) {
|
|
||||||
case 'SET_HANGING_PROTOCOL': {
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
...{ hangingProtocol: action.payload.hangingProtocol },
|
|
||||||
};
|
|
||||||
}
|
|
||||||
case 'SET_HANGING_PROTOCOL_APPLIED_FOR_VIEWPORT': {
|
|
||||||
const index = action.payload.index;
|
|
||||||
const newHPAlreadyApplied = Object.assign({}, state.hpAlreadyApplied);
|
|
||||||
|
|
||||||
newHPAlreadyApplied[index] = true;
|
|
||||||
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
...{ hpAlreadyApplied: newHPAlreadyApplied },
|
|
||||||
};
|
|
||||||
}
|
|
||||||
case 'RESET': {
|
|
||||||
return {
|
|
||||||
hangingProtocol: null,
|
|
||||||
hpAlreadyApplied: {},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'SET': {
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
...action.payload,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
default:
|
|
||||||
return action.payload;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const [hangingProtocolState, dispatch] = useReducer(
|
|
||||||
hangingProtocolReducer,
|
|
||||||
DEFAULT_STATE
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log('hangingProtocolState', hangingProtocolState);
|
|
||||||
|
|
||||||
const getState = useCallback(() => hangingProtocolState, [
|
|
||||||
hangingProtocolState,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const setHangingProtocol = useCallback(
|
|
||||||
hangingProtocol =>
|
|
||||||
dispatch({
|
|
||||||
type: 'SET_HANGING_PROTOCOL',
|
|
||||||
payload: {
|
|
||||||
hangingProtocol,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
[dispatch]
|
|
||||||
);
|
|
||||||
|
|
||||||
const setHangingProtocolAppliedForViewport = useCallback(
|
|
||||||
index =>
|
|
||||||
dispatch({
|
|
||||||
type: 'SET_HANGING_PROTOCOL_APPLIED_FOR_VIEWPORT',
|
|
||||||
payload: {
|
|
||||||
index,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
[dispatch]
|
|
||||||
);
|
|
||||||
|
|
||||||
const reset = useCallback(
|
|
||||||
() =>
|
|
||||||
dispatch({
|
|
||||||
type: 'RESET',
|
|
||||||
payload: {},
|
|
||||||
}),
|
|
||||||
[dispatch]
|
|
||||||
);
|
|
||||||
|
|
||||||
const set = useCallback(
|
|
||||||
payload =>
|
|
||||||
dispatch({
|
|
||||||
type: 'SET',
|
|
||||||
payload,
|
|
||||||
}),
|
|
||||||
[dispatch]
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the implementation of the HangingProtocolService that can be used by extensions.
|
|
||||||
*
|
|
||||||
* @returns void
|
|
||||||
*/
|
|
||||||
useEffect(() => {
|
|
||||||
if (service) {
|
|
||||||
service.setServiceImplementation({
|
|
||||||
getState,
|
|
||||||
setHangingProtocol,
|
|
||||||
setHangingProtocolAppliedForViewport,
|
|
||||||
reset,
|
|
||||||
set,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [
|
|
||||||
getState,
|
|
||||||
service,
|
|
||||||
setHangingProtocol,
|
|
||||||
setHangingProtocolAppliedForViewport,
|
|
||||||
reset,
|
|
||||||
set,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const api = {
|
|
||||||
// getState,
|
|
||||||
setHangingProtocol,
|
|
||||||
setHangingProtocolAppliedForViewport,
|
|
||||||
reset,
|
|
||||||
set,
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<HangingProtocolContext.Provider value={[hangingProtocolState, api]}>
|
|
||||||
{children}
|
|
||||||
</HangingProtocolContext.Provider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
HangingProtocolProvider.propTypes = {
|
|
||||||
children: PropTypes.any,
|
|
||||||
service: PropTypes.shape({
|
|
||||||
setServiceImplementation: PropTypes.func,
|
|
||||||
}).isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useHangingProtocol = () => useContext(HangingProtocolContext);
|
|
||||||
@ -1,4 +1,4 @@
|
|||||||
import React, { createContext, useContext, useReducer } from 'react';
|
import React, { createContext, useContext, useReducer, useMemo } from 'react';
|
||||||
|
|
||||||
// export const IMAGE_VIEWER_DEFAULT_VALUE = {
|
// export const IMAGE_VIEWER_DEFAULT_VALUE = {
|
||||||
// StudyInstanceUIDs: [],
|
// StudyInstanceUIDs: [],
|
||||||
@ -7,9 +7,18 @@ import React, { createContext, useContext, useReducer } from 'react';
|
|||||||
|
|
||||||
export const ImageViewerContext = createContext();
|
export const ImageViewerContext = createContext();
|
||||||
|
|
||||||
export function ImageViewerProvider({ reducer, initialState, children }) {
|
export function ImageViewerProvider({
|
||||||
|
StudyInstanceUIDs,
|
||||||
|
reducer,
|
||||||
|
initialState,
|
||||||
|
children,
|
||||||
|
}) {
|
||||||
|
const value = useMemo(() => {
|
||||||
|
return { StudyInstanceUIDs };
|
||||||
|
}, [StudyInstanceUIDs]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ImageViewerContext.Provider value={useReducer(reducer, initialState)}>
|
<ImageViewerContext.Provider value={value}>
|
||||||
{children}
|
{children}
|
||||||
</ImageViewerContext.Provider>
|
</ImageViewerContext.Provider>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -8,8 +8,10 @@ import React, {
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
const DEFAULT_STATE = {
|
const DEFAULT_STATE = {
|
||||||
numRows: 1,
|
// starting from null, hanging
|
||||||
numCols: 1,
|
// protocol will defined number of rows and cols
|
||||||
|
numRows: null,
|
||||||
|
numCols: null,
|
||||||
viewports: [
|
viewports: [
|
||||||
/*
|
/*
|
||||||
* {
|
* {
|
||||||
@ -58,12 +60,14 @@ export function ViewportGridProvider({ children, service }) {
|
|||||||
}
|
}
|
||||||
case 'RESET': {
|
case 'RESET': {
|
||||||
return {
|
return {
|
||||||
numCols: 1,
|
numCols: null,
|
||||||
numRows: 1,
|
numRows: null,
|
||||||
activeViewportIndex: 0,
|
activeViewportIndex: 0,
|
||||||
viewports: [{
|
viewports: [
|
||||||
displaySetInstanceUID: null,
|
{
|
||||||
}],
|
displaySetInstanceUID: null,
|
||||||
|
},
|
||||||
|
],
|
||||||
cachedLayout: null,
|
cachedLayout: null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -41,9 +41,3 @@ export {
|
|||||||
ViewportGridProvider,
|
ViewportGridProvider,
|
||||||
useViewportGrid,
|
useViewportGrid,
|
||||||
} from './ViewportGridProvider';
|
} from './ViewportGridProvider';
|
||||||
|
|
||||||
export {
|
|
||||||
HangingProtocolContext,
|
|
||||||
HangingProtocolProvider,
|
|
||||||
useHangingProtocol,
|
|
||||||
} from './HangingProtocolProvider';
|
|
||||||
|
|||||||
@ -10,9 +10,9 @@ services:
|
|||||||
ohif_viewer:
|
ohif_viewer:
|
||||||
build:
|
build:
|
||||||
# Project root
|
# Project root
|
||||||
context: ./../../
|
context: ./../../../../
|
||||||
# Relative to context
|
# Relative to context
|
||||||
dockerfile: ./docker/OpenResty-Orthanc/dockerfile
|
dockerfile: ./platform/viewer/.recipes/OpenResty-Orthanc/dockerfile
|
||||||
image: webapp:latest
|
image: webapp:latest
|
||||||
container_name: webapp
|
container_name: webapp
|
||||||
volumes:
|
volumes:
|
||||||
|
|||||||
@ -23,20 +23,39 @@
|
|||||||
|
|
||||||
|
|
||||||
# Stage 1: Build the application
|
# 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
|
RUN mkdir /usr/src/app
|
||||||
WORKDIR /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 APP_CONFIG=config/docker_openresty-orthanc.js
|
||||||
ENV PATH /usr/src/app/node_modules/.bin:$PATH
|
ENV PATH /usr/src/app/node_modules/.bin:$PATH
|
||||||
|
|
||||||
COPY package.json /usr/src/app/package.json
|
ENV QUICK_BUILD true
|
||||||
COPY yarn.lock /usr/src/app/yarn.lock
|
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
|
# Stage 2: Bundle the built application into a Docker container
|
||||||
# which runs openresty (nginx) using Alpine Linux
|
# which runs openresty (nginx) using Alpine Linux
|
||||||
@ -60,6 +79,6 @@ RUN luarocks install lua-resty-openidc
|
|||||||
RUN luarocks install luacrypto
|
RUN luarocks install luacrypto
|
||||||
|
|
||||||
# Copy build output to image
|
# 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;"]
|
ENTRYPOINT ["/usr/local/openresty/nginx/sbin/nginx", "-g", "daemon off;"]
|
||||||
|
|||||||
21
platform/viewer/cypress/plugins/index.js
Normal file
21
platform/viewer/cypress/plugins/index.js
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
/// <reference types="cypress" />
|
||||||
|
// ***********************************************************
|
||||||
|
// This example plugins/index.js can be used to load plugins
|
||||||
|
//
|
||||||
|
// You can change the location of this file or turn off loading
|
||||||
|
// the plugins file with the 'pluginsFile' configuration option.
|
||||||
|
//
|
||||||
|
// You can read more here:
|
||||||
|
// https://on.cypress.io/plugins-guide
|
||||||
|
// ***********************************************************
|
||||||
|
|
||||||
|
// This function is called when a project is opened or re-opened (e.g. due to
|
||||||
|
// the project's config changing)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {Cypress.PluginConfig}
|
||||||
|
*/
|
||||||
|
module.exports = (on, config) => {
|
||||||
|
// `on` is used to hook into various events Cypress emits
|
||||||
|
// `config` is the resolved Cypress config
|
||||||
|
}
|
||||||
@ -55,7 +55,7 @@
|
|||||||
"@types/react": "^16.0.0",
|
"@types/react": "^16.0.0",
|
||||||
"classnames": "^2.2.6",
|
"classnames": "^2.2.6",
|
||||||
"core-js": "^3.2.1",
|
"core-js": "^3.2.1",
|
||||||
"cornerstone-math": "^0.1.8",
|
"cornerstone-math": "^0.1.9",
|
||||||
"cornerstone-tools": "5.1.2",
|
"cornerstone-tools": "5.1.2",
|
||||||
"cornerstone-wado-image-loader": "^3.1.2",
|
"cornerstone-wado-image-loader": "^3.1.2",
|
||||||
"dcmjs": "0.16.1",
|
"dcmjs": "0.16.1",
|
||||||
|
|||||||
@ -25,5 +25,24 @@ window.config = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
// whiteLabeling: {
|
||||||
|
// /* Optional: Should return a React component to be rendered in the "Logo" section of the application's Top Navigation bar */
|
||||||
|
// createLogoComponentFn: function (React) {
|
||||||
|
// return React.createElement(
|
||||||
|
// 'a',
|
||||||
|
// {
|
||||||
|
// target: '_self',
|
||||||
|
// rel: 'noopener noreferrer',
|
||||||
|
// className: 'text-purple-600 line-through',
|
||||||
|
// href: '/',
|
||||||
|
// },
|
||||||
|
// React.createElement('img',
|
||||||
|
// {
|
||||||
|
// src: './customLogo.svg',
|
||||||
|
// className: 'w-8 h-8',
|
||||||
|
// }
|
||||||
|
// ))
|
||||||
|
// },
|
||||||
|
// },
|
||||||
defaultDataSourceName: 'dicomweb',
|
defaultDataSourceName: 'dicomweb',
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,9 +1,14 @@
|
|||||||
window.config = {
|
window.config = {
|
||||||
routerBasename: '/',
|
routerBasename: '/',
|
||||||
showStudyList: true,
|
showStudyList: true,
|
||||||
servers: {
|
extensions: [],
|
||||||
dicomWeb: [
|
modes: [],
|
||||||
{
|
dataSources: [
|
||||||
|
{
|
||||||
|
friendlyName: 'Orthanc Server',
|
||||||
|
namespace: 'org.ohif.default.dataSourcesModule.dicomweb',
|
||||||
|
sourceName: 'dicomweb',
|
||||||
|
configuration: {
|
||||||
name: 'Orthanc',
|
name: 'Orthanc',
|
||||||
wadoUriRoot: '/wado',
|
wadoUriRoot: '/wado',
|
||||||
qidoRoot: '/dicom-web',
|
qidoRoot: '/dicom-web',
|
||||||
@ -12,6 +17,7 @@ window.config = {
|
|||||||
imageRendering: 'wadors',
|
imageRendering: 'wadors',
|
||||||
thumbnailRendering: 'wadors',
|
thumbnailRendering: 'wadors',
|
||||||
},
|
},
|
||||||
],
|
},
|
||||||
},
|
],
|
||||||
|
defaultDataSourceName: 'dicomweb',
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,10 +1,14 @@
|
|||||||
window.config = {
|
window.config = {
|
||||||
routerBasename: '/',
|
routerBasename: '/',
|
||||||
showStudyList: true,
|
showStudyList: true,
|
||||||
servers: {
|
extensions: [],
|
||||||
// This is an array, but we'll only use the first entry for now
|
modes: [],
|
||||||
dicomWeb: [
|
dataSources: [
|
||||||
{
|
{
|
||||||
|
friendlyName: 'Orthanc Server',
|
||||||
|
namespace: 'org.ohif.default.dataSourcesModule.dicomweb',
|
||||||
|
sourceName: 'dicomweb',
|
||||||
|
configuration: {
|
||||||
name: 'Orthanc',
|
name: 'Orthanc',
|
||||||
wadoUriRoot: 'http://127.0.0.1/pacs/wado',
|
wadoUriRoot: 'http://127.0.0.1/pacs/wado',
|
||||||
qidoRoot: 'http://127.0.0.1/pacs/dicom-web',
|
qidoRoot: 'http://127.0.0.1/pacs/dicom-web',
|
||||||
@ -12,11 +16,8 @@ window.config = {
|
|||||||
qidoSupportsIncludeField: false,
|
qidoSupportsIncludeField: false,
|
||||||
imageRendering: 'wadors',
|
imageRendering: 'wadors',
|
||||||
thumbnailRendering: 'wadors',
|
thumbnailRendering: 'wadors',
|
||||||
// requestOptions: {
|
|
||||||
// undefined to use JWT + Bearer auth
|
|
||||||
// auth: 'orthanc:orthanc',
|
|
||||||
// },
|
|
||||||
},
|
},
|
||||||
],
|
},
|
||||||
},
|
],
|
||||||
|
defaultDataSourceName: 'dicomweb',
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,11 +1,14 @@
|
|||||||
window.config = {
|
window.config = {
|
||||||
// default: '/'
|
|
||||||
routerBasename: '/',
|
routerBasename: '/',
|
||||||
// default: ''
|
|
||||||
showStudyList: true,
|
showStudyList: true,
|
||||||
servers: {
|
extensions: [],
|
||||||
dicomWeb: [
|
modes: [],
|
||||||
{
|
dataSources: [
|
||||||
|
{
|
||||||
|
friendlyName: 'DCM4CHEE Server',
|
||||||
|
namespace: 'org.ohif.default.dataSourcesModule.dicomweb',
|
||||||
|
sourceName: 'dicomweb',
|
||||||
|
configuration: {
|
||||||
name: 'DCM4CHEE',
|
name: 'DCM4CHEE',
|
||||||
wadoUriRoot: 'http://localhost:8080/dcm4chee-arc/aets/DCM4CHEE/wado',
|
wadoUriRoot: 'http://localhost:8080/dcm4chee-arc/aets/DCM4CHEE/wado',
|
||||||
qidoRoot: 'http://localhost:8080/dcm4chee-arc/aets/DCM4CHEE/rs',
|
qidoRoot: 'http://localhost:8080/dcm4chee-arc/aets/DCM4CHEE/rs',
|
||||||
@ -17,7 +20,8 @@ window.config = {
|
|||||||
auth: 'admin:admin',
|
auth: 'admin:admin',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
},
|
||||||
},
|
],
|
||||||
studyListFunctionsEnabled: true,
|
studyListFunctionsEnabled: true,
|
||||||
|
defaultDataSourceName: 'dicomweb',
|
||||||
};
|
};
|
||||||
|
|||||||
65
platform/viewer/public/customLogo.svg
Normal file
65
platform/viewer/public/customLogo.svg
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="265" height="24" viewBox="0 0 265 24">
|
||||||
|
<g fill="none" fill-rule="evenodd">
|
||||||
|
<g>
|
||||||
|
<path fill="#FFF" d="M2.431 8.257c.733.734 1.582 1.12 2.547 1.12.964 0 1.813-.348 2.546-1.12.733-.733 1.08-1.736 1.08-3.01 0-1.273-.385-2.276-1.118-3.01-.733-.732-1.582-1.08-2.547-1.08-.965 0-1.814.348-2.547 1.08-.733.734-1.119 1.737-1.119 3.01.077 1.274.425 2.316 1.158 3.01m7.563-3.01c0 1.583-.463 2.856-1.428 3.82-.965.965-2.122 1.467-3.55 1.467-1.427 0-2.624-.502-3.588-1.505C.463 8.026 0 6.753 0 5.21c0-1.543.502-2.817 1.466-3.781C2.431.463 3.627 0 5.016 0c1.35 0 2.547.463 3.511 1.428.965.964 1.467 2.238 1.467 3.82" transform="translate(1 1) translate(33.803 4.985)"/>
|
||||||
|
<path stroke="#FFF" stroke-width=".75" d="M2.431 8.257c.733.734 1.582 1.12 2.547 1.12.964 0 1.813-.348 2.546-1.12.733-.733 1.08-1.736 1.08-3.01 0-1.273-.385-2.276-1.118-3.01-.733-.732-1.582-1.08-2.547-1.08-.965 0-1.814.348-2.547 1.08-.733.734-1.119 1.737-1.119 3.01.077 1.274.425 2.316 1.158 3.01zm7.563-3.01c0 1.583-.463 2.856-1.428 3.82-.965.965-2.122 1.467-3.55 1.467-1.427 0-2.624-.502-3.588-1.505C.463 8.026 0 6.753 0 5.21c0-1.543.502-2.817 1.466-3.781C2.431.463 3.627 0 5.016 0c1.35 0 2.547.463 3.511 1.428.965.964 1.467 2.238 1.467 3.82z" transform="translate(1 1) translate(33.803 4.985)"/>
|
||||||
|
<path fill="#FFF" d="M13.35 6.945c0 .85.232 1.467.657 1.93.424.463.964.694 1.582.694.617 0 1.119-.231 1.543-.733.425-.501.618-1.119.618-1.929s-.193-1.428-.618-1.89c-.424-.464-.926-.734-1.543-.734-.618 0-1.158.231-1.582.695-.425.463-.656 1.119-.656 1.967m.964 6.715h-2.893c-.116 0-.193-.04-.193-.155v-.617c0-.116.077-.155.193-.155h.81V4.322h-.772c-.116 0-.193-.039-.193-.155V3.55c0-.116.077-.154.193-.154h1.698c.116 0 .193.038.193.154v1.119c.193-.386.463-.733.887-1.003.425-.27.926-.425 1.505-.425.965 0 1.737.347 2.354 1.003.617.656.887 1.544.887 2.624s-.308 1.968-.887 2.663c-.579.694-1.35 1.003-2.354 1.003-.579 0-1.08-.154-1.505-.424-.386-.27-.694-.618-.849-1.004h-.038c.038.31.038.54.038.656v2.933h.926c.116 0 .193.038.193.154v.618c0 .154-.077.193-.193.193" transform="translate(1 1) translate(33.803 4.985)"/>
|
||||||
|
<path stroke="#FFF" stroke-width=".75" d="M13.35 6.945c0 .85.232 1.467.657 1.93.424.463.964.694 1.582.694.617 0 1.119-.231 1.543-.733.425-.501.618-1.119.618-1.929s-.193-1.428-.618-1.89c-.424-.464-.926-.734-1.543-.734-.618 0-1.158.231-1.582.695-.425.463-.656 1.119-.656 1.967zm.965 6.715h-2.893c-.116 0-.193-.04-.193-.155v-.617c0-.116.077-.155.193-.155h.81V4.322h-.772c-.116 0-.193-.039-.193-.155V3.55c0-.116.077-.154.193-.154h1.698c.116 0 .193.038.193.154v1.119c.193-.386.463-.733.887-1.003.425-.27.926-.425 1.505-.425.965 0 1.737.347 2.354 1.003.617.656.887 1.544.887 2.624s-.308 1.968-.887 2.663c-.579.694-1.35 1.003-2.354 1.003-.579 0-1.08-.154-1.505-.424-.386-.27-.694-.618-.849-1.004h-.038c.038.31.038.54.038.656v2.933h.926c.116 0 .193.038.193.154v.618c0 .154-.077.193-.193.193z" transform="translate(1 1) translate(33.803 4.985)"/>
|
||||||
|
<path fill="#FFF" d="M24.811 4.746c-.347-.347-.81-.54-1.39-.54-.578 0-1.08.154-1.465.501-.425.31-.656.81-.734 1.467h4.168c-.039-.618-.232-1.08-.579-1.428m1.582 2.315h-5.17c.038.81.308 1.39.771 1.852.463.464 1.003.656 1.582.656.579 0 1.08-.115 1.505-.386.424-.231.694-.501.849-.733.077-.077.154-.115.27-.038l.386.386c.039.038.077.115.077.154 0 .039-.038.077-.077.154s-.116.155-.27.309c-.155.154-.309.309-.579.502-.27.192-.579.347-.965.463-.386.115-.81.193-1.234.193-1.004 0-1.853-.348-2.509-1.004-.656-.656-1.003-1.543-1.003-2.662s.347-2.007 1.003-2.662c.656-.656 1.505-.965 2.47-.965.965 0 1.736.309 2.276.926.54.617.85 1.466.85 2.547v.115c-.04.116-.116.193-.232.193" transform="translate(1 1) translate(33.803 4.985)"/>
|
||||||
|
<path stroke="#FFF" stroke-width=".75" d="M24.811 4.746c-.347-.347-.81-.54-1.39-.54-.578 0-1.08.154-1.465.501-.425.31-.656.81-.734 1.467h4.168c-.039-.618-.232-1.08-.579-1.428zm1.582 2.315h-5.17c.038.81.308 1.39.771 1.852.463.464 1.003.656 1.582.656.579 0 1.08-.115 1.505-.386.424-.231.694-.501.849-.733.077-.077.154-.115.27-.038l.386.386c.039.038.077.115.077.154 0 .039-.038.077-.077.154s-.116.155-.27.309c-.155.154-.309.309-.579.502-.27.192-.579.347-.965.463-.386.115-.81.193-1.234.193-1.004 0-1.853-.348-2.509-1.004-.656-.656-1.003-1.543-1.003-2.662s.347-2.007 1.003-2.662c.656-.656 1.505-.965 2.47-.965.965 0 1.736.309 2.276.926.54.617.85 1.466.85 2.547v.115c-.04.116-.116.193-.232.193z" transform="translate(1 1) translate(33.803 4.985)"/>
|
||||||
|
<path fill="#FFF" d="M35.345 10.457h-1.35c-.425 0-.618-.193-.618-.618V6.058c0-.54-.154-1.003-.386-1.312-.27-.309-.656-.463-1.157-.463-.733 0-1.312.309-1.737.926-.038.039-.116.155-.193.309v3.936h.772c.116 0 .193.038.193.154v.617c0 .116-.077.155-.193.155h-2.74c-.115 0-.192-.039-.192-.155v-.617c0-.116.077-.154.192-.154h.772V4.322h-.772c-.115 0-.192-.039-.192-.155V3.55c0-.116.077-.154.192-.154h1.698c.116 0 .193.038.193.154l-.038.965h.038c.502-.85 1.274-1.235 2.315-1.235.772 0 1.35.231 1.814.694.463.463.656 1.042.656 1.814v3.704h.81c.116 0 .193.039.193.155v.617c-.077.116-.116.193-.27.193" transform="translate(1 1) translate(33.803 4.985)"/>
|
||||||
|
<path stroke="#FFF" stroke-width=".75" d="M35.345 10.457h-1.35c-.425 0-.618-.193-.618-.618V6.058c0-.54-.154-1.003-.386-1.312-.27-.309-.656-.463-1.157-.463-.733 0-1.312.309-1.737.926-.038.039-.116.155-.193.309v3.936h.772c.116 0 .193.038.193.154v.617c0 .116-.077.155-.193.155h-2.74c-.115 0-.192-.039-.192-.155v-.617c0-.116.077-.154.192-.154h.772V4.322h-.772c-.115 0-.192-.039-.192-.155V3.55c0-.116.077-.154.192-.154h1.698c.116 0 .193.038.193.154l-.038.965h.038c.502-.85 1.274-1.235 2.315-1.235.772 0 1.35.231 1.814.694.463.463.656 1.042.656 1.814v3.704h.81c.116 0 .193.039.193.155v.617c-.077.116-.116.193-.27.193z" transform="translate(1 1) translate(33.803 4.985)"/>
|
||||||
|
<path fill="#FFF" d="M51.513 10.457H48.04c-.116 0-.193-.039-.193-.155V9.57c0-.115.039-.154.193-.154h1.119V5.904h-5.364v3.511h1.12c.115 0 .192.039.192.154v.733c0 .116-.077.155-.193.155H41.48c-.116 0-.193-.039-.193-.155V9.57c0-.115.077-.154.193-.154h1.08V1.196h-1.08c-.116 0-.193-.038-.193-.154V.309c0-.116.077-.155.193-.155h3.434c.116 0 .193.039.193.155v.733c0 .116-.077.154-.193.154h-1.119v3.589h5.364V1.196h-1.12c-.115 0-.192-.038-.192-.154V.309c0-.116.039-.155.193-.155h3.473c.115 0 .192.039.192.155v.733c0 .116-.038.154-.192.154h-1.12v8.18h1.12c.115 0 .192.039.192.155v.733c0 .116-.077.193-.192.193" transform="translate(1 1) translate(33.803 4.985)"/>
|
||||||
|
<path stroke="#FFF" stroke-width=".75" d="M51.513 10.457H48.04c-.116 0-.193-.039-.193-.155V9.57c0-.115.039-.154.193-.154h1.119V5.904h-5.364v3.511h1.12c.115 0 .192.039.192.154v.733c0 .116-.077.155-.193.155H41.48c-.116 0-.193-.039-.193-.155V9.57c0-.115.077-.154.193-.154h1.08V1.196h-1.08c-.116 0-.193-.038-.193-.154V.309c0-.116.077-.155.193-.155h3.434c.116 0 .193.039.193.155v.733c0 .116-.077.154-.193.154h-1.119v3.589h5.364V1.196h-1.12c-.115 0-.192-.038-.192-.154V.309c0-.116.039-.155.193-.155h3.473c.115 0 .192.039.192.155v.733c0 .116-.038.154-.192.154h-1.12v8.18h1.12c.115 0 .192.039.192.155v.733c0 .116-.077.193-.192.193z" transform="translate(1 1) translate(33.803 4.985)"/>
|
||||||
|
<path fill="#FFF" d="M57.34 4.746c-.348-.347-.811-.54-1.39-.54-.579 0-1.08.154-1.466.501-.425.31-.656.81-.733 1.467h4.167c-.039-.618-.232-1.08-.579-1.428m1.544 2.315h-5.171c.039.81.309 1.39.772 1.852.463.425 1.003.656 1.582.656.578 0 1.08-.115 1.505-.386.424-.231.694-.501.849-.733.077-.077.154-.115.27-.038l.385.386c.039.038.078.115.078.154 0 .039-.039.077-.078.154-.038.077-.115.155-.27.309-.154.154-.308.309-.578.502-.27.192-.58.347-.965.463-.386.115-.81.193-1.235.193-1.003 0-1.852-.348-2.508-1.004-.656-.656-1.003-1.543-1.003-2.662s.347-2.007 1.003-2.662c.656-.656 1.505-.965 2.47-.965.964 0 1.736.309 2.276.926.54.617.85 1.466.85 2.547v.115c-.04.116-.078.193-.232.193" transform="translate(1 1) translate(33.803 4.985)"/>
|
||||||
|
<path stroke="#FFF" stroke-width=".75" d="M57.34 4.746c-.348-.347-.811-.54-1.39-.54-.579 0-1.08.154-1.466.501-.425.31-.656.81-.733 1.467h4.167c-.039-.618-.232-1.08-.579-1.428zm1.543 2.315h-5.171c.039.81.309 1.39.772 1.852.463.425 1.003.656 1.582.656.578 0 1.08-.115 1.505-.386.424-.231.694-.501.849-.733.077-.077.154-.115.27-.038l.385.386c.039.038.078.115.078.154 0 .039-.039.077-.078.154-.038.077-.115.155-.27.309-.154.154-.308.309-.578.502-.27.192-.58.347-.965.463-.386.115-.81.193-1.235.193-1.003 0-1.852-.348-2.508-1.004-.656-.656-1.003-1.543-1.003-2.662s.347-2.007 1.003-2.662c.656-.656 1.505-.965 2.47-.965.964 0 1.736.309 2.276.926.54.617.85 1.466.85 2.547v.115c-.04.116-.078.193-.232.193z" transform="translate(1 1) translate(33.803 4.985)"/>
|
||||||
|
<path fill="#FFF" d="M65.25 7.138h-1.12c-.54 0-1.003.04-1.389.078-.347.038-.694.193-.926.385-.27.193-.386.464-.386.81 0 .348.155.618.425.811.27.193.617.309 1.08.309.926 0 1.698-.425 2.354-1.235l-.039-1.158zm2.006 3.319H66.02c-.424 0-.617-.193-.617-.618v-.733h-.039c-.694.965-1.62 1.467-2.817 1.467-.694 0-1.234-.193-1.698-.54-.424-.348-.656-.85-.656-1.467 0-.772.348-1.35 1.042-1.736.695-.386 1.621-.54 2.817-.54h1.196v-.155c0-1.196-.617-1.813-1.852-1.813-.578 0-1.003.038-1.273.154-.27.116-.425.193-.54.27-.116.077-.193.116-.232.154-.154.116-.231.116-.309.04l-.385-.503c-.078-.077-.04-.154.038-.231.154-.232.502-.425 1.003-.617.502-.193 1.12-.31 1.814-.31 2.006 0 3.01.966 3.01 2.856v3.319h.81c.116 0 .193.038.193.154v.617c-.077.155-.154.232-.27.232z" transform="translate(1 1) translate(33.803 4.985)"/>
|
||||||
|
<path stroke="#FFF" stroke-width=".75" d="M65.25 7.138h-1.12c-.54 0-1.003.04-1.389.078-.347.038-.694.193-.926.385-.27.193-.386.464-.386.81 0 .348.155.618.425.811.27.193.617.309 1.08.309.926 0 1.698-.425 2.354-1.235l-.039-1.158zm2.006 3.319H66.02c-.424 0-.617-.193-.617-.618v-.733h-.039c-.694.965-1.62 1.467-2.817 1.467-.694 0-1.234-.193-1.698-.54-.424-.348-.656-.85-.656-1.467 0-.772.348-1.35 1.042-1.736.695-.386 1.621-.54 2.817-.54h1.196v-.155c0-1.196-.617-1.813-1.852-1.813-.578 0-1.003.038-1.273.154-.27.116-.425.193-.54.27-.116.077-.193.116-.232.154-.154.116-.231.116-.309.04l-.385-.503c-.078-.077-.04-.154.038-.231.154-.232.502-.425 1.003-.617.502-.193 1.12-.31 1.814-.31 2.006 0 3.01.966 3.01 2.856v3.319h.81c.116 0 .193.038.193.154v.617c-.077.155-.154.232-.27.232z" transform="translate(1 1) translate(33.803 4.985)"/>
|
||||||
|
<path fill="#FFF" d="M71.385 10.457h-2.74c-.116 0-.193-.039-.193-.155v-.617c0-.116.077-.154.193-.154h.772V1.08h-.772c-.116 0-.193-.04-.193-.155V.309c0-.116.077-.155.193-.155h1.582c.27 0 .386.116.386.348v8.952h.772c.115 0 .192.038.192.154v.617c0 .155-.077.232-.192.232" transform="translate(1 1) translate(33.803 4.985)"/>
|
||||||
|
<path stroke="#FFF" stroke-width=".75" d="M71.385 10.457h-2.74c-.116 0-.193-.039-.193-.155v-.617c0-.116.077-.154.193-.154h.772V1.08h-.772c-.116 0-.193-.04-.193-.155V.309c0-.116.077-.155.193-.155h1.582c.27 0 .386.116.386.348v8.952h.772c.115 0 .192.038.192.154v.617c0 .155-.077.232-.192.232z" transform="translate(1 1) translate(33.803 4.985)"/>
|
||||||
|
<path fill="#FFF" d="M77.404 9.724c.039.038.039.077.039.116 0 .038-.04.077-.078.154-.424.386-.964.54-1.582.54-.617 0-1.157-.193-1.582-.617-.424-.425-.617-1.004-.617-1.814V4.322h-.81c-.155 0-.193-.078-.193-.193V3.55c0-.116.077-.193.193-.193h.81V1.89c0-.155.077-.193.193-.193h.81c.116 0 .193.077.193.193v1.466h1.814c.154 0 .193.077.193.193v.579c0 .115-.078.193-.193.193H74.78v3.743c0 .462.116.848.309 1.08.193.231.501.347.849.347.347 0 .617-.077.81-.27.077-.077.193-.077.27 0l.386.502z" transform="translate(1 1) translate(33.803 4.985)"/>
|
||||||
|
<path stroke="#FFF" stroke-width=".75" d="M77.404 9.724c.039.038.039.077.039.116 0 .038-.04.077-.078.154-.424.386-.964.54-1.582.54-.617 0-1.157-.193-1.582-.617-.424-.425-.617-1.004-.617-1.814V4.322h-.81c-.155 0-.193-.078-.193-.193V3.55c0-.116.077-.193.193-.193h.81V1.89c0-.155.077-.193.193-.193h.81c.116 0 .193.077.193.193v1.466h1.814c.154 0 .193.077.193.193v.579c0 .115-.078.193-.193.193H74.78v3.743c0 .462.116.848.309 1.08.193.231.501.347.849.347.347 0 .617-.077.81-.27.077-.077.193-.077.27 0l.386.502z" transform="translate(1 1) translate(33.803 4.985)"/>
|
||||||
|
<path fill="#FFF" d="M85.893 10.457h-1.35c-.425 0-.618-.193-.618-.618V6.058c0-.54-.154-1.003-.386-1.312-.27-.309-.656-.463-1.157-.463-.386 0-.695.077-.888.193-.193.116-.347.231-.463.309-.116.077-.193.193-.27.27-.116.154-.193.309-.309.463v3.936h.772c.116 0 .193.038.193.154v.617c0 .116-.077.155-.193.155h-2.74c-.115 0-.193-.039-.193-.155v-.617c0-.116.078-.154.194-.154h.771V1.08h-.771c-.116 0-.194-.038-.194-.154V.309c0-.116.078-.155.194-.155h1.813c.116 0 .193.039.193.155V3.51c0 .31 0 .618-.039.888h.039c.463-.733 1.196-1.12 2.238-1.12.772 0 1.35.232 1.813.695.463.463.656 1.042.656 1.814v3.704h.81c.116 0 .194.039.194.155v.617c-.116.116-.193.193-.309.193" transform="translate(1 1) translate(33.803 4.985)"/>
|
||||||
|
<path stroke="#FFF" stroke-width=".75" d="M85.893 10.457h-1.35c-.425 0-.618-.193-.618-.618V6.058c0-.54-.154-1.003-.386-1.312-.27-.309-.656-.463-1.157-.463-.386 0-.695.077-.888.193-.193.116-.347.231-.463.309-.116.077-.193.193-.27.27-.116.154-.193.309-.309.463v3.936h.772c.116 0 .193.038.193.154v.617c0 .116-.077.155-.193.155h-2.74c-.115 0-.193-.039-.193-.155v-.617c0-.116.078-.154.194-.154h.771V1.08h-.771c-.116 0-.194-.038-.194-.154V.309c0-.116.078-.155.194-.155h1.813c.116 0 .193.039.193.155V3.51c0 .31 0 .618-.039.888h.039c.463-.733 1.196-1.12 2.238-1.12.772 0 1.35.232 1.813.695.463.463.656 1.042.656 1.814v3.704h.81c.116 0 .194.039.194.155v.617c-.116.116-.193.193-.309.193z" transform="translate(1 1) translate(33.803 4.985)"/>
|
||||||
|
<g>
|
||||||
|
<path fill="#FFF" d="M3.627 10.302H.193c-.116 0-.193-.038-.193-.154v-.733c0-.116.077-.154.193-.154h1.08V1.042H.193C.077 1.042 0 1.003 0 .888V.154C0 .04.077 0 .193 0h3.434c.116 0 .193.039.193.154v.734c0 .115-.077.154-.193.154H2.508v8.18h1.119c.116 0 .193.039.193.154v.734c0 .115-.077.192-.193.192" transform="translate(1 1) translate(33.803 4.985) translate(91.924 .113)"/>
|
||||||
|
<path stroke="#FFF" stroke-width=".75" d="M3.627 10.302H.193c-.116 0-.193-.038-.193-.154v-.733c0-.116.077-.154.193-.154h1.08V1.042H.193C.077 1.042 0 1.003 0 .888V.154C0 .04.077 0 .193 0h3.434c.116 0 .193.039.193.154v.734c0 .115-.077.154-.193.154H2.508v8.18h1.119c.116 0 .193.039.193.154v.734c0 .115-.077.192-.193.192z" transform="translate(1 1) translate(33.803 4.985) translate(91.924 .113)"/>
|
||||||
|
<path fill="#FFF" d="M17.094 10.302h-1.35c-.232 0-.387-.038-.464-.154-.116-.077-.154-.231-.154-.463V5.904c0-.54-.116-1.004-.386-1.312-.27-.309-.656-.463-1.158-.463-.771 0-1.35.347-1.775 1.003.039.154.077.347.077.579v3.627h.81c.117 0 .194.038.194.154v.618c0 .115-.039.154-.193.154h-2.74c-.116 0-.193-.039-.193-.154v-.618c0-.116.077-.154.193-.154h.772V5.904c0-.54-.154-1.004-.386-1.312-.27-.309-.656-.463-1.158-.463-.385 0-.694.077-.887.193-.193.116-.347.231-.463.308-.116.078-.193.193-.27.27-.116.155-.193.31-.309.464v3.935h.695c.116 0 .193.039.193.155v.617c0 .116-.039.154-.193.154H5.248c-.116 0-.193-.038-.193-.154v-.617c0-.116.077-.155.193-.155h.771V4.206h-.771c-.116 0-.193-.039-.193-.154v-.618c0-.116.077-.154.193-.154h1.389c.154 0 .27.038.347.077.077.077.116.154.116.347l-.039.734H7.1c.501-.85 1.273-1.235 2.315-1.235s1.775.386 2.16 1.196c.541-.81 1.313-1.196 2.316-1.196.772 0 1.35.231 1.775.694.424.463.656 1.042.656 1.814v3.704h.81c.116 0 .193.039.193.154v.618c-.038.038-.116.115-.231.115" transform="translate(1 1) translate(33.803 4.985) translate(91.924 .113)"/>
|
||||||
|
<path stroke="#FFF" stroke-width=".75" d="M17.094 10.302h-1.35c-.232 0-.387-.038-.464-.154-.116-.077-.154-.231-.154-.463V5.904c0-.54-.116-1.004-.386-1.312-.27-.309-.656-.463-1.158-.463-.771 0-1.35.347-1.775 1.003.039.154.077.347.077.579v3.627h.81c.117 0 .194.038.194.154v.618c0 .115-.039.154-.193.154h-2.74c-.116 0-.193-.039-.193-.154v-.618c0-.116.077-.154.193-.154h.772V5.904c0-.54-.154-1.004-.386-1.312-.27-.309-.656-.463-1.158-.463-.385 0-.694.077-.887.193-.193.116-.347.231-.463.308-.116.078-.193.193-.27.27-.116.155-.193.31-.309.464v3.935h.695c.116 0 .193.039.193.155v.617c0 .116-.039.154-.193.154H5.248c-.116 0-.193-.038-.193-.154v-.617c0-.116.077-.155.193-.155h.771V4.206h-.771c-.116 0-.193-.039-.193-.154v-.618c0-.116.077-.154.193-.154h1.389c.154 0 .27.038.347.077.077.077.116.154.116.347l-.039.734H7.1c.501-.85 1.273-1.235 2.315-1.235s1.775.386 2.16 1.196c.541-.81 1.313-1.196 2.316-1.196.772 0 1.35.231 1.775.694.424.463.656 1.042.656 1.814v3.704h.81c.116 0 .193.039.193.154v.618c-.038.038-.116.115-.231.115z" transform="translate(1 1) translate(33.803 4.985) translate(91.924 .113)"/>
|
||||||
|
<path fill="#FFF" d="M23.267 6.984h-1.119c-.54 0-1.003.039-1.389.077-.347.039-.694.193-.926.386-.27.193-.386.463-.386.81 0 .348.155.618.425.81.27.194.617.31 1.08.31.926 0 1.698-.425 2.354-1.235l-.039-1.158zm2.007 3.318h-1.235c-.424 0-.617-.192-.617-.617v-.733h-.039c-.694.965-1.62 1.466-2.817 1.466-.694 0-1.234-.193-1.697-.54-.425-.347-.656-.849-.656-1.466 0-.772.347-1.35 1.042-1.736.694-.387 1.62-.54 2.816-.54h1.196V5.98c0-1.196-.617-1.814-1.852-1.814-.579 0-1.003.04-1.273.155-.27.115-.425.193-.54.27-.116.077-.193.115-.232.154-.154.116-.231.116-.309.039l-.385-.502c-.077-.077-.039-.154.038-.231.155-.232.502-.425 1.003-.618.502-.193 1.12-.309 1.814-.309 2.007 0 3.01.965 3.01 2.856v3.318h.81c.116 0 .193.039.193.155v.617c-.077.154-.154.231-.27.231z" transform="translate(1 1) translate(33.803 4.985) translate(91.924 .113)"/>
|
||||||
|
<path stroke="#FFF" stroke-width=".75" d="M23.267 6.984h-1.119c-.54 0-1.003.039-1.389.077-.347.039-.694.193-.926.386-.27.193-.386.463-.386.81 0 .348.155.618.425.81.27.194.617.31 1.08.31.926 0 1.698-.425 2.354-1.235l-.039-1.158zm2.007 3.318h-1.235c-.424 0-.617-.192-.617-.617v-.733h-.039c-.694.965-1.62 1.466-2.817 1.466-.694 0-1.234-.193-1.697-.54-.425-.347-.656-.849-.656-1.466 0-.772.347-1.35 1.042-1.736.694-.387 1.62-.54 2.816-.54h1.196V5.98c0-1.196-.617-1.814-1.852-1.814-.579 0-1.003.04-1.273.155-.27.115-.425.193-.54.27-.116.077-.193.115-.232.154-.154.116-.231.116-.309.039l-.385-.502c-.077-.077-.039-.154.038-.231.155-.232.502-.425 1.003-.618.502-.193 1.12-.309 1.814-.309 2.007 0 3.01.965 3.01 2.856v3.318h.81c.116 0 .193.039.193.155v.617c-.077.154-.154.231-.27.231z" transform="translate(1 1) translate(33.803 4.985) translate(91.924 .113)"/>
|
||||||
|
<path fill="#FFF" d="M27.55 6.753c0 .81.193 1.427.618 1.929.424.463.965.733 1.582.733.617 0 1.158-.231 1.582-.733.425-.502.617-1.12.617-1.93 0-.81-.192-1.427-.617-1.929-.424-.501-.926-.694-1.582-.694-.617 0-1.158.231-1.582.733-.386.463-.617 1.119-.617 1.89m2.122 6.908c-.463 0-.849-.039-1.08-.116-.232-.077-.425-.116-.58-.193-.154-.039-.27-.116-.385-.155-.116-.038-.193-.115-.309-.154-.424-.27-.579-.463-.463-.617l.386-.54c.038-.077.154-.077.231 0l.078.077c.038.038.115.077.154.116.038.038.116.077.231.154.078.038.193.116.31.154.115.039.23.077.385.116.309.077.656.116 1.042.116.694 0 1.235-.155 1.659-.502.425-.347.617-.849.617-1.543v-1.66c-.192.386-.463.734-.848 1.042-.386.31-.888.425-1.505.425-.965 0-1.775-.347-2.354-1.003-.579-.695-.888-1.582-.888-2.663 0-1.08.309-1.968.888-2.624.579-.656 1.389-1.003 2.354-1.003.578 0 1.08.154 1.505.424.424.27.694.618.887 1.004h.039v-1.12c0-.115.038-.154.192-.154h1.698c.116 0 .193.039.193.155v.617c0 .116-.038.154-.193.154h-.771v6.367c0 1.042-.31 1.814-.927 2.315-.694.54-1.504.81-2.546.81" transform="translate(1 1) translate(33.803 4.985) translate(91.924 .113)"/>
|
||||||
|
<path stroke="#FFF" stroke-width=".75" d="M27.55 6.753c0 .81.193 1.427.618 1.929.424.463.965.733 1.582.733.617 0 1.158-.231 1.582-.733.425-.502.617-1.12.617-1.93 0-.81-.192-1.427-.617-1.929-.424-.501-.926-.694-1.582-.694-.617 0-1.158.231-1.582.733-.386.463-.617 1.119-.617 1.89zm2.123 6.907c-.463 0-.849-.039-1.08-.116-.232-.077-.425-.116-.58-.193-.154-.039-.27-.116-.385-.155-.116-.038-.193-.115-.309-.154-.424-.27-.579-.463-.463-.617l.386-.54c.038-.077.154-.077.231 0l.078.077c.038.038.115.077.154.116.038.038.116.077.231.154.078.038.193.116.31.154.115.039.23.077.385.116.309.077.656.116 1.042.116.694 0 1.235-.155 1.659-.502.425-.347.617-.849.617-1.543v-1.66c-.192.386-.463.734-.848 1.042-.386.31-.888.425-1.505.425-.965 0-1.775-.347-2.354-1.003-.579-.695-.888-1.582-.888-2.663 0-1.08.309-1.968.888-2.624.579-.656 1.389-1.003 2.354-1.003.578 0 1.08.154 1.505.424.424.27.694.618.887 1.004h.039v-1.12c0-.115.038-.154.192-.154h1.698c.116 0 .193.039.193.155v.617c0 .116-.038.154-.193.154h-.771v6.367c0 1.042-.31 1.814-.927 2.315-.694.54-1.504.81-2.546.81z" transform="translate(1 1) translate(33.803 4.985) translate(91.924 .113)"/>
|
||||||
|
<path fill="#FFF" d="M37.274 1.08c.155.155.232.348.232.58 0 .23-.077.424-.232.54-.154.154-.347.231-.579.231-.231 0-.424-.077-.578-.232-.155-.154-.232-.347-.232-.54 0-.231.077-.424.232-.579.154-.154.347-.23.578-.23.232 0 .425.076.58.23m.81 9.223h-2.74c-.116 0-.193-.039-.193-.155v-.617c0-.116.077-.155.193-.155h.772V4.167h-.772c-.116 0-.193-.038-.193-.154v-.617c0-.116.077-.155.193-.155h1.814c.115 0 .192.039.192.155v5.942h.772c.116 0 .193.038.193.154v.618c-.039.115-.077.193-.231.193" transform="translate(1 1) translate(33.803 4.985) translate(91.924 .113)"/>
|
||||||
|
<path stroke="#FFF" stroke-width=".75" d="M37.274 1.08c.155.155.232.348.232.58 0 .23-.077.424-.232.54-.154.154-.347.231-.579.231-.231 0-.424-.077-.578-.232-.155-.154-.232-.347-.232-.54 0-.231.077-.424.232-.579.154-.154.347-.23.578-.23.232 0 .425.076.58.23zm.81 9.223h-2.739c-.116 0-.193-.039-.193-.155v-.617c0-.116.077-.155.193-.155h.772V4.167h-.772c-.116 0-.193-.038-.193-.154v-.617c0-.116.077-.155.193-.155h1.814c.115 0 .192.039.192.155v5.942h.772c.116 0 .193.038.193.154v.618c-.039.115-.077.193-.231.193z" transform="translate(1 1) translate(33.803 4.985) translate(91.924 .113)"/>
|
||||||
|
<path fill="#FFF" d="M47.037 10.302h-1.351c-.424 0-.617-.192-.617-.617V5.904c0-.54-.155-1.004-.386-1.312-.27-.309-.656-.463-1.158-.463-.733 0-1.312.309-1.736.926-.039.039-.116.154-.193.309v3.935h.772c.115 0 .193.039.193.155v.617c0 .116-.078.154-.193.154h-2.74c-.116 0-.193-.038-.193-.154v-.617c0-.116.077-.155.193-.155h.772V4.206h-.772c-.116 0-.193-.039-.193-.154v-.618c0-.116.077-.154.193-.154h1.698c.116 0 .193.038.193.154l-.039.965h.039c.501-.849 1.273-1.235 2.315-1.235.772 0 1.35.232 1.814.695.462.463.655 1.041.655 1.813v3.704h.81c.116 0 .194.04.194.155v.617c-.116.077-.155.154-.27.154" transform="translate(1 1) translate(33.803 4.985) translate(91.924 .113)"/>
|
||||||
|
<path stroke="#FFF" stroke-width=".75" d="M47.037 10.302h-1.351c-.424 0-.617-.192-.617-.617V5.904c0-.54-.155-1.004-.386-1.312-.27-.309-.656-.463-1.158-.463-.733 0-1.312.309-1.736.926-.039.039-.116.154-.193.309v3.935h.772c.115 0 .193.039.193.155v.617c0 .116-.078.154-.193.154h-2.74c-.116 0-.193-.038-.193-.154v-.617c0-.116.077-.155.193-.155h.772V4.206h-.772c-.116 0-.193-.039-.193-.154v-.618c0-.116.077-.154.193-.154h1.698c.116 0 .193.038.193.154l-.039.965h.039c.501-.849 1.273-1.235 2.315-1.235.772 0 1.35.232 1.814.695.462.463.655 1.041.655 1.813v3.704h.81c.116 0 .194.04.194.155v.617c-.116.077-.155.154-.27.154z" transform="translate(1 1) translate(33.803 4.985) translate(91.924 .113)"/>
|
||||||
|
<path fill="#FFF" d="M49.197 6.753c0 .81.193 1.427.618 1.929.424.463.965.733 1.582.733.617 0 1.157-.231 1.582-.733.424-.502.617-1.12.617-1.93 0-.81-.193-1.427-.617-1.929-.425-.501-.926-.694-1.582-.694-.617 0-1.158.231-1.582.733-.425.501-.618 1.119-.618 1.89m2.161 6.908c-.463 0-.849-.039-1.08-.116-.232-.077-.425-.116-.579-.193-.154-.039-.27-.116-.386-.155-.116-.038-.193-.115-.308-.154-.425-.27-.58-.463-.464-.617l.386-.54c.039-.077.155-.077.232 0l.077.077c.039.038.116.077.154.116.039.038.116.077.232.154.077.038.193.116.309.154.115.039.231.077.385.116.31.077.656.116 1.042.116.695 0 1.235-.155 1.66-.502.424-.347.617-.849.617-1.543v-1.66c-.193.386-.463.734-.85 1.042-.385.31-.887.425-1.504.425-.965 0-1.775-.347-2.354-1.003-.578-.695-.887-1.582-.887-2.663 0-1.08.309-1.968.887-2.624.58-.656 1.39-1.003 2.354-1.003.579 0 1.08.154 1.505.424.424.27.695.618.887 1.004h.039v-1.12c0-.115.039-.154.193-.154h1.698c.115 0 .193.039.193.155v.617c0 .116-.039.154-.193.154h-.772v6.367c0 1.042-.309 1.814-.926 2.315-.733.54-1.544.81-2.547.81" transform="translate(1 1) translate(33.803 4.985) translate(91.924 .113)"/>
|
||||||
|
<path stroke="#FFF" stroke-width=".75" d="M49.197 6.753c0 .81.193 1.427.618 1.929.424.463.965.733 1.582.733.617 0 1.157-.231 1.582-.733.424-.502.617-1.12.617-1.93 0-.81-.193-1.427-.617-1.929-.425-.501-.926-.694-1.582-.694-.617 0-1.158.231-1.582.733-.425.501-.618 1.119-.618 1.89zm2.161 6.907c-.463 0-.849-.039-1.08-.116-.232-.077-.425-.116-.579-.193-.154-.039-.27-.116-.386-.155-.116-.038-.193-.115-.308-.154-.425-.27-.58-.463-.464-.617l.386-.54c.039-.077.155-.077.232 0l.077.077c.039.038.116.077.154.116.039.038.116.077.232.154.077.038.193.116.309.154.115.039.231.077.385.116.31.077.656.116 1.042.116.695 0 1.235-.155 1.66-.502.424-.347.617-.849.617-1.543v-1.66c-.193.386-.463.734-.85 1.042-.385.31-.887.425-1.504.425-.965 0-1.775-.347-2.354-1.003-.578-.695-.887-1.582-.887-2.663 0-1.08.309-1.968.887-2.624.58-.656 1.39-1.003 2.354-1.003.579 0 1.08.154 1.505.424.424.27.695.618.887 1.004h.039v-1.12c0-.115.039-.154.193-.154h1.698c.115 0 .193.039.193.155v.617c0 .116-.039.154-.193.154h-.772v6.367c0 1.042-.309 1.814-.926 2.315-.733.54-1.544.81-2.547.81z" transform="translate(1 1) translate(33.803 4.985) translate(91.924 .113)"/>
|
||||||
|
<path fill="#FFF" d="M65.596 10.302h-3.974c-.116 0-.193-.038-.193-.154v-.733c0-.116.077-.154.193-.154h1.08V1.042h-1.08c-.116 0-.193-.039-.193-.154V.154c0-.115.077-.154.193-.154h7.37c.116 0 .193.039.193.154V2.47c0 .116-.077.194-.193.194h-.771c-.116 0-.194-.078-.194-.194V1.08h-4.09v3.55h3.396c.116 0 .193.039.193.155v.733c0 .116-.077.154-.193.154h-3.396v3.512h1.62c.117 0 .194.038.194.154v.733c.038.154-.039.231-.155.231" transform="translate(1 1) translate(33.803 4.985) translate(91.924 .113)"/>
|
||||||
|
<path stroke="#FFF" stroke-width=".75" d="M65.596 10.302h-3.974c-.116 0-.193-.038-.193-.154v-.733c0-.116.077-.154.193-.154h1.08V1.042h-1.08c-.116 0-.193-.039-.193-.154V.154c0-.115.077-.154.193-.154h7.37c.116 0 .193.039.193.154V2.47c0 .116-.077.194-.193.194h-.771c-.116 0-.194-.078-.194-.194V1.08h-4.09v3.55h3.396c.116 0 .193.039.193.155v.733c0 .116-.077.154-.193.154h-3.396v3.512h1.62c.117 0 .194.038.194.154v.733c.038.154-.039.231-.155.231z" transform="translate(1 1) translate(33.803 4.985) translate(91.924 .113)"/>
|
||||||
|
<path fill="#FFF" d="M70.535 6.753c0 .771.232 1.427.695 1.929.463.502 1.003.733 1.698.733.694 0 1.235-.231 1.698-.733.463-.502.694-1.12.694-1.93 0-.771-.231-1.427-.694-1.89-.463-.463-1.004-.733-1.66-.733-.656 0-1.196.231-1.697.733-.502.463-.734 1.08-.734 1.89m4.94 2.625c-.656.656-1.544 1.003-2.586 1.003-1.042 0-1.89-.347-2.585-1.003-.656-.656-1.003-1.544-1.003-2.663 0-1.119.347-1.968 1.003-2.624.695-.656 1.544-.965 2.547-.965 1.003 0 1.89.348 2.546 1.004.656.656 1.004 1.543 1.004 2.624.077 1.08-.232 1.968-.926 2.624" transform="translate(1 1) translate(33.803 4.985) translate(91.924 .113)"/>
|
||||||
|
<path stroke="#FFF" stroke-width=".75" d="M70.535 6.753c0 .771.232 1.427.695 1.929.463.502 1.003.733 1.698.733.694 0 1.235-.231 1.698-.733.463-.502.694-1.12.694-1.93 0-.771-.231-1.427-.694-1.89-.463-.463-1.004-.733-1.66-.733-.656 0-1.196.231-1.697.733-.502.463-.734 1.08-.734 1.89zm4.94 2.624c-.656.656-1.544 1.003-2.586 1.003-1.042 0-1.89-.347-2.585-1.003-.656-.656-1.003-1.544-1.003-2.663 0-1.119.347-1.968 1.003-2.624.695-.656 1.544-.965 2.547-.965 1.003 0 1.89.348 2.546 1.004.656.656 1.004 1.543 1.004 2.624.077 1.08-.232 1.968-.926 2.624z" transform="translate(1 1) translate(33.803 4.985) translate(91.924 .113)"/>
|
||||||
|
<path fill="#FFF" d="M84.812 10.302h-1.234c-.425 0-.618-.192-.618-.617v-.54h-.038c-.386.81-1.158 1.235-2.238 1.235-.772 0-1.39-.232-1.814-.695-.424-.463-.656-1.08-.694-1.813V4.167h-.81c-.116 0-.194-.038-.194-.154v-.617c0-.116.039-.155.193-.155h1.39c.424 0 .617.193.617.618V7.64c0 .54.154 1.003.386 1.312.27.309.656.463 1.196.463.695 0 1.234-.27 1.659-.772.193-.231.27-.385.27-.424V4.167h-.772c-.115 0-.193-.038-.193-.154v-.617c0-.116.04-.155.193-.155h1.775c.116 0 .193.039.193.155v5.942h.81c.116 0 .194.038.194.154v.618c-.078.115-.155.192-.27.192" transform="translate(1 1) translate(33.803 4.985) translate(91.924 .113)"/>
|
||||||
|
<path stroke="#FFF" stroke-width=".75" d="M84.812 10.302h-1.234c-.425 0-.618-.192-.618-.617v-.54h-.038c-.386.81-1.158 1.235-2.238 1.235-.772 0-1.39-.232-1.814-.695-.424-.463-.656-1.08-.694-1.813V4.167h-.81c-.116 0-.194-.038-.194-.154v-.617c0-.116.039-.155.193-.155h1.39c.424 0 .617.193.617.618V7.64c0 .54.154 1.003.386 1.312.27.309.656.463 1.196.463.695 0 1.234-.27 1.659-.772.193-.231.27-.385.27-.424V4.167h-.772c-.115 0-.193-.038-.193-.154v-.617c0-.116.04-.155.193-.155h1.775c.116 0 .193.039.193.155v5.942h.81c.116 0 .194.038.194.154v.618c-.078.115-.155.192-.27.192z" transform="translate(1 1) translate(33.803 4.985) translate(91.924 .113)"/>
|
||||||
|
<path fill="#FFF" d="M93.687 10.302h-1.35c-.425 0-.618-.192-.618-.617V5.904c0-.54-.154-1.004-.385-1.312-.27-.309-.656-.463-1.158-.463-.733 0-1.312.309-1.736.926-.04.039-.116.154-.193.309v3.935h.771c.116 0 .193.039.193.155v.617c0 .116-.077.154-.193.154h-2.74c-.115 0-.192-.038-.192-.154v-.617c0-.116.077-.155.193-.155h.772V4.206h-.772c-.116 0-.193-.039-.193-.154v-.618c0-.116.077-.154.193-.154h1.698c.115 0 .192.038.192.154l-.038.965h.038c.502-.849 1.274-1.235 2.316-1.235.771 0 1.35.232 1.813.695.463.463.656 1.041.656 1.813v3.704h.81c.116 0 .193.04.193.155v.617c-.077.077-.154.154-.27.154" transform="translate(1 1) translate(33.803 4.985) translate(91.924 .113)"/>
|
||||||
|
<path stroke="#FFF" stroke-width=".75" d="M93.687 10.302h-1.35c-.425 0-.618-.192-.618-.617V5.904c0-.54-.154-1.004-.385-1.312-.27-.309-.656-.463-1.158-.463-.733 0-1.312.309-1.736.926-.04.039-.116.154-.193.309v3.935h.771c.116 0 .193.039.193.155v.617c0 .116-.077.154-.193.154h-2.74c-.115 0-.192-.038-.192-.154v-.617c0-.116.077-.155.193-.155h.772V4.206h-.772c-.116 0-.193-.039-.193-.154v-.618c0-.116.077-.154.193-.154h1.698c.115 0 .192.038.192.154l-.038.965h.038c.502-.849 1.274-1.235 2.316-1.235.771 0 1.35.232 1.813.695.463.463.656 1.041.656 1.813v3.704h.81c.116 0 .193.04.193.155v.617c-.077.077-.154.154-.27.154z" transform="translate(1 1) translate(33.803 4.985) translate(91.924 .113)"/>
|
||||||
|
<path fill="#FFF" d="M95.77 6.714c0 .81.194 1.428.618 1.93.425.5.965.732 1.582.732.656 0 1.158-.231 1.583-.733.424-.501.617-1.119.617-1.929s-.193-1.428-.617-1.89c-.425-.464-.965-.695-1.583-.695-.617 0-1.157.231-1.582.694-.386.463-.617 1.12-.617 1.891m6.367 3.589h-1.235c-.232 0-.386-.039-.463-.155-.116-.077-.154-.231-.154-.463v-.81c-.155.386-.425.772-.85 1.042-.424.308-.925.463-1.543.463-.965 0-1.736-.348-2.353-1.004-.58-.655-.888-1.543-.888-2.623 0-1.08.309-1.968.888-2.624.578-.656 1.388-1.003 2.353-1.003.579 0 1.08.154 1.466.424.386.27.695.617.85 1.003V.965h-.811c-.116 0-.193-.039-.193-.155V.193c0-.116.077-.154.193-.154h1.775c.116 0 .193.038.193.154v9.183h.81c.116 0 .193.04.193.155v.617c-.077.077-.116.155-.231.155" transform="translate(1 1) translate(33.803 4.985) translate(91.924 .113)"/>
|
||||||
|
<path stroke="#FFF" stroke-width=".75" d="M95.77 6.714c0 .81.194 1.428.618 1.93.425.5.965.732 1.582.732.656 0 1.158-.231 1.583-.733.424-.501.617-1.119.617-1.929s-.193-1.428-.617-1.89c-.425-.464-.965-.695-1.583-.695-.617 0-1.157.231-1.582.694-.386.463-.617 1.12-.617 1.891zm6.368 3.589h-1.235c-.232 0-.386-.039-.463-.155-.116-.077-.154-.231-.154-.463v-.81c-.155.386-.425.772-.85 1.042-.424.308-.925.463-1.543.463-.965 0-1.736-.348-2.353-1.004-.58-.655-.888-1.543-.888-2.623 0-1.08.309-1.968.888-2.624.578-.656 1.388-1.003 2.353-1.003.579 0 1.08.154 1.466.424.386.27.695.617.85 1.003V.965h-.811c-.116 0-.193-.039-.193-.155V.193c0-.116.077-.154.193-.154h1.775c.116 0 .193.038.193.154v9.183h.81c.116 0 .193.04.193.155v.617c-.077.077-.116.155-.231.155z" transform="translate(1 1) translate(33.803 4.985) translate(91.924 .113)"/>
|
||||||
|
<path fill="#FFF" d="M108.466 6.984h-1.158c-.54 0-1.003.039-1.389.077-.347.039-.695.193-.926.386-.27.193-.386.463-.386.81 0 .348.116.618.425.81.308.194.617.31 1.08.31.926 0 1.698-.425 2.354-1.235V6.984zm2.006 3.318h-1.234c-.425 0-.618-.192-.618-.617v-.733h-.038c-.695.965-1.621 1.466-2.817 1.466-.695 0-1.235-.193-1.698-.54-.424-.347-.656-.849-.656-1.466 0-.772.347-1.35 1.042-1.736.694-.387 1.62-.54 2.817-.54h1.196V5.98c0-1.196-.618-1.814-1.852-1.814-.579 0-1.004.04-1.274.155-.27.115-.463.193-.54.27-.116.077-.193.115-.27.154-.154.116-.27.116-.309.039l-.385-.502c-.078-.077-.078-.154.038-.231.154-.232.502-.425 1.003-.618.502-.193 1.12-.309 1.814-.309 2.006 0 3.01.965 3.01 2.856v3.318h.81c.116 0 .193.039.193.155v.617c-.077.154-.116.231-.232.231z" transform="translate(1 1) translate(33.803 4.985) translate(91.924 .113)"/>
|
||||||
|
<path stroke="#FFF" stroke-width=".75" d="M108.466 6.984h-1.158c-.54 0-1.003.039-1.389.077-.347.039-.695.193-.926.386-.27.193-.386.463-.386.81 0 .348.116.618.425.81.308.194.617.31 1.08.31.926 0 1.698-.425 2.354-1.235V6.984zm2.006 3.318h-1.234c-.425 0-.618-.192-.618-.617v-.733h-.038c-.695.965-1.621 1.466-2.817 1.466-.695 0-1.235-.193-1.698-.54-.424-.347-.656-.849-.656-1.466 0-.772.347-1.35 1.042-1.736.694-.387 1.62-.54 2.817-.54h1.196V5.98c0-1.196-.618-1.814-1.852-1.814-.579 0-1.004.04-1.274.155-.27.115-.463.193-.54.27-.116.077-.193.115-.27.154-.154.116-.27.116-.309.039l-.385-.502c-.078-.077-.078-.154.038-.231.154-.232.502-.425 1.003-.618.502-.193 1.12-.309 1.814-.309 2.006 0 3.01.965 3.01 2.856v3.318h.81c.116 0 .193.039.193.155v.617c-.077.154-.116.231-.232.231z" transform="translate(1 1) translate(33.803 4.985) translate(91.924 .113)"/>
|
||||||
|
<path fill="#FFF" d="M116.106 9.57c.039.038.077.077.077.115 0 .039-.038.077-.077.155-.425.385-.965.54-1.582.54-.618 0-1.158-.193-1.582-.618-.425-.424-.618-1.003-.618-1.813V4.167h-.81c-.116 0-.193-.077-.193-.193v-.578c0-.116.077-.194.193-.194h.81V1.736c0-.154.078-.192.193-.192h.81c.117 0 .194.077.194.192v1.466h1.813c.116 0 .193.078.193.194v.578c0 .116-.077.193-.193.193h-1.813V7.91c0 .463.115.85.308 1.08.193.232.502.348.85.348.346 0 .617-.077.81-.27.077-.077.192-.077.27 0l.347.501z" transform="translate(1 1) translate(33.803 4.985) translate(91.924 .113)"/>
|
||||||
|
<path stroke="#FFF" stroke-width=".75" d="M116.106 9.57c.039.038.077.077.077.115 0 .039-.038.077-.077.155-.425.385-.965.54-1.582.54-.618 0-1.158-.193-1.582-.618-.425-.424-.618-1.003-.618-1.813V4.167h-.81c-.116 0-.193-.077-.193-.193v-.578c0-.116.077-.194.193-.194h.81V1.736c0-.154.078-.192.193-.192h.81c.117 0 .194.077.194.192v1.466h1.813c.116 0 .193.078.193.194v.578c0 .116-.077.193-.193.193h-1.813V7.91c0 .463.115.85.308 1.08.193.232.502.348.85.348.346 0 .617-.077.81-.27.077-.077.192-.077.27 0l.347.501z" transform="translate(1 1) translate(33.803 4.985) translate(91.924 .113)"/>
|
||||||
|
<path fill="#FFF" d="M119.077 1.08c.154.155.231.348.231.58 0 .23-.077.424-.231.54-.154.154-.347.231-.579.231-.231 0-.424-.077-.579-.232-.154-.154-.231-.347-.231-.54 0-.231.077-.424.231-.579.155-.154.348-.23.58-.23.23 0 .424.076.578.23m.81 9.223h-2.74c-.115 0-.192-.039-.192-.155v-.617c0-.116.077-.155.193-.155h.771V4.167h-.771c-.116 0-.193-.038-.193-.154v-.617c0-.116.077-.155.193-.155h1.813c.116 0 .193.039.193.155v5.942h.772c.116 0 .193.038.193.154v.618c-.039.115-.077.193-.232.193" transform="translate(1 1) translate(33.803 4.985) translate(91.924 .113)"/>
|
||||||
|
<path stroke="#FFF" stroke-width=".75" d="M119.077 1.08c.154.155.231.348.231.58 0 .23-.077.424-.231.54-.154.154-.347.231-.579.231-.231 0-.424-.077-.579-.232-.154-.154-.231-.347-.231-.54 0-.231.077-.424.231-.579.155-.154.348-.23.58-.23.23 0 .424.076.578.23zm.81 9.223h-2.74c-.115 0-.192-.039-.192-.155v-.617c0-.116.077-.155.193-.155h.771V4.167h-.771c-.116 0-.193-.038-.193-.154v-.617c0-.116.077-.155.193-.155h1.813c.116 0 .193.039.193.155v5.942h.772c.116 0 .193.038.193.154v.618c-.039.115-.077.193-.232.193z" transform="translate(1 1) translate(33.803 4.985) translate(91.924 .113)"/>
|
||||||
|
<path fill="#FFF" d="M122.202 6.753c0 .771.232 1.427.695 1.929.463.502 1.042.733 1.698.733.656 0 1.235-.231 1.698-.733.463-.502.694-1.12.694-1.93 0-.771-.231-1.427-.694-1.89-.463-.463-1.042-.733-1.66-.733-.617 0-1.196.231-1.697.733-.502.463-.734 1.08-.734 1.89m4.978 2.625c-.656.656-1.543 1.003-2.585 1.003s-1.89-.347-2.585-1.003c-.695-.656-1.004-1.544-1.004-2.663 0-1.119.348-1.968 1.042-2.624.695-.656 1.544-.965 2.585-.965 1.042 0 1.891.348 2.547 1.004.656.656 1.003 1.543 1.003 2.624 0 1.08-.347 1.968-1.003 2.624" transform="translate(1 1) translate(33.803 4.985) translate(91.924 .113)"/>
|
||||||
|
<path stroke="#FFF" stroke-width=".75" d="M122.202 6.753c0 .771.232 1.427.695 1.929.463.502 1.042.733 1.698.733.656 0 1.235-.231 1.698-.733.463-.502.694-1.12.694-1.93 0-.771-.231-1.427-.694-1.89-.463-.463-1.042-.733-1.66-.733-.617 0-1.196.231-1.697.733-.502.463-.734 1.08-.734 1.89zm4.978 2.624c-.656.656-1.543 1.003-2.585 1.003s-1.89-.347-2.585-1.003c-.695-.656-1.004-1.544-1.004-2.663 0-1.119.348-1.968 1.042-2.624.695-.656 1.544-.965 2.585-.965 1.042 0 1.891.348 2.547 1.004.656.656 1.003 1.543 1.003 2.624 0 1.08-.347 1.968-1.003 2.624z" transform="translate(1 1) translate(33.803 4.985) translate(91.924 .113)"/>
|
||||||
|
<path fill="#FFF" d="M136.865 10.302h-1.35c-.424 0-.618-.192-.618-.617V5.904c0-.54-.115-1.004-.385-1.312-.27-.309-.656-.463-1.158-.463-.733 0-1.312.309-1.737.926-.038.039-.115.154-.192.309v3.935h.771c.116 0 .193.039.193.155v.617c0 .116-.077.154-.193.154h-2.74c-.115 0-.192-.038-.192-.154v-.617c0-.116.077-.155.193-.155h.771V4.206h-.771c-.116 0-.193-.039-.193-.154v-.618c0-.116.077-.154.193-.154h1.698c.115 0 .193.038.193.154l-.04.965h.04c.501-.849 1.273-1.235 2.315-1.235.771 0 1.35.232 1.813.695.463.463.656 1.041.656 1.813v3.704h.81c.116 0 .193.04.193.155v.617c-.077.077-.115.154-.27.154" transform="translate(1 1) translate(33.803 4.985) translate(91.924 .113)"/>
|
||||||
|
<path stroke="#FFF" stroke-width=".75" d="M136.865 10.302h-1.35c-.424 0-.618-.192-.618-.617V5.904c0-.54-.115-1.004-.385-1.312-.27-.309-.656-.463-1.158-.463-.733 0-1.312.309-1.737.926-.038.039-.115.154-.192.309v3.935h.771c.116 0 .193.039.193.155v.617c0 .116-.077.154-.193.154h-2.74c-.115 0-.192-.038-.192-.154v-.617c0-.116.077-.155.193-.155h.771V4.206h-.771c-.116 0-.193-.039-.193-.154v-.618c0-.116.077-.154.193-.154h1.698c.115 0 .193.038.193.154l-.04.965h.04c.501-.849 1.273-1.235 2.315-1.235.771 0 1.35.232 1.813.695.463.463.656 1.041.656 1.813v3.704h.81c.116 0 .193.04.193.155v.617c-.077.077-.115.154-.27.154z" transform="translate(1 1) translate(33.803 4.985) translate(91.924 .113)"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<g fill="#FFF" fill-rule="nonzero">
|
||||||
|
<path d="M22.85 21.43c0 .713-.58 1.294-1.293 1.294h-8.308c-.713 0-1.293-.58-1.293-1.293v-8.308c0-.713.58-1.293 1.293-1.293h8.308c.712 0 1.293.58 1.293 1.293v8.308zm-1.327-8.273h-8.24v8.24h8.24v-8.24zM22.85 8.937c0 .713-.58 1.293-1.293 1.293h-8.308c-.713 0-1.293-.58-1.293-1.293V.63c0-.712.58-1.292 1.293-1.292h8.308c.712 0 1.293.58 1.293 1.292v8.308zM21.523.663h-8.24v8.24h8.24V.663zM10.23 21.43c0 .713-.58 1.294-1.293 1.294H.63c-.712 0-1.292-.58-1.292-1.293v-8.308c0-.713.58-1.293 1.292-1.293h8.308c.713 0 1.293.58 1.293 1.293v8.308zm-1.327-8.273H.663v8.24h8.24v-8.24zM10.23 8.937c0 .713-.58 1.293-1.293 1.293H.63c-.712 0-1.292-.58-1.292-1.293V.63c0-.712.58-1.292 1.292-1.292h8.308c.713 0 1.293.58 1.293 1.292v8.308zm-1.327-.034V.663H.663v8.24h8.24z" transform="translate(1 1)"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 39 KiB |
@ -3,7 +3,7 @@ import React from 'react';
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import i18n from '@ohif/i18n';
|
import i18n from '@ohif/i18n';
|
||||||
import { I18nextProvider } from 'react-i18next';
|
import { I18nextProvider } from 'react-i18next';
|
||||||
import { BrowserRouter, HashRouter } from 'react-router-dom';
|
import { Router } from 'react-router-dom';
|
||||||
import {
|
import {
|
||||||
DialogProvider,
|
DialogProvider,
|
||||||
Modal,
|
Modal,
|
||||||
@ -12,7 +12,6 @@ import {
|
|||||||
ThemeWrapper,
|
ThemeWrapper,
|
||||||
ViewportDialogProvider,
|
ViewportDialogProvider,
|
||||||
ViewportGridProvider,
|
ViewportGridProvider,
|
||||||
HangingProtocolProvider,
|
|
||||||
CineProvider,
|
CineProvider,
|
||||||
} from '@ohif/ui';
|
} from '@ohif/ui';
|
||||||
// Viewer Project
|
// Viewer Project
|
||||||
@ -20,6 +19,7 @@ import {
|
|||||||
import { AppConfigProvider } from '@state';
|
import { AppConfigProvider } from '@state';
|
||||||
import createRoutes from './routes';
|
import createRoutes from './routes';
|
||||||
import appInit from './appInit.js';
|
import appInit from './appInit.js';
|
||||||
|
import history from './history'
|
||||||
|
|
||||||
// TODO: Temporarily for testing
|
// TODO: Temporarily for testing
|
||||||
import '@ohif/mode-longitudinal';
|
import '@ohif/mode-longitudinal';
|
||||||
@ -27,9 +27,7 @@ import '@ohif/mode-longitudinal';
|
|||||||
/**
|
/**
|
||||||
* ENV Variable to determine routing behavior
|
* ENV Variable to determine routing behavior
|
||||||
*/
|
*/
|
||||||
const Router = JSON.parse(process.env.USE_HASH_ROUTER)
|
const OHIFRouter = Router
|
||||||
? HashRouter
|
|
||||||
: BrowserRouter;
|
|
||||||
|
|
||||||
let commandsManager, extensionManager, servicesManager, hotkeysManager;
|
let commandsManager, extensionManager, servicesManager, hotkeysManager;
|
||||||
|
|
||||||
@ -59,32 +57,29 @@ function App({ config, defaultExtensions }) {
|
|||||||
UINotificationService,
|
UINotificationService,
|
||||||
UIViewportDialogService,
|
UIViewportDialogService,
|
||||||
ViewportGridService, // TODO: Should this be a "UI" Service?
|
ViewportGridService, // TODO: Should this be a "UI" Service?
|
||||||
HangingProtocolService,
|
|
||||||
CineService
|
CineService
|
||||||
} = servicesManager.services;
|
} = servicesManager.services;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppConfigProvider value={appConfigState}>
|
<AppConfigProvider value={appConfigState}>
|
||||||
<I18nextProvider i18n={i18n}>
|
<I18nextProvider i18n={i18n}>
|
||||||
<Router basename={routerBasename}>
|
<OHIFRouter basename={routerBasename} history={history}>
|
||||||
<ThemeWrapper>
|
<ThemeWrapper>
|
||||||
<ViewportGridProvider service={ViewportGridService}>
|
<ViewportGridProvider service={ViewportGridService}>
|
||||||
<HangingProtocolProvider service={HangingProtocolService}>
|
<ViewportDialogProvider service={UIViewportDialogService}>
|
||||||
<ViewportDialogProvider service={UIViewportDialogService}>
|
<CineProvider service={CineService}>
|
||||||
<CineProvider service={CineService}>
|
<SnackbarProvider service={UINotificationService}>
|
||||||
<SnackbarProvider service={UINotificationService}>
|
<DialogProvider service={UIDialogService}>
|
||||||
<DialogProvider service={UIDialogService}>
|
<ModalProvider modal={Modal} service={UIModalService}>
|
||||||
<ModalProvider modal={Modal} service={UIModalService}>
|
{appRoutes}
|
||||||
{appRoutes}
|
</ModalProvider>
|
||||||
</ModalProvider>
|
</DialogProvider>
|
||||||
</DialogProvider>
|
</SnackbarProvider>
|
||||||
</SnackbarProvider>
|
</CineProvider>
|
||||||
</CineProvider>
|
</ViewportDialogProvider>
|
||||||
</ViewportDialogProvider>
|
|
||||||
</HangingProtocolProvider>
|
|
||||||
</ViewportGridProvider>
|
</ViewportGridProvider>
|
||||||
</ThemeWrapper>
|
</ThemeWrapper>
|
||||||
</Router>
|
</OHIFRouter>
|
||||||
</I18nextProvider>
|
</I18nextProvider>
|
||||||
</AppConfigProvider>
|
</AppConfigProvider>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -81,6 +81,7 @@ function appInit(appConfigOrFunc, defaultExtensions) {
|
|||||||
// appConfig.modes.push(window.segmentationMode);
|
// appConfig.modes.push(window.segmentationMode);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
appConfig,
|
appConfig,
|
||||||
commandsManager,
|
commandsManager,
|
||||||
|
|||||||
@ -26,38 +26,38 @@ function ViewerViewportGrid(props) {
|
|||||||
HangingProtocolService,
|
HangingProtocolService,
|
||||||
} = servicesManager.services;
|
} = servicesManager.services;
|
||||||
|
|
||||||
// This is a placeholder for applying hanging protocols
|
|
||||||
// It probably shouldn't be done here
|
// Using Hanging protocol engine to match the displaysets
|
||||||
// For now it just hangs the first display set in the study in 1x1
|
|
||||||
// as sorted by SeriesNumber
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const { unsubscribe } = DisplaySetService.subscribe(
|
const { unsubscribe } = DisplaySetService.subscribe(
|
||||||
DisplaySetService.EVENTS.DISPLAY_SETS_ADDED,
|
DisplaySetService.EVENTS.DISPLAY_SETS_ADDED,
|
||||||
eventData => {
|
eventData => {
|
||||||
const { displaySetsAdded } = eventData;
|
const { displaySetsAdded } = eventData;
|
||||||
|
|
||||||
const data = HangingProtocolService.getState();
|
const [
|
||||||
|
matchDetails,
|
||||||
// TODO: Sometimes this is undefined?
|
hpAlreadyApplied,
|
||||||
const { hpAlreadyApplied } = data;
|
] = HangingProtocolService.getState();
|
||||||
|
|
||||||
|
if (!matchDetails.length) return;
|
||||||
// Match each viewport individually
|
// Match each viewport individually
|
||||||
const numViewports = numRows * numCols;
|
|
||||||
|
const numViewports = viewportGrid.numRows * viewportGrid.numCols;
|
||||||
for (let i = 0; i < numViewports; i++) {
|
for (let i = 0; i < numViewports; i++) {
|
||||||
if (hpAlreadyApplied[i] === true) {
|
if (hpAlreadyApplied[i] === true) {
|
||||||
return;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Temporary until matching is ported back over from the Meteor version.
|
// if current viewport doesn't have a match
|
||||||
const reqSeriesInstanceUID =
|
if (matchDetails[i] === undefined) return
|
||||||
data.hangingProtocol.stages[0].viewports[0].seriesMatchingRules[0]
|
|
||||||
.constraint.equals.value;
|
const { SeriesInstanceUID } = matchDetails[i];
|
||||||
const matchingDisplaySet = displaySetsAdded.find(ds => {
|
const matchingDisplaySet = displaySetsAdded.find(ds => {
|
||||||
return ds.SeriesInstanceUID === reqSeriesInstanceUID;
|
return ds.SeriesInstanceUID === SeriesInstanceUID;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!matchingDisplaySet) {
|
if (!matchingDisplaySet) {
|
||||||
return;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
viewportGridService.setDisplaysetForViewport({
|
viewportGridService.setDisplaysetForViewport({
|
||||||
@ -73,7 +73,23 @@ function ViewerViewportGrid(props) {
|
|||||||
return () => {
|
return () => {
|
||||||
unsubscribe();
|
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(() => {
|
useEffect(() => {
|
||||||
const { unsubscribe } = MeasurementService.subscribe(
|
const { unsubscribe } = MeasurementService.subscribe(
|
||||||
@ -82,16 +98,24 @@ function ViewerViewportGrid(props) {
|
|||||||
const referencedDisplaySetInstanceUID =
|
const referencedDisplaySetInstanceUID =
|
||||||
measurement.displaySetInstanceUID;
|
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 (
|
if (
|
||||||
viewports[viewportIndex].displaySetInstanceUID !==
|
viewportsDisplaySetInstanceUIDs.indexOf(
|
||||||
referencedDisplaySetInstanceUID
|
referencedDisplaySetInstanceUID
|
||||||
|
) > -1
|
||||||
) {
|
) {
|
||||||
viewportGridService.setDisplaysetForViewport({
|
return;
|
||||||
viewportIndex,
|
|
||||||
displaySetInstanceUID: referencedDisplaySetInstanceUID,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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;
|
return viewportPanes;
|
||||||
};
|
};
|
||||||
|
|
||||||
// const ViewportPanes = React.useMemo(getViewportPanes, [
|
if (!numCols || !numCols) {
|
||||||
// viewportComponents,
|
return null;
|
||||||
// activeViewportIndex,
|
}
|
||||||
// viewportGrid,
|
|
||||||
// ]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ViewportGrid numRows={numRows} numCols={numCols}>
|
<ViewportGrid numRows={numRows} numCols={numCols}>
|
||||||
|
|||||||
5
platform/viewer/src/history.js
Normal file
5
platform/viewer/src/history.js
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import { createBrowserHistory, createHashHistory } from 'history';
|
||||||
|
const useHashRouting = JSON.parse(process.env.USE_HASH_ROUTER);
|
||||||
|
const router = useHashRouting ? createHashHistory() : createBrowserHistory();
|
||||||
|
|
||||||
|
export default router;
|
||||||
@ -1,14 +1,57 @@
|
|||||||
import React, { useEffect } from 'react';
|
import React, { useEffect, useState, useRef } from 'react';
|
||||||
import { useParams } from 'react-router';
|
import { useParams } from 'react-router';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
// TODO: DicomMetadataStore should be injected?
|
// TODO: DicomMetadataStore should be injected?
|
||||||
import { DicomMetadataStore, utils } from '@ohif/core';
|
import { DicomMetadataStore } from '@ohif/core';
|
||||||
import { DragAndDropProvider, ImageViewerProvider } from '@ohif/ui';
|
import { DragAndDropProvider, ImageViewerProvider } from '@ohif/ui';
|
||||||
import { useQuery } from '@hooks';
|
import { useQuery } from '@hooks';
|
||||||
import ViewportGrid from '@components/ViewportGrid';
|
import ViewportGrid from '@components/ViewportGrid';
|
||||||
import Compose from './Compose';
|
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({
|
export default function ModeRoute({
|
||||||
location,
|
location,
|
||||||
@ -20,15 +63,25 @@ export default function ModeRoute({
|
|||||||
}) {
|
}) {
|
||||||
// Parse route params/querystring
|
// Parse route params/querystring
|
||||||
const query = useQuery();
|
const query = useQuery();
|
||||||
const queryStudyInstanceUIDs = query.get('StudyInstanceUIDs');
|
const params = useParams();
|
||||||
const { StudyInstanceUIDs: paramsStudyInstanceUIDs } = useParams();
|
|
||||||
const StudyInstanceUIDs = queryStudyInstanceUIDs || paramsStudyInstanceUIDs;
|
|
||||||
const StudyInstanceUIDsAsArray =
|
|
||||||
StudyInstanceUIDs && Array.isArray(StudyInstanceUIDs)
|
|
||||||
? StudyInstanceUIDs
|
|
||||||
: [StudyInstanceUIDs];
|
|
||||||
|
|
||||||
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) {
|
if (dataSourceName === undefined) {
|
||||||
dataSourceName = extensionManager.defaultDataSourceName;
|
dataSourceName = extensionManager.defaultDataSourceName;
|
||||||
@ -43,19 +96,6 @@ export default function ModeRoute({
|
|||||||
// Only handling one route per mode for now
|
// Only handling one route per mode for now
|
||||||
const route = mode.routes[0];
|
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
|
// For each extension, look up their context modules
|
||||||
// TODO: move to extension manager.
|
// TODO: move to extension manager.
|
||||||
let contextModules = [];
|
let contextModules = [];
|
||||||
@ -81,9 +121,52 @@ export default function ModeRoute({
|
|||||||
return ViewportGrid({ ...props, dataSource });
|
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(() => {
|
useEffect(() => {
|
||||||
if (!hotkeys) {
|
if (!hotkeys) {
|
||||||
console.warn('[hotkeys] No bindings defined for hotkeys hook!');
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -96,19 +179,61 @@ export default function ModeRoute({
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (!layoutTemplateData.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
// TODO: For some reason this is running before the Providers
|
// TODO: For some reason this is running before the Providers
|
||||||
// are calling setServiceImplementation
|
// are calling setServiceImplementationf
|
||||||
// TOOD -> iterate through services.
|
// TOOD -> iterate through services.
|
||||||
|
|
||||||
// Extension
|
// Extension
|
||||||
|
|
||||||
|
// Add SOPClassHandlers to a new SOPClassManager.
|
||||||
|
DisplaySetService.init(extensionManager, sopClassHandlers);
|
||||||
|
|
||||||
extensionManager.onModeEnter();
|
extensionManager.onModeEnter();
|
||||||
mode?.onModeEnter({ servicesManager, extensionManager });
|
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 () => {
|
return () => {
|
||||||
extensionManager.onModeExit();
|
extensionManager.onModeExit();
|
||||||
mode?.onModeExit({ servicesManager, extensionManager });
|
mode?.onModeExit({ servicesManager, extensionManager });
|
||||||
|
unsubscriptions.forEach(unsub => {
|
||||||
|
unsub();
|
||||||
|
});
|
||||||
};
|
};
|
||||||
}, [
|
}, [
|
||||||
mode,
|
mode,
|
||||||
@ -118,140 +243,35 @@ export default function ModeRoute({
|
|||||||
servicesManager,
|
servicesManager,
|
||||||
extensionManager,
|
extensionManager,
|
||||||
hotkeysManager,
|
hotkeysManager,
|
||||||
|
studyInstanceUIDs,
|
||||||
|
refresh,
|
||||||
|
hangingProtocols,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// This queries for series, but... What does it do with them?
|
const renderLayoutData = props => {
|
||||||
useEffect(() => {
|
const layoutTemplateModuleEntry = extensionManager.getModuleEntry(
|
||||||
// Add SOPClassHandlers to a new SOPClassManager.
|
layoutTemplateData.current.id
|
||||||
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 LayoutComponent = layoutTemplateModuleEntry.component;
|
||||||
|
|
||||||
StudyInstanceUIDsAsArray.forEach(StudyInstanceUID => {
|
return <LayoutComponent {...props} />;
|
||||||
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 (
|
return (
|
||||||
<ImageViewerProvider
|
<ImageViewerProvider
|
||||||
initialState={{ StudyInstanceUIDs: StudyInstanceUIDsAsArray }}
|
// initialState={{ StudyInstanceUIDs: StudyInstanceUIDs }}
|
||||||
|
StudyInstanceUIDs={studyInstanceUIDs}
|
||||||
|
// reducer={reducer}
|
||||||
>
|
>
|
||||||
<CombinedContextProvider>
|
<CombinedContextProvider>
|
||||||
<DragAndDropProvider>
|
<DragAndDropProvider>
|
||||||
<LayoutComponent
|
{layoutTemplateData.current &&
|
||||||
{...layoutTemplateData.props}
|
studyInstanceUIDs?.length &&
|
||||||
StudyInstanceUIDs={StudyInstanceUIDs}
|
studyInstanceUIDs[0] !== undefined &&
|
||||||
ViewportGridComp={ViewportGridWithDataSource}
|
renderLayoutData({
|
||||||
/>
|
...layoutTemplateData.current.props,
|
||||||
|
ViewportGridComp: ViewportGridWithDataSource,
|
||||||
|
})}
|
||||||
</DragAndDropProvider>
|
</DragAndDropProvider>
|
||||||
</CombinedContextProvider>
|
</CombinedContextProvider>
|
||||||
</ImageViewerProvider>
|
</ImageViewerProvider>
|
||||||
|
|||||||
@ -408,7 +408,7 @@ function WorkList({
|
|||||||
'h-screen': !hasStudies,
|
'h-screen': !hasStudies,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<Header isSticky menuOptions={menuOptions} isReturnEnabled={false} />
|
<Header isSticky menuOptions={menuOptions} isReturnEnabled={false} WhiteLabeling={appConfig.whiteLabeling} />
|
||||||
<StudyListFilter
|
<StudyListFilter
|
||||||
numOfStudies={pageNumber * resultsPerPage > 100 ? 101 : numOfStudies}
|
numOfStudies={pageNumber * resultsPerPage > 100 ? 101 : numOfStudies}
|
||||||
filtersMeta={filtersMeta}
|
filtersMeta={filtersMeta}
|
||||||
|
|||||||
140
yarn.lock
140
yarn.lock
@ -3729,10 +3729,10 @@ acorn-globals@^4.1.0:
|
|||||||
acorn "^6.0.1"
|
acorn "^6.0.1"
|
||||||
acorn-walk "^6.0.1"
|
acorn-walk "^6.0.1"
|
||||||
|
|
||||||
acorn-jsx@^5.0.0, acorn-jsx@^5.2.0:
|
acorn-jsx@^5.2.0:
|
||||||
version "5.3.1"
|
version "5.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.1.tgz#fc8661e11b7ac1539c47dbfea2e72b3af34d267b"
|
resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.2.0.tgz#4c66069173d6fdd68ed85239fc256226182b2ebe"
|
||||||
integrity sha512-K0Ptm/47OKfQRpNQ2J/oIN/3QYiK6FwW+eJbILhsdxh2WTLdl+30o8aGdTbm5JbffpFFAg/g+zi1E+jvJha5ng==
|
integrity sha512-HiUX/+K2YpkpJ+SzBffkM/AQ2YE03S0U1kjTLVpoJdhZMOWy8qvXVN9JdLqv2QsaQ6MPYQIuNmwD8zOiYUofLQ==
|
||||||
|
|
||||||
acorn-node@^1.6.1:
|
acorn-node@^1.6.1:
|
||||||
version "1.8.2"
|
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"
|
resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.7.4.tgz#3e8d8a9947d0599a1796d10225d7432f4a4acf5e"
|
||||||
integrity sha512-1D++VG7BhrtvQpNbBzovKNc1FLGGEE/oGe7b9xJm/RFHMBeUaUGpluV9RLjZa47YFdPcDAenEYuq9pQPcMdLJg==
|
integrity sha512-1D++VG7BhrtvQpNbBzovKNc1FLGGEE/oGe7b9xJm/RFHMBeUaUGpluV9RLjZa47YFdPcDAenEYuq9pQPcMdLJg==
|
||||||
|
|
||||||
acorn@^6.0.1, acorn@^6.0.7, acorn@^6.4.1:
|
acorn@^6.0.1, acorn@^6.4.1:
|
||||||
version "6.4.2"
|
version "6.4.1"
|
||||||
resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.4.2.tgz#35866fd710528e92de10cf06016498e47e39e1e6"
|
resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.4.1.tgz#531e58ba3f51b9dacb9a6646ca4debf5b14ca474"
|
||||||
integrity sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ==
|
integrity sha512-ZVA9k326Nwrj3Cj9jlh3wGFutC2ZornPNARZwsNYqQYgN0EsV2d53w5RN/co65Ohn4sUAUtb1rSUAOD6XN9idA==
|
||||||
|
|
||||||
acorn@^7.0.0, acorn@^7.1.0, acorn@^7.1.1:
|
acorn@^7.0.0, acorn@^7.1.0, acorn@^7.1.1:
|
||||||
version "7.4.1"
|
version "7.4.1"
|
||||||
@ -3857,7 +3857,17 @@ ajv@6.5.3:
|
|||||||
json-schema-traverse "^0.4.1"
|
json-schema-traverse "^0.4.1"
|
||||||
uri-js "^4.2.2"
|
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"
|
version "6.12.6"
|
||||||
resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4"
|
resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4"
|
||||||
integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==
|
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"
|
resolved "https://registry.yarnpkg.com/cornerstone-math/-/cornerstone-math-0.1.7.tgz#7c55536f02e7221b05fc49a4c780bb82c234ce39"
|
||||||
integrity sha512-svsDSoqLNFM9niCV2AtV2DNmRkYztT1v80lVdbA5hkgdGdpWg5/h2On794zbqBK+aI3hNOHufERDsI4wdXGknA==
|
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"
|
version "0.1.9"
|
||||||
resolved "https://registry.yarnpkg.com/cornerstone-math/-/cornerstone-math-0.1.9.tgz#7ce5509e8b9f465b01f7c548470725e7569859fc"
|
resolved "https://registry.yarnpkg.com/cornerstone-math/-/cornerstone-math-0.1.9.tgz#7ce5509e8b9f465b01f7c548470725e7569859fc"
|
||||||
integrity sha512-NxdooV73asEQgav1S+0e+a4K+W3CXJdLXyFkVN24qqCtmIpzZzwtw3F9KWPCekzSAJmbhtQ3HicOQj3d4vRtuw==
|
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"
|
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==
|
integrity sha512-bY2sGqyptzFBDLh/GMbAxfdJC+b0f23ME63FOE4+Jao0oZ3E1LEwFtWJX/1pGMJLiTtrSSern2CRM/g+dfc0eQ==
|
||||||
|
|
||||||
eslint-plugin-react-hooks@1.5.0:
|
eslint-plugin-react-hooks@4.2.0:
|
||||||
version "1.5.0"
|
version "4.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-1.5.0.tgz#cdd958cfff55bd5fa4f84db90d1490fb5ca4ae2b"
|
resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.2.0.tgz#8c229c268d468956334c943bb45fc860280f5556"
|
||||||
integrity sha512-iwDuWR2ReRgvJsNm8fXPtTKdg78IVQF8I4+am3ntztPf/+nPnWZfArFu6aXpaC75/iCYRrkqI8nPCYkxJstmpA==
|
integrity sha512-623WEiZJqxR7VdxFCKLI6d6LLpwJkGPYKODnkH3D7WpOG5KM8yWueBd8TLsNAetEJNF5iJmolaAKO3F8yzyVBQ==
|
||||||
|
|
||||||
eslint-plugin-react-hooks@^1.7.0:
|
eslint-plugin-react-hooks@^1.7.0:
|
||||||
version "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"
|
esrecurse "^4.3.0"
|
||||||
estraverse "^4.1.1"
|
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"
|
version "1.4.3"
|
||||||
resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-1.4.3.tgz#74fec7c54d0776b6f67e0251040b5806564e981f"
|
resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-1.4.3.tgz#74fec7c54d0776b6f67e0251040b5806564e981f"
|
||||||
integrity sha512-fbBN5W2xdY45KulGXmLHZ3c3FHfVYmKg0IrAKGOkT/464PQsx2UeIzfz1RmEci+KLm1bBaAzZAh8+/E+XAeZ8Q==
|
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"
|
resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz#30ebd1ef7c2fdff01c3a4f151044af25fab0523e"
|
||||||
integrity sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==
|
integrity sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==
|
||||||
|
|
||||||
eslint@5.16.0:
|
eslint@6.8.0, eslint@^6.8.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:
|
|
||||||
version "6.8.0"
|
version "6.8.0"
|
||||||
resolved "https://registry.yarnpkg.com/eslint/-/eslint-6.8.0.tgz#62262d6729739f9275723824302fb227c8c93ffb"
|
resolved "https://registry.yarnpkg.com/eslint/-/eslint-6.8.0.tgz#62262d6729739f9275723824302fb227c8c93ffb"
|
||||||
integrity sha512-K+Iayyo2LtyYhDSYwz5D5QdWw0hCacNzyq1Y821Xna2xSJj7cijoLLYmLxTQgcgZ9mC61nryMy9S7GRbYpI5Ig==
|
integrity sha512-K+Iayyo2LtyYhDSYwz5D5QdWw0hCacNzyq1Y821Xna2xSJj7cijoLLYmLxTQgcgZ9mC61nryMy9S7GRbYpI5Ig==
|
||||||
@ -8381,15 +8349,6 @@ eslint@^6.8.0:
|
|||||||
text-table "^0.2.0"
|
text-table "^0.2.0"
|
||||||
v8-compile-cache "^2.0.3"
|
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:
|
espree@^6.1.2:
|
||||||
version "6.2.1"
|
version "6.2.1"
|
||||||
resolved "https://registry.yarnpkg.com/espree/-/espree-6.2.1.tgz#77fc72e1fd744a2052c20f38a5b575832e82734a"
|
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"
|
min-document "^2.19.0"
|
||||||
process "^0.11.10"
|
process "^0.11.10"
|
||||||
|
|
||||||
globals@^11.1.0, globals@^11.7.0:
|
globals@^11.1.0:
|
||||||
version "11.12.0"
|
version "11.12.0"
|
||||||
resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e"
|
resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e"
|
||||||
integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==
|
integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==
|
||||||
@ -11405,7 +11364,7 @@ inquirer@3.3.0:
|
|||||||
strip-ansi "^4.0.0"
|
strip-ansi "^4.0.0"
|
||||||
through "^2.3.6"
|
through "^2.3.6"
|
||||||
|
|
||||||
inquirer@^6.2.0, inquirer@^6.2.2:
|
inquirer@^6.2.0:
|
||||||
version "6.5.2"
|
version "6.5.2"
|
||||||
resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-6.5.2.tgz#ad50942375d036d327ff528c08bd5fab089928ca"
|
resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-6.5.2.tgz#ad50942375d036d327ff528c08bd5fab089928ca"
|
||||||
integrity sha512-cntlB5ghuB0iuO65Ovoi8ogLHiWGs/5yNrtUcKjFhSSiVeAIVpD7koaSU9RM8mpXw5YDi9RdYXGQMaOURB7ycQ==
|
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"
|
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b"
|
||||||
integrity sha1-mGbfOVECEw449/mWvOtlRDIJwls=
|
integrity sha1-mGbfOVECEw449/mWvOtlRDIJwls=
|
||||||
|
|
||||||
js-yaml@^3.11.0, js-yaml@^3.13.0, js-yaml@^3.13.1, js-yaml@^3.9.0:
|
js-yaml@^3.11.0, js-yaml@^3.13.1, js-yaml@^3.9.0:
|
||||||
version "3.14.1"
|
version "3.14.0"
|
||||||
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.1.tgz#dae812fdb3825fa306609a8717383c50c36a0537"
|
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.0.tgz#a7a34170f26a21bb162424d8adacb4113a69e482"
|
||||||
integrity sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==
|
integrity sha512-/4IbIeHcD9VMHFqDR/gQ7EdZdLimOvW2DdcxFjdyyZ9NsbS+ccrXqVWDtab/lRl5AlUqmpBx8EhPaWR+OtY17A==
|
||||||
dependencies:
|
dependencies:
|
||||||
argparse "^1.0.7"
|
argparse "^1.0.7"
|
||||||
esprima "^4.0.0"
|
esprima "^4.0.0"
|
||||||
@ -14893,7 +14852,7 @@ optimize-css-assets-webpack-plugin@^5.0.3:
|
|||||||
cssnano "^4.1.10"
|
cssnano "^4.1.10"
|
||||||
last-call-webpack-plugin "^3.0.0"
|
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"
|
version "0.8.3"
|
||||||
resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.3.tgz#84fa1d036fe9d3c7e21d99884b601167ec8fb495"
|
resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.3.tgz#84fa1d036fe9d3c7e21d99884b601167ec8fb495"
|
||||||
integrity sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==
|
integrity sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==
|
||||||
@ -16849,10 +16808,10 @@ react-ace@^7.0.2:
|
|||||||
lodash.isequal "^4.5.0"
|
lodash.isequal "^4.5.0"
|
||||||
prop-types "^15.7.2"
|
prop-types "^15.7.2"
|
||||||
|
|
||||||
react-cornerstone-viewport@4.0.2:
|
react-cornerstone-viewport@4.0.4:
|
||||||
version "4.0.2"
|
version "4.0.4"
|
||||||
resolved "https://registry.yarnpkg.com/react-cornerstone-viewport/-/react-cornerstone-viewport-4.0.2.tgz#49bc8f8464164d8e779138dd9244d5c2524e0418"
|
resolved "https://registry.yarnpkg.com/react-cornerstone-viewport/-/react-cornerstone-viewport-4.0.4.tgz#0e4291d9b0fe55c003e275175b0902ced61b0c80"
|
||||||
integrity sha512-mF/piahoPG5U5TbxVMtc15bXrQZxcaPycbu4i4KD2rZkiWh1Zs24ryY1S+lFlDB/RcOAm0u+luEEt1tOg8F+UQ==
|
integrity sha512-RjCqOtQep04dRoHtsju0USlxN/vGUI2WnEt1aN84iYU7eZ+AXnwJo1nAd0RMX2obcXaGCIqMDtMRkE/zm3oZHw==
|
||||||
dependencies:
|
dependencies:
|
||||||
classnames "^2.2.6"
|
classnames "^2.2.6"
|
||||||
date-fns "^2.2.1"
|
date-fns "^2.2.1"
|
||||||
@ -16860,10 +16819,10 @@ react-cornerstone-viewport@4.0.2:
|
|||||||
prop-types "^15.7.2"
|
prop-types "^15.7.2"
|
||||||
react-resize-detector "^4.2.1"
|
react-resize-detector "^4.2.1"
|
||||||
|
|
||||||
react-cornerstone-viewport@4.0.4:
|
react-cornerstone-viewport@4.0.5:
|
||||||
version "4.0.4"
|
version "4.0.5"
|
||||||
resolved "https://registry.yarnpkg.com/react-cornerstone-viewport/-/react-cornerstone-viewport-4.0.4.tgz#0e4291d9b0fe55c003e275175b0902ced61b0c80"
|
resolved "https://registry.yarnpkg.com/react-cornerstone-viewport/-/react-cornerstone-viewport-4.0.5.tgz#3de83cdd3643980e0330f3743f631dbbb24d98a7"
|
||||||
integrity sha512-RjCqOtQep04dRoHtsju0USlxN/vGUI2WnEt1aN84iYU7eZ+AXnwJo1nAd0RMX2obcXaGCIqMDtMRkE/zm3oZHw==
|
integrity sha512-uamDtFljWWEHe26kOBJJ1Bu//6hAgEra7BDVykOvtMC/W7kW34Q2fFX3ZwkL6TDdCQ94O6K98IXPS9HbrEJWYg==
|
||||||
dependencies:
|
dependencies:
|
||||||
classnames "^2.2.6"
|
classnames "^2.2.6"
|
||||||
date-fns "^2.2.1"
|
date-fns "^2.2.1"
|
||||||
@ -19549,16 +19508,16 @@ strip-indent@^3.0.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
min-indent "^1.0.0"
|
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"
|
version "2.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a"
|
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a"
|
||||||
integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo=
|
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:
|
strong-log-transformer@^2.0.0:
|
||||||
version "2.1.0"
|
version "2.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/strong-log-transformer/-/strong-log-transformer-2.1.0.tgz#0f5ed78d325e0421ac6f90f7f10e691d6ae3ae10"
|
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:
|
dependencies:
|
||||||
builtins "^1.0.3"
|
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:
|
value-equal@^1.0.1:
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/value-equal/-/value-equal-1.0.1.tgz#1e0b794c734c5c0cade179c437d356d931a34d6c"
|
resolved "https://registry.yarnpkg.com/value-equal/-/value-equal-1.0.1.tgz#1e0b794c734c5c0cade179c437d356d931a34d6c"
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user