ohif-viewer/extensions/dicom-rt/src/components/RTPanel/RTPanel.js
2022-08-31 14:57:14 +02:00

348 lines
11 KiB
JavaScript

import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import cornerstoneTools from 'cornerstone-tools';
import cornerstone from 'cornerstone-core';
import { ScrollableArea, TableList, Icon } from '@ohif/ui';
import { utils } from '@ohif/core';
import './RTPanel.css';
import StructureSetItem from '../StructureSetItem/StructureSetItem';
import RTPanelSettings from '../RTSettings/RTSettings';
import PanelSection from '../PanelSection/PanelSection';
import LoadingIndicator from '../LoadingIndicator/LoadingIndicator';
import TOOL_NAMES from '../../utils/toolNames';
const { RTSTRUCT_DISPLAY_TOOL } = TOOL_NAMES;
const { studyMetadataManager } = utils;
const refreshViewport = () => {
cornerstone.getEnabledElements().forEach(enabledElement => {
if (enabledElement.image) {
cornerstone.updateImage(enabledElement.element);
}
});
};
/**
* RTPanel component
*
* @param {Object} props
* @param {Array} props.studies
* @param {Array} props.getActiveViewport - get active viewport data
* @param {number} props.activeIndex - activeViewportIndex
* @param {number} props.isOpen - isOpen
* @returns component
*/
const RTPanel = ({
studies,
activeIndex,
isOpen,
onContourItemClick,
activeContexts = [],
contexts = {},
activeViewport,
getActiveViewport,
}) => {
const isVTK = () => activeContexts.includes(contexts.VTK);
const isCornerstone = () => activeContexts.includes(contexts.CORNERSTONE);
const [selectedContour, setSelectedContour] = useState();
const DEFAULT_SET_INDEX = 0;
const DEFAULT_STATE = {
referencedDisplaysets: [],
sets: [],
selectedSet: null,
isLocked: false,
};
const [state, setState] = useState(DEFAULT_STATE);
const [showSettings, setShowSettings] = useState(false);
/*
* TODO: Improve the way we notify parts of the app that depends on rts to be loaded.
*
* Currently we are using a non-ideal implementation through a custom event to notify the rtstruct panel
* or other components that could rely on loaded rtstructs that
* the first batch of structs were loaded so that e.g. when the user opens the panel
* before the structs are fully loaded, the panel can subscribe to this custom event
* and update itself with the new structs.
*
* This limitation is due to the fact that the rtmodule is an object (which will be
* updated after the structs are loaded) that React its not aware of its changes
* because the module object its not passed in to the panel component as prop but accessed externally.
*
* Improving this event approach to something reactive that can be tracked inside the react lifecycle,
* allows us to easily watch the module or the rtstruct loading process in any other component
* without subscribing to external events.
*/
useEffect(() => {
updateStructureSets();
document.addEventListener('rt-panel-tab-updated', updateStructureSets);
return () => {
document.removeEventListener('rt-panel-tab-updated', updateStructureSets);
};
}, []);
useEffect(() => {
const studyMetadata = studyMetadataManager.get(
activeViewport.StudyInstanceUID
);
const referencedDisplaysets = studyMetadata.getDerivedDatasets({
referencedSeriesInstanceUID: activeViewport.SeriesInstanceUID,
Modality: 'RTSTRUCT',
});
setState(state => ({ ...state, isLocked: !referencedDisplaysets.length }));
}, [activeViewport]);
const updateStructureSets = () => {
const viewport = getActiveViewport();
const module = cornerstoneTools.getModule('rtstruct');
const StructureSets = module.state.StructureSets;
if (StructureSets && StructureSets.length) {
const viewportSets = module.getters.structuresSetsWhichReferenceSeriesInstanceUid(
viewport.SeriesInstanceUID
);
const studyMetadata = studyMetadataManager.get(viewport.StudyInstanceUID);
const referencedDisplaysets = studyMetadata.getDerivedDatasets({
referencedSeriesInstanceUID: viewport.SeriesInstanceUID,
Modality: 'RTSTRUCT',
});
if (viewportSets.length) {
const defaultSet = viewportSets[DEFAULT_SET_INDEX];
setState({
referencedDisplaysets,
selectedSet: defaultSet,
sets: viewportSets,
});
} else {
setState(DEFAULT_STATE);
}
}
};
useEffect(() => {
setShowSettings(showSettings && !isOpen);
}, [isOpen]);
const toContourItem = (
{ ROINumber, ROIName, RTROIObservations, colorArray, visible, isSupported },
loadedSet
) => {
let interpretedType = '';
if (RTROIObservations && RTROIObservations.RTROIInterpretedType) {
interpretedType = `(${RTROIObservations.RTROIInterpretedType})`;
}
const isSameContour = selectedContour && selectedContour === ROINumber;
return (
<StructureSetItem
key={ROINumber}
selected={isSameContour}
isDisabled={!isSupported}
onClick={() => {
setSelectedContour(isSameContour ? null : ROINumber);
if (isCornerstone()) {
const enabledElements = cornerstone.getEnabledElements();
const element = enabledElements[activeIndex].element;
const stackToolState = cornerstoneTools.getToolState(
element,
'stack'
);
if (!stackToolState) {
return;
}
const imageIds = stackToolState.data[0].imageIds;
const module = cornerstoneTools.getModule('rtstruct');
const imageId = module.getters.imageIdOfCenterFrameOfROIContour(
state.selectedSet.SeriesInstanceUID,
ROINumber,
imageIds
);
const toolState = cornerstoneTools.globalImageIdSpecificToolStateManager.saveToolState();
const imageIdSpecificToolState = toolState[imageId];
const rtstructData =
imageIdSpecificToolState[RTSTRUCT_DISPLAY_TOOL];
const specificData = rtstructData.data.find(
rtData => rtData.ROINumber === ROINumber
);
specificData.highlight = true;
const frameIndex = imageIds.indexOf(imageId);
const SOPInstanceUID = cornerstone.metaData.get(
'SOPInstanceUID',
imageId
);
const StudyInstanceUID = cornerstone.metaData.get(
'StudyInstanceUID',
imageId
);
onContourItemClick({
StudyInstanceUID,
SOPInstanceUID,
frameIndex,
activeViewportIndex: activeIndex,
});
}
}}
label={`${ROIName} ${interpretedType}`}
index={ROINumber}
color={colorArray}
visible={visible}
onVisibilityChange={() => {
const module = cornerstoneTools.getModule('rtstruct');
module.setters.toggleROIContour(
state.selectedSet.SeriesInstanceUID,
ROINumber
);
}}
/>
);
};
const configurationChangeHandler = newConfiguration => {
const module = cornerstoneTools.getModule('rtstruct');
module.configuration.lineWidth = newConfiguration.lineWidth;
module.configuration.opacity = newConfiguration.opacity;
refreshViewport();
};
if (showSettings) {
const module = cornerstoneTools.getModule('rtstruct');
return (
<RTPanelSettings
configuration={module.configuration}
onBack={() => setShowSettings(false)}
onChange={configurationChangeHandler}
/>
);
}
return (
<div className="dcmrt-panel">
<div className="dcmrt-panel-header">
{' '}
{!state.isLocked && (
<Icon
className="cog-icon"
name="cog"
width="25px"
height="25px"
onClick={() => setShowSettings(true)}
/>
)}
<h3>RT Structure Sets</h3>
</div>
{!state.isLocked && !state.referencedDisplaysets.length && (
<LoadingIndicator expand height="70px" width="70px" />
)}
{state.sets &&
state.referencedDisplaysets.map(displaySet => {
const { SeriesInstanceUID, metadata, isLoaded } = displaySet;
const module = cornerstoneTools.getModule('rtstruct');
const sets = module.getters.structuresSetsWhichReferenceSeriesInstanceUid(
activeViewport.SeriesInstanceUID
);
const loadedSet = sets.find(
set => set.SeriesInstanceUID === SeriesInstanceUID
);
if (!loadedSet) return null;
return (
<PanelSection
key={SeriesInstanceUID}
title={metadata.StructureSetLabel}
loading={!isLoaded || !loadedSet}
visible={isLoaded && loadedSet && loadedSet.visible}
hideVisibleButton={!isLoaded}
expanded={
isLoaded &&
loadedSet.SeriesInstanceUID ===
state.selectedSet.SeriesInstanceUID
}
onVisibilityChange={newVisibility => {
const module = cornerstoneTools.getModule('rtstruct');
if (newVisibility) {
module.setters.showStructureSet(loadedSet.SeriesInstanceUID);
} else {
module.setters.hideStructureSet(loadedSet.SeriesInstanceUID);
}
const sets = module.getters.structuresSetsWhichReferenceSeriesInstanceUid(
activeViewport.SeriesInstanceUID
);
setState(state => ({ ...state, sets }));
refreshViewport();
}}
onExpandChange={async () => {
if (!isLoaded) {
await displaySet.load(activeViewport, studies);
const module = cornerstoneTools.getModule('rtstruct');
const sets = module.getters.structuresSetsWhichReferenceSeriesInstanceUid(
activeViewport.SeriesInstanceUID
);
const selectedSet = sets.find(
set => set.SeriesInstanceUID === SeriesInstanceUID
);
setState(state => ({ ...state, selectedSet, sets }));
}
}}
>
<ScrollableArea>
<TableList headless>
{isLoaded &&
loadedSet.ROIContours.map(c => toContourItem(c, loadedSet))}
</TableList>
</ScrollableArea>
</PanelSection>
);
})}
</div>
);
};
RTPanel.propTypes = {
viewports: PropTypes.shape({
displaySetInstanceUID: PropTypes.string,
frameRate: PropTypes.any,
InstanceNumber: PropTypes.number,
isMultiFrame: PropTypes.bool,
isReconstructable: PropTypes.bool,
Modality: PropTypes.string,
plugin: PropTypes.string,
SeriesDate: PropTypes.string,
SeriesDescription: PropTypes.string,
SeriesInstanceUID: PropTypes.string,
SeriesNumber: PropTypes.any,
SeriesTime: PropTypes.string,
sopClassUIDs: PropTypes.arrayOf(PropTypes.string),
StudyInstanceUID: PropTypes.string,
}),
activeIndex: PropTypes.number.isRequired,
studies: PropTypes.array.isRequired,
isOpen: PropTypes.bool.isRequired,
};
RTPanel.defaultProps = {};
export default RTPanel;