feat: add viewport overlays to microscopy mode (#4776)
This commit is contained in:
parent
5043ea3260
commit
084a10f783
@ -1,101 +1,50 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { cleanDenaturalizedDataset } from '@ohif/extension-default';
|
import { cleanDenaturalizedDataset } from '@ohif/extension-default';
|
||||||
|
|
||||||
import './DicomMicroscopyViewport.css';
|
import './DicomMicroscopyViewport.css';
|
||||||
import ViewportOverlay from './components/ViewportOverlay';
|
import ViewportOverlay from './components/ViewportOverlay';
|
||||||
import getDicomWebClient from './utils/dicomWebClient';
|
import getDicomWebClient from './utils/dicomWebClient';
|
||||||
import dcmjs from 'dcmjs';
|
import dcmjs from 'dcmjs';
|
||||||
import MicroscopyService from './services/MicroscopyService';
|
import { useSystem } from '@ohif/core';
|
||||||
import { CustomizationService } from '@ohif/core';
|
|
||||||
|
|
||||||
class DicomMicroscopyViewport extends Component {
|
function DicomMicroscopyViewport({
|
||||||
state = {
|
activeViewportId,
|
||||||
error: null as any,
|
setViewportActive,
|
||||||
isLoaded: false,
|
displaySets,
|
||||||
};
|
viewportId,
|
||||||
|
dataSource,
|
||||||
|
resizeRef,
|
||||||
|
}: {
|
||||||
|
activeViewportId: string;
|
||||||
|
setViewportActive: Function;
|
||||||
|
displaySets: any[];
|
||||||
|
viewportId: string;
|
||||||
|
dataSource: any;
|
||||||
|
resizeRef: any;
|
||||||
|
}) {
|
||||||
|
const { servicesManager, extensionManager } = useSystem();
|
||||||
|
const [isLoaded, setIsLoaded] = useState(false);
|
||||||
|
const [viewer, setViewer] = useState(null);
|
||||||
|
const [managedViewer, setManagedViewer] = useState(null);
|
||||||
|
const overlayElement = useRef();
|
||||||
|
const container = useRef();
|
||||||
|
const { microscopyService, customizationService } = servicesManager.services;
|
||||||
|
|
||||||
microscopyService: MicroscopyService;
|
const overlayData = customizationService.getCustomization('microscopyViewport.overlay');
|
||||||
customizationService: CustomizationService;
|
|
||||||
viewer: any = null; // dicom-microscopy-viewer instance
|
|
||||||
managedViewer: any = null; // managed wrapper of microscopy-dicom extension
|
|
||||||
|
|
||||||
container = React.createRef();
|
|
||||||
overlayElement = React.createRef();
|
|
||||||
|
|
||||||
constructor(props: any) {
|
|
||||||
super(props);
|
|
||||||
|
|
||||||
const { microscopyService, customizationService } = this.props.servicesManager.services;
|
|
||||||
this.microscopyService = microscopyService;
|
|
||||||
this.customizationService = customizationService;
|
|
||||||
}
|
|
||||||
|
|
||||||
static propTypes = {
|
|
||||||
viewportData: PropTypes.object,
|
|
||||||
activeViewportId: PropTypes.string,
|
|
||||||
setViewportActive: PropTypes.func,
|
|
||||||
|
|
||||||
// props from OHIF Viewport Grid
|
|
||||||
displaySets: PropTypes.array,
|
|
||||||
viewportId: PropTypes.string,
|
|
||||||
viewportLabel: PropTypes.string,
|
|
||||||
dataSource: PropTypes.object,
|
|
||||||
viewportOptions: PropTypes.object,
|
|
||||||
displaySetOptions: PropTypes.array,
|
|
||||||
|
|
||||||
// other props from wrapping component
|
|
||||||
servicesManager: PropTypes.object,
|
|
||||||
extensionManager: PropTypes.object,
|
|
||||||
commandsManager: PropTypes.object,
|
|
||||||
resizeRef: PropTypes.oneOfType([PropTypes.func, PropTypes.shape({ current: PropTypes.any })]),
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the nearest ROI from the mouse click point
|
|
||||||
*
|
|
||||||
* @param event
|
|
||||||
* @param autoselect
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
getNearbyROI(event: Event, autoselect = true) {
|
|
||||||
const symbols = Object.getOwnPropertySymbols(this.viewer);
|
|
||||||
const _drawingSource = symbols.find(p => p.description === 'drawingSource');
|
|
||||||
const _pyramid = symbols.find(p => p.description === 'pyramid');
|
|
||||||
const _map = symbols.find(p => p.description === 'map');
|
|
||||||
const _affine = symbols.find(p => p.description === 'affine');
|
|
||||||
|
|
||||||
const feature = this.viewer[_drawingSource].getClosestFeatureToCoordinate(
|
|
||||||
this.viewer[_map].getEventCoordinate(event)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!feature) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const roiAnnotation = this.viewer._getROIFromFeature(
|
|
||||||
feature,
|
|
||||||
this.viewer[_pyramid].metadata,
|
|
||||||
this.viewer[_affine]
|
|
||||||
);
|
|
||||||
if (roiAnnotation && autoselect) {
|
|
||||||
this.microscopyService.selectAnnotation(roiAnnotation);
|
|
||||||
}
|
|
||||||
return roiAnnotation;
|
|
||||||
}
|
|
||||||
|
|
||||||
// install the microscopy renderer into the web page.
|
// install the microscopy renderer into the web page.
|
||||||
// you should only do this once.
|
// you should only do this once.
|
||||||
async installOpenLayersRenderer(container, displaySet) {
|
const installOpenLayersRenderer = useCallback(
|
||||||
|
async (container, displaySet) => {
|
||||||
const loadViewer = async metadata => {
|
const loadViewer = async metadata => {
|
||||||
const dicomMicroscopyModule = await this.microscopyService.importDicomMicroscopyViewer();
|
const dicomMicroscopyModule = await microscopyService.importDicomMicroscopyViewer();
|
||||||
const { viewer: DicomMicroscopyViewer, metadata: metadataUtils } = dicomMicroscopyModule;
|
const { viewer: DicomMicroscopyViewer, metadata: metadataUtils } = dicomMicroscopyModule;
|
||||||
|
|
||||||
const microscopyViewer = DicomMicroscopyViewer.VolumeImageViewer;
|
const microscopyViewer = DicomMicroscopyViewer.VolumeImageViewer;
|
||||||
|
|
||||||
const client = getDicomWebClient({
|
const client = getDicomWebClient({
|
||||||
extensionManager: this.props.extensionManager,
|
extensionManager,
|
||||||
servicesManager: this.props.servicesManager,
|
servicesManager,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Parse, format, and filter metadata
|
// Parse, format, and filter metadata
|
||||||
@ -152,7 +101,7 @@ class DicomMicroscopyViewport extends Component {
|
|||||||
{
|
{
|
||||||
StudyInstanceUID: m.StudyInstanceUID,
|
StudyInstanceUID: m.StudyInstanceUID,
|
||||||
SeriesInstanceUID: m.SeriesInstanceUID,
|
SeriesInstanceUID: m.SeriesInstanceUID,
|
||||||
dataSourceConfig: this.props.dataSource.getConfig(),
|
dataSourceConfig: dataSource.getConfig(),
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
if (!inst['00480105']) {
|
if (!inst['00480105']) {
|
||||||
@ -188,36 +137,39 @@ class DicomMicroscopyViewport extends Component {
|
|||||||
controls: ['overview', 'position'],
|
controls: ['overview', 'position'],
|
||||||
};
|
};
|
||||||
|
|
||||||
this.viewer = new microscopyViewer(options);
|
const viewer = new microscopyViewer(options);
|
||||||
|
|
||||||
if (this.overlayElement && this.overlayElement.current && this.viewer.addViewportOverlay) {
|
if (overlayElement && overlayElement.current && viewer.addViewportOverlay) {
|
||||||
this.viewer.addViewportOverlay({
|
viewer.addViewportOverlay({
|
||||||
element: this.overlayElement.current,
|
element: overlayElement.current,
|
||||||
coordinates: [0, 0], // TODO: dicom-microscopy-viewer documentation says this can be false to be automatically, but it is not.
|
coordinates: [0, 0], // TODO: dicom-microscopy-viewer documentation says this can be false to be automatically, but it is not.
|
||||||
navigate: true,
|
navigate: true,
|
||||||
className: 'OpenLayersOverlay',
|
className: 'OpenLayersOverlay',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
this.viewer.render({ container });
|
viewer.render({ container });
|
||||||
|
|
||||||
const { StudyInstanceUID, SeriesInstanceUID } = displaySet;
|
const { StudyInstanceUID, SeriesInstanceUID } = displaySet;
|
||||||
|
|
||||||
this.managedViewer = this.microscopyService.addViewer(
|
const managedViewer = microscopyService.addViewer(
|
||||||
this.viewer,
|
viewer,
|
||||||
this.props.viewportId,
|
viewportId,
|
||||||
container,
|
container,
|
||||||
StudyInstanceUID,
|
StudyInstanceUID,
|
||||||
SeriesInstanceUID
|
SeriesInstanceUID
|
||||||
);
|
);
|
||||||
|
|
||||||
this.managedViewer.addContextMenuCallback((event: Event) => {
|
managedViewer.addContextMenuCallback((event: Event) => {
|
||||||
// TODO: refactor this after Bill's changes on ContextMenu feature get merged
|
// TODO: refactor this after Bill's changes on ContextMenu feature get merged
|
||||||
// const roiAnnotationNearBy = this.getNearbyROI(event);
|
// const roiAnnotationNearBy = this.getNearbyROI(event);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
setViewer(viewer);
|
||||||
|
setManagedViewer(managedViewer);
|
||||||
};
|
};
|
||||||
|
|
||||||
this.microscopyService.clearAnnotations();
|
microscopyService.clearAnnotations();
|
||||||
|
|
||||||
let smDisplaySet = displaySet;
|
let smDisplaySet = displaySet;
|
||||||
if (displaySet.Modality === 'SR') {
|
if (displaySet.Modality === 'SR') {
|
||||||
@ -231,49 +183,39 @@ class DicomMicroscopyViewport extends Component {
|
|||||||
if (displaySet.Modality === 'SR') {
|
if (displaySet.Modality === 'SR') {
|
||||||
displaySet.load(smDisplaySet);
|
displaySet.load(smDisplaySet);
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
[dataSource, extensionManager, microscopyService, servicesManager, viewportId]
|
||||||
|
);
|
||||||
|
|
||||||
componentDidMount() {
|
useEffect(() => {
|
||||||
const { displaySets, viewportOptions } = this.props;
|
|
||||||
// Todo-rename: this is always getting the 0
|
|
||||||
const displaySet = displaySets[0];
|
const displaySet = displaySets[0];
|
||||||
this.installOpenLayersRenderer(this.container.current, displaySet).then(() => {
|
installOpenLayersRenderer(container.current, displaySet).then(() => {
|
||||||
this.setState({ isLoaded: true });
|
setIsLoaded(true);
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate(prevProps: Readonly<{}>, prevState: Readonly<{}>, snapshot?: any): void {
|
return () => {
|
||||||
if (this.managedViewer && prevProps.displaySets !== this.props.displaySets) {
|
if (viewer) {
|
||||||
const { displaySets } = this.props;
|
microscopyService.removeViewer(viewer);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
const displaySet = displaySets[0];
|
const displaySet = displaySets[0];
|
||||||
|
|
||||||
this.microscopyService.clearAnnotations();
|
microscopyService.clearAnnotations();
|
||||||
|
|
||||||
// loading SR
|
// loading SR
|
||||||
if (displaySet.Modality === 'SR') {
|
if (displaySet.Modality === 'SR') {
|
||||||
const referencedDisplaySet = displaySet.getSourceDisplaySet();
|
const referencedDisplaySet = displaySet.getSourceDisplaySet();
|
||||||
displaySet.load(referencedDisplaySet);
|
displaySet.load(referencedDisplaySet);
|
||||||
}
|
}
|
||||||
}
|
}, [managedViewer, displaySets, microscopyService]);
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
this.microscopyService.removeViewer(this.viewer);
|
|
||||||
}
|
|
||||||
|
|
||||||
setViewportActiveHandler = () => {
|
|
||||||
const { setViewportActive, viewportId, activeViewportId } = this.props;
|
|
||||||
|
|
||||||
if (viewportId !== activeViewportId) {
|
|
||||||
setViewportActive(viewportId);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const style = { width: '100%', height: '100%' };
|
const style = { width: '100%', height: '100%' };
|
||||||
const displaySet = this.props.displaySets[0];
|
const displaySet = displaySets[0];
|
||||||
const firstInstance = displaySet.firstInstance || displaySet.instance;
|
const firstInstance = displaySet.firstInstance || displaySet.instance;
|
||||||
const LoadingIndicatorProgress = this.customizationService.getCustomization(
|
const LoadingIndicatorProgress = customizationService.getCustomization(
|
||||||
'ui.loadingIndicatorProgress'
|
'ui.loadingIndicatorProgress'
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -281,13 +223,18 @@ class DicomMicroscopyViewport extends Component {
|
|||||||
<div
|
<div
|
||||||
className={'DicomMicroscopyViewer'}
|
className={'DicomMicroscopyViewer'}
|
||||||
style={style}
|
style={style}
|
||||||
onClick={this.setViewportActiveHandler}
|
onClick={() => {
|
||||||
|
if (viewportId !== activeViewportId) {
|
||||||
|
setViewportActive(viewportId);
|
||||||
|
}
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<div style={{ ...style, display: 'none' }}>
|
<div style={{ ...style, display: 'none' }}>
|
||||||
<div style={{ ...style }} ref={this.overlayElement}>
|
<div style={{ ...style }} ref={overlayElement}>
|
||||||
<div style={{ position: 'relative', height: '100%', width: '100%' }}>
|
<div style={{ position: 'relative', height: '100%', width: '100%' }}>
|
||||||
{displaySet && firstInstance.imageId && (
|
{displaySet && firstInstance.imageId && (
|
||||||
<ViewportOverlay
|
<ViewportOverlay
|
||||||
|
overlayData={overlayData}
|
||||||
displaySet={displaySet}
|
displaySet={displaySet}
|
||||||
instance={displaySet.instance}
|
instance={displaySet.instance}
|
||||||
metadata={displaySet.metadata}
|
metadata={displaySet.metadata}
|
||||||
@ -296,23 +243,16 @@ class DicomMicroscopyViewport extends Component {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{this.state.error ? (
|
|
||||||
<h2>{JSON.stringify(this.state.error)}</h2>
|
|
||||||
) : (
|
|
||||||
<div
|
<div
|
||||||
style={style}
|
style={style}
|
||||||
ref={(ref: any) => {
|
ref={(ref: any) => {
|
||||||
this.container.current = ref;
|
container.current = ref;
|
||||||
this.props.resizeRef.current = ref;
|
resizeRef.current = ref;
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
{isLoaded ? null : <LoadingIndicatorProgress className={'h-full w-full bg-black'} />}
|
||||||
{this.state.isLoaded ? null : (
|
|
||||||
<LoadingIndicatorProgress className={'h-full w-full bg-black'} />
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
export default DicomMicroscopyViewport;
|
export default DicomMicroscopyViewport;
|
||||||
|
|||||||
@ -24,20 +24,18 @@ interface OverlayItem {
|
|||||||
* @param {*} extensionManager is used to load the image data.
|
* @param {*} extensionManager is used to load the image data.
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const generateFromConfig = ({
|
export const generateFromConfig = ({ config, overlayData, ...props }) => {
|
||||||
|
const {
|
||||||
topLeft = [],
|
topLeft = [],
|
||||||
topRight = [],
|
topRight = [],
|
||||||
bottomLeft = [],
|
bottomLeft = [],
|
||||||
bottomRight = [],
|
bottomRight = [],
|
||||||
itemGenerator = () => {},
|
|
||||||
}: {
|
}: {
|
||||||
topLeft?: OverlayItem[];
|
topLeft?: OverlayItem[];
|
||||||
topRight?: OverlayItem[];
|
topRight?: OverlayItem[];
|
||||||
bottomLeft?: OverlayItem[];
|
bottomLeft?: OverlayItem[];
|
||||||
bottomRight?: OverlayItem[];
|
bottomRight?: OverlayItem[];
|
||||||
itemGenerator?: (props: any) => any;
|
} = overlayData ?? {};
|
||||||
}) => {
|
|
||||||
return (props: any) => {
|
|
||||||
const topLeftClass = 'top-viewport left-viewport text-primary-light';
|
const topLeftClass = 'top-viewport left-viewport text-primary-light';
|
||||||
const topRightClass = 'top-viewport right-viewport-scrollbar text-primary-light';
|
const topRightClass = 'top-viewport right-viewport-scrollbar text-primary-light';
|
||||||
const bottomRightClass = 'bottom-viewport right-viewport-scrollbar text-primary-light';
|
const bottomRightClass = 'bottom-viewport right-viewport-scrollbar text-primary-light';
|
||||||
@ -52,10 +50,7 @@ export const generateFromConfig = ({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{topRight && topRight.length > 0 && (
|
{topRight && topRight.length > 0 && (
|
||||||
<div
|
<div data-cy={'viewport-overlay-top-right'} className={classnames(overlay, topRightClass)}>
|
||||||
data-cy={'viewport-overlay-top-right'}
|
|
||||||
className={classnames(overlay, topRightClass)}
|
|
||||||
>
|
|
||||||
{listComponentGenerator({
|
{listComponentGenerator({
|
||||||
...props,
|
...props,
|
||||||
list: topRight,
|
list: topRight,
|
||||||
@ -90,7 +85,6 @@ export const generateFromConfig = ({
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
};
|
|
||||||
|
|
||||||
const itemGenerator = (props: any) => {
|
const itemGenerator = (props: any) => {
|
||||||
const { item } = props;
|
const { item } = props;
|
||||||
@ -123,4 +117,4 @@ const itemGenerator = (props: any) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default generateFromConfig({});
|
export default generateFromConfig;
|
||||||
|
|||||||
@ -83,9 +83,6 @@ const extension: Types.Extensions.Extension = {
|
|||||||
return (
|
return (
|
||||||
<MicroscopyViewport
|
<MicroscopyViewport
|
||||||
key={displaySetsKey}
|
key={displaySetsKey}
|
||||||
servicesManager={servicesManager}
|
|
||||||
extensionManager={extensionManager}
|
|
||||||
commandsManager={commandsManager}
|
|
||||||
activeViewportId={activeViewportId}
|
activeViewportId={activeViewportId}
|
||||||
setViewportActive={(viewportId: string) => {
|
setViewportActive={(viewportId: string) => {
|
||||||
viewportGridService.setActiveViewportId(viewportId);
|
viewportGridService.setActiveViewportId(viewportId);
|
||||||
|
|||||||
@ -14,7 +14,7 @@ interface SystemContextProviderProps {
|
|||||||
const systemContext = createContext(null);
|
const systemContext = createContext(null);
|
||||||
const { Provider } = systemContext;
|
const { Provider } = systemContext;
|
||||||
|
|
||||||
export const useSystem = () => useContext(systemContext);
|
export const useSystem = () => useContext<SystemContextProviderProps>(systemContext);
|
||||||
|
|
||||||
export function SystemContextProvider({
|
export function SystemContextProvider({
|
||||||
children,
|
children,
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user