[IDC-1670] Better display of derived datasets in side panel. (#1962)

* WIP

* WIP

* WIP

* WIP

* Working click on seg.

* Load SEG if not loaded and set active.

* RTSTRUCT

* feat: 🎸 Display SEG and RTSTRUCT in side panel

* Throw error if dataset not available.

* Add delay in microscopy viewer e2e test.

* Add delay in microscopy viewer e2e test.
This commit is contained in:
James Petts 2020-08-10 10:20:31 +01:00 committed by GitHub
parent b7ab88b0f1
commit f8fc31bcac
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 245 additions and 91 deletions

View File

@ -1,5 +1,6 @@
import { MODULE_TYPES, utils, DICOMWeb } from '@ohif/core'; import { MODULE_TYPES, utils, DICOMWeb } from '@ohif/core';
import loadRTStruct from './loadRTStruct'; import loadRTStruct from './loadRTStruct';
import getSourceDisplaySet from './getSourceDisplaySet';
import id from './id'; import id from './id';
@ -67,6 +68,10 @@ const OHIFDicomRTStructSopClassHandler = {
} }
} }
rtStructDisplaySet.getSourceDisplaySet = function(studies) {
return getSourceDisplaySet(studies, rtStructDisplaySet);
};
rtStructDisplaySet.load = function(referencedDisplaySet, studies) { rtStructDisplaySet.load = function(referencedDisplaySet, studies) {
return loadRTStruct( return loadRTStruct(
rtStructDisplaySet, rtStructDisplaySet,

View File

@ -242,7 +242,7 @@ const RTPanel = ({
key={SeriesInstanceUID} key={SeriesInstanceUID}
title={metadata.StructureSetLabel} title={metadata.StructureSetLabel}
loading={!isLoaded || !loadedSet} loading={!isLoaded || !loadedSet}
visible={isLoaded && loadedSet.visible} visible={isLoaded && loadedSet && loadedSet.visible}
hideVisibleButton={!isLoaded} hideVisibleButton={!isLoaded}
expanded={ expanded={
isLoaded && isLoaded &&

View File

@ -0,0 +1,38 @@
export default function getSourceDisplaySet(studies, rtStructDisplaySet) {
const referencedDisplaySet = _getReferencedDisplaySet(
rtStructDisplaySet,
studies
);
rtStructDisplaySet.load(referencedDisplaySet, studies);
return referencedDisplaySet;
}
const _getReferencedDisplaySet = (rtStructDisplaySet, studies) => {
let allDisplaySets = [];
studies.forEach(study => {
allDisplaySets = allDisplaySets.concat(study.displaySets);
});
const otherDisplaySets = allDisplaySets.filter(
ds => ds.displaySetInstanceUID !== rtStructDisplaySet.displaySetInstanceUID
);
const ReferencedSeriesSequence = Array.isArray(
rtStructDisplaySet.metadata.ReferencedSeriesSequence
)
? rtStructDisplaySet.metadata.ReferencedSeriesSequence
: [rtStructDisplaySet.metadata.ReferencedSeriesSequence];
const referencedSeriesInstanceUIDs = ReferencedSeriesSequence.map(
ReferencedSeries => ReferencedSeries.SeriesInstanceUID
);
const referencedDisplaySet = otherDisplaySets.find(ds =>
referencedSeriesInstanceUIDs.includes(ds.SeriesInstanceUID)
);
return referencedDisplaySet;
};

View File

@ -6,6 +6,9 @@ import moment from 'moment';
import { utils, log } from '@ohif/core'; import { utils, log } from '@ohif/core';
import { ScrollableArea, TableList, Icon } from '@ohif/ui'; import { ScrollableArea, TableList, Icon } from '@ohif/ui';
import setActiveLabelmap from '../../utils/setActiveLabelMap';
import refreshViewports from '../../utils/refreshViewports';
import { import {
BrushColorSelector, BrushColorSelector,
BrushRadius, BrushRadius,
@ -19,12 +22,6 @@ import SegmentationSettings from '../SegmentationSettings/SegmentationSettings';
const { studyMetadataManager } = utils; const { studyMetadataManager } = utils;
const refreshViewport = () => {
cornerstone.getEnabledElements().forEach(enabledElement => {
cornerstone.updateImage(enabledElement.element);
});
};
/** /**
* SegmentationPanel component * SegmentationPanel component
* *
@ -68,8 +65,7 @@ const SegmentationPanel = ({
*/ */
const [state, setState] = useState({ const [state, setState] = useState({
brushRadius: DEFAULT_BRUSH_RADIUS, brushRadius: DEFAULT_BRUSH_RADIUS,
brushColor: brushColor: 'rgba(221, 85, 85, 1)',
'rgba(221, 85, 85, 1)',
selectedSegment: null, selectedSegment: null,
selectedSegmentation: null, selectedSegmentation: null,
showSegmentationSettings: false, showSegmentationSettings: false,
@ -78,7 +74,7 @@ const SegmentationPanel = ({
segmentList: [], segmentList: [],
cachedSegmentsProperties: [], cachedSegmentsProperties: [],
isLoading: false, isLoading: false,
isDisabled: true isDisabled: true,
}); });
useEffect(() => { useEffect(() => {
@ -112,7 +108,10 @@ const SegmentationPanel = ({
* allows us to easily watch the module or the segmentations loading process in any other component * allows us to easily watch the module or the segmentations loading process in any other component
* without subscribing to external events. * without subscribing to external events.
*/ */
document.addEventListener('extensiondicomsegmentationsegloaded', refreshSegmentations); document.addEventListener(
'extensiondicomsegmentationsegloaded',
refreshSegmentations
);
/* /*
* These are specific to each element; * These are specific to each element;
@ -127,7 +126,10 @@ const SegmentationPanel = ({
); );
return () => { return () => {
document.removeEventListener('extensiondicomsegmentationsegloaded', refreshSegmentations); document.removeEventListener(
'extensiondicomsegmentationsegloaded',
refreshSegmentations
);
cornerstoneTools.store.state.enabledElements.forEach(enabledElement => cornerstoneTools.store.state.enabledElements.forEach(enabledElement =>
enabledElement.removeEventListener( enabledElement.removeEventListener(
'cornerstonetoolslabelmapmodified', 'cornerstonetoolslabelmapmodified',
@ -169,26 +171,29 @@ const SegmentationPanel = ({
selectedSegmentation: brushStackState.activeLabelmapIndex, selectedSegmentation: brushStackState.activeLabelmapIndex,
labelmapList, labelmapList,
segmentList, segmentList,
isDisabled isDisabled,
})); }));
} else { } else {
setState(state => ({ setState(state => ({
...state, ...state,
labelmapList: [], labelmapList: [],
segmentList: [], segmentList: [],
isDisabled isDisabled,
})); }));
} }
} }
}, [ }, [viewports, activeIndex, state.isLoading]);
viewports,
activeIndex,
state.isLoading
]);
useEffect(() => { useEffect(() => {
refreshSegmentations(); refreshSegmentations();
}, [viewports, activeIndex, isOpen, state.selectedSegmentation, activeContexts, state.isLoading]); }, [
viewports,
activeIndex,
isOpen,
state.selectedSegmentation,
activeContexts,
state.isLoading,
]);
/* Handle open/closed panel behaviour */ /* Handle open/closed panel behaviour */
useEffect(() => { useEffect(() => {
@ -223,12 +228,10 @@ const SegmentationPanel = ({
title: displayDescription, title: displayDescription,
description: displayDate, description: displayDate,
onClick: async () => { onClick: async () => {
const activatedLabelmapIndex = await _setActiveLabelmap( const activatedLabelmapIndex = await setActiveLabelmap(
activeViewport, activeViewport,
studies, studies,
displaySet, displaySet,
firstImageId,
brushStackState.activeLabelmapIndex,
() => onSelectedSegmentationChange(), () => onSelectedSegmentationChange(),
onDisplaySetLoadFailure onDisplaySetLoadFailure
); );
@ -376,9 +379,13 @@ const SegmentationPanel = ({
return !segmentsHidden[segmentIndex]; return !segmentsHidden[segmentIndex];
}; };
const cachedSegmentProperties = state.cachedSegmentsProperties[segmentNumber]; const cachedSegmentProperties =
state.cachedSegmentsProperties[segmentNumber];
let visible = isSegmentVisible(); let visible = isSegmentVisible();
if (cachedSegmentProperties && cachedSegmentProperties.visible !== visible) { if (
cachedSegmentProperties &&
cachedSegmentProperties.visible !== visible
) {
toggleSegmentVisibility(); toggleSegmentVisibility();
} }
@ -406,8 +413,10 @@ const SegmentationPanel = ({
onSegmentVisibilityChange(segmentNumber, newVisibility); onSegmentVisibilityChange(segmentNumber, newVisibility);
} }
updateCachedSegmentsProperties(segmentNumber, { visible: newVisibility }); updateCachedSegmentsProperties(segmentNumber, {
refreshViewport(); visible: newVisibility,
});
refreshViewports();
}} }}
/> />
); );
@ -429,10 +438,9 @@ const SegmentationPanel = ({
const segmentsProperties = state.cachedSegmentsProperties; const segmentsProperties = state.cachedSegmentsProperties;
const segmentProperties = state.cachedSegmentsProperties[segmentNumber]; const segmentProperties = state.cachedSegmentsProperties[segmentNumber];
segmentsProperties[segmentNumber] = segmentsProperties[segmentNumber] = segmentProperties
segmentProperties ? ? { ...segmentProperties, ...properties }
{ ...segmentProperties, ...properties } : : properties;
properties;
updateState('cachedSegmentsProperties', segmentsProperties); updateState('cachedSegmentsProperties', segmentsProperties);
}; };
@ -499,10 +507,13 @@ const SegmentationPanel = ({
configuration.fillAlphaInactive = newConfiguration.fillAlphaInactive; configuration.fillAlphaInactive = newConfiguration.fillAlphaInactive;
configuration.outlineAlphaInactive = newConfiguration.outlineAlphaInactive; configuration.outlineAlphaInactive = newConfiguration.outlineAlphaInactive;
onConfigurationChange(newConfiguration); onConfigurationChange(newConfiguration);
refreshViewport(); refreshViewports();
}; };
const disabledConfigurationFields = ['outlineAlpha', 'shouldRenderInactiveLabelmaps']; const disabledConfigurationFields = [
'outlineAlpha',
'shouldRenderInactiveLabelmaps',
];
if (state.showSegmentationSettings) { if (state.showSegmentationSettings) {
return ( return (
<SegmentationSettings <SegmentationSettings
@ -514,7 +525,10 @@ const SegmentationPanel = ({
); );
} else { } else {
return ( return (
<div className={`dcmseg-segmentation-panel ${state.isDisabled && 'disabled'}`}> <div
className={`dcmseg-segmentation-panel ${state.isDisabled &&
'disabled'}`}
>
<Icon <Icon
className="cog-icon" className="cog-icon"
name="cog" name="cog"
@ -613,57 +627,6 @@ const _getReferencedSegDisplaysets = (StudyInstanceUID, SeriesInstanceUID) => {
return referencedDisplaysets; return referencedDisplaysets;
}; };
/**
*
*
* @param {*} viewportSpecificData
* @param {*} studies
* @param {*} displaySet
* @param {*} firstImageId
* @param {*} activeLabelmapIndex
* @returns
*/
const _setActiveLabelmap = async (
viewportSpecificData,
studies,
displaySet,
firstImageId,
activeLabelmapIndex,
callback = () => { },
onDisplaySetLoadFailure
) => {
if (displaySet.labelmapIndex === activeLabelmapIndex) {
log.warn(`${activeLabelmapIndex} is already the active labelmap`);
return displaySet.labelmapIndex;
}
if (!displaySet.isLoaded) {
// What props does this expect `viewportSpecificData` to have?
// TODO: Should this return the `labelmapIndex`?
const loadPromise = displaySet.load(viewportSpecificData, studies);
loadPromise.catch(error => {
onDisplaySetLoadFailure(error);
// Return old index.
return activeLabelmapIndex;
});
await loadPromise;
}
const { state } = cornerstoneTools.getModule('segmentation');
const brushStackState = state.series[firstImageId];
brushStackState.activeLabelmapIndex = displaySet.labelmapIndex;
refreshViewport();
callback();
return displaySet.labelmapIndex;
};
/** /**
* *
* @param {*} firstImageId * @param {*} firstImageId
@ -683,7 +646,7 @@ const _setActiveSegment = (firstImageId, segmentIndex, activeSegmentIndex) => {
brushStackState.labelmaps3D[brushStackState.activeLabelmapIndex]; brushStackState.labelmaps3D[brushStackState.activeLabelmapIndex];
labelmap3D.activeSegmentIndex = segmentIndex; labelmap3D.activeSegmentIndex = segmentIndex;
refreshViewport(); refreshViewports();
return segmentIndex; return segmentIndex;
}; };

View File

@ -1,5 +1,6 @@
import { MODULE_TYPES, utils } from '@ohif/core'; import { MODULE_TYPES, utils } from '@ohif/core';
import loadSegmentation from './loadSegmentation'; import loadSegmentation from './loadSegmentation';
import getSourceDisplaySet from './getSourceDisplaySet';
// TODO: Should probably use dcmjs for this // TODO: Should probably use dcmjs for this
const SOP_CLASS_UIDS = { const SOP_CLASS_UIDS = {
@ -54,6 +55,10 @@ export default function getSopClassHandlerModule({ servicesManager }) {
SeriesDescription, SeriesDescription,
}; };
segDisplaySet.getSourceDisplaySet = function(studies) {
return getSourceDisplaySet(studies, segDisplaySet);
};
segDisplaySet.load = function(referencedDisplaySet, studies) { segDisplaySet.load = function(referencedDisplaySet, studies) {
return loadSegmentation(segDisplaySet, referencedDisplaySet, studies); return loadSegmentation(segDisplaySet, referencedDisplaySet, studies);
}; };

View File

@ -0,0 +1,37 @@
import setActiveLabelmap from './utils/setActiveLabelMap';
export default function getSourceDisplaySet(studies, segDisplaySet) {
const referencedDisplaySet = _getReferencedDisplaySet(segDisplaySet, studies);
setActiveLabelmap(referencedDisplaySet, studies, segDisplaySet);
return referencedDisplaySet;
}
const _getReferencedDisplaySet = (segDisplaySet, studies) => {
let allDisplaySets = [];
studies.forEach(study => {
allDisplaySets = allDisplaySets.concat(study.displaySets);
});
const otherDisplaySets = allDisplaySets.filter(
ds => ds.displaySetInstanceUID !== segDisplaySet.displaySetInstanceUID
);
const ReferencedSeriesSequence = Array.isArray(
segDisplaySet.metadata.ReferencedSeriesSequence
)
? segDisplaySet.metadata.ReferencedSeriesSequence
: [segDisplaySet.metadata.ReferencedSeriesSequence];
const referencedSeriesInstanceUIDs = ReferencedSeriesSequence.map(
ReferencedSeries => ReferencedSeries.SeriesInstanceUID
);
const referencedDisplaySet = otherDisplaySets.find(ds =>
referencedSeriesInstanceUIDs.includes(ds.SeriesInstanceUID)
);
return referencedDisplaySet;
};

View File

@ -0,0 +1,5 @@
export default function refreshViewports() {
cornerstone.getEnabledElements().forEach(enabledElement => {
cornerstone.updateImage(enabledElement.element);
});
}

View File

@ -0,0 +1,67 @@
import { utils, log } from '@ohif/core';
import cornerstoneTools from 'cornerstone-tools';
import refreshViewports from './refreshViewports';
const { studyMetadataManager } = utils;
/**
*
*
* @param {*} viewportSpecificData
* @param {*} studies
* @param {*} displaySet
* @param {*} firstImageId
* @param {*} activeLabelmapIndex
* @returns
*/
export default async function setActiveLabelmap(
referencedDisplaySet,
studies,
displaySet,
callback = () => {},
onDisplaySetLoadFailure = err => {
throw new Error(err.message);
}
) {
const studyMetadata = studyMetadataManager.get(
referencedDisplaySet.StudyInstanceUID
);
const firstImageId = studyMetadata.getFirstImageId(
referencedDisplaySet.displaySetInstanceUID
);
let { state } = cornerstoneTools.getModule('segmentation');
let brushStackState = state.series[firstImageId];
const activeLabelmapIndex = brushStackState
? brushStackState.activeLabelmapIndex
: undefined;
if (displaySet.labelmapIndex === activeLabelmapIndex) {
log.warn(`${activeLabelmapIndex} is already the active labelmap`);
return displaySet.labelmapIndex;
}
if (!displaySet.isLoaded) {
const loadPromise = displaySet.load(referencedDisplaySet, studies);
loadPromise.catch(error => {
onDisplaySetLoadFailure(error);
// Return old index.
return activeLabelmapIndex;
});
await loadPromise;
}
// This might have just been created, so need to use the non-cached value.
state = cornerstoneTools.getModule('segmentation').state;
brushStackState = state.series[firstImageId];
brushStackState.activeLabelmapIndex = displaySet.labelmapIndex;
refreshViewports();
callback();
return displaySet.labelmapIndex;
}

View File

@ -139,9 +139,11 @@ export class StudyMetadata extends Metadata {
if (displaySet) { if (displaySet) {
displaySet.sopClassModule = true; displaySet.sopClassModule = true;
displaySet.isDerived if (displaySet.isDerived) {
? this._addDerivedDisplaySet(displaySet) this._addDerivedDisplaySet(displaySet);
: displaySets.push(displaySet); }
displaySets.push(displaySet);
return displaySets; return displaySets;
} }

View File

@ -12,6 +12,8 @@ describe('OHIF Microscopy Extension', () => {
}); });
it('drags and drop a SM thumbnail into viewport', () => { it('drags and drop a SM thumbnail into viewport', () => {
// Waiting for series list to load all displaySets (lots of SRs, before defining which dom element to grab.)
cy.wait(3000);
cy.get('[data-cy="thumbnail-list"]') cy.get('[data-cy="thumbnail-list"]')
.contains('SM') .contains('SM')
.drag('.viewport-drop-target'); .drag('.viewport-drop-target');

View File

@ -80,6 +80,7 @@ const ViewportGrid = function(props) {
// - When updating a panel, ensure that the currently enabled plugin // - When updating a panel, ensure that the currently enabled plugin
// in the viewport is capable of rendering this display set. If not // in the viewport is capable of rendering this display set. If not
// then use the most capable available plugin // then use the most capable available plugin
const pluginName = const pluginName =
!layout.plugin && displaySet && displaySet.plugin !layout.plugin && displaySet && displaySet.plugin
? displaySet.plugin ? displaySet.plugin

View File

@ -4,6 +4,8 @@ import { StudyBrowser } from '@ohif/ui';
import cloneDeep from 'lodash.clonedeep'; import cloneDeep from 'lodash.clonedeep';
import findDisplaySetByUID from './findDisplaySetByUID'; import findDisplaySetByUID from './findDisplaySetByUID';
const { studyMetadataManager } = OHIF.utils;
const { setActiveViewportSpecificData } = OHIF.redux.actions; const { setActiveViewportSpecificData } = OHIF.redux.actions;
// TODO // TODO
@ -39,11 +41,27 @@ const mapStateToProps = (state, ownProps) => {
const mapDispatchToProps = (dispatch, ownProps) => { const mapDispatchToProps = (dispatch, ownProps) => {
return { return {
onThumbnailClick: displaySetInstanceUID => { onThumbnailClick: displaySetInstanceUID => {
const displaySet = findDisplaySetByUID( let displaySet = findDisplaySetByUID(
ownProps.studyMetadata, ownProps.studyMetadata,
displaySetInstanceUID displaySetInstanceUID
); );
if (displaySet.isDerived) {
const { Modality } = displaySet;
displaySet = displaySet.getSourceDisplaySet(ownProps.studyMetadata);
if (!displaySet) {
throw new Error(
`Referenced series for ${Modality} dataset not present.`
);
}
if (!displaySet) {
throw new Error('Source data not present');
}
}
dispatch(setActiveViewportSpecificData(displaySet)); dispatch(setActiveViewportSpecificData(displaySet));
}, },
}; };

View File

@ -133,12 +133,23 @@ class ViewerMain extends Component {
StudyInstanceUID, StudyInstanceUID,
displaySetInstanceUID, displaySetInstanceUID,
}) => { }) => {
const displaySet = this.findDisplaySet( let displaySet = this.findDisplaySet(
this.props.studies, this.props.studies,
StudyInstanceUID, StudyInstanceUID,
displaySetInstanceUID displaySetInstanceUID
); );
if (displaySet.isDerived) {
const { Modality } = displaySet;
displaySet = displaySet.getSourceDisplaySet(this.props.studies);
if (!displaySet) {
throw new Error(
`Referenced series for ${Modality} dataset not present.`
);
}
}
this.props.setViewportSpecificData(viewportIndex, displaySet); this.props.setViewportSpecificData(viewportIndex, displaySet);
}; };