[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 loadRTStruct from './loadRTStruct';
import getSourceDisplaySet from './getSourceDisplaySet';
import id from './id';
@ -67,6 +68,10 @@ const OHIFDicomRTStructSopClassHandler = {
}
}
rtStructDisplaySet.getSourceDisplaySet = function(studies) {
return getSourceDisplaySet(studies, rtStructDisplaySet);
};
rtStructDisplaySet.load = function(referencedDisplaySet, studies) {
return loadRTStruct(
rtStructDisplaySet,

View File

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

View File

@ -1,5 +1,6 @@
import { MODULE_TYPES, utils } from '@ohif/core';
import loadSegmentation from './loadSegmentation';
import getSourceDisplaySet from './getSourceDisplaySet';
// TODO: Should probably use dcmjs for this
const SOP_CLASS_UIDS = {
@ -54,6 +55,10 @@ export default function getSopClassHandlerModule({ servicesManager }) {
SeriesDescription,
};
segDisplaySet.getSourceDisplaySet = function(studies) {
return getSourceDisplaySet(studies, segDisplaySet);
};
segDisplaySet.load = function(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) {
displaySet.sopClassModule = true;
displaySet.isDerived
? this._addDerivedDisplaySet(displaySet)
: displaySets.push(displaySet);
if (displaySet.isDerived) {
this._addDerivedDisplaySet(displaySet);
}
displaySets.push(displaySet);
return displaySets;
}

View File

@ -12,6 +12,8 @@ describe('OHIF Microscopy Extension', () => {
});
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"]')
.contains('SM')
.drag('.viewport-drop-target');

View File

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

View File

@ -4,6 +4,8 @@ import { StudyBrowser } from '@ohif/ui';
import cloneDeep from 'lodash.clonedeep';
import findDisplaySetByUID from './findDisplaySetByUID';
const { studyMetadataManager } = OHIF.utils;
const { setActiveViewportSpecificData } = OHIF.redux.actions;
// TODO
@ -39,11 +41,27 @@ const mapStateToProps = (state, ownProps) => {
const mapDispatchToProps = (dispatch, ownProps) => {
return {
onThumbnailClick: displaySetInstanceUID => {
const displaySet = findDisplaySetByUID(
let displaySet = findDisplaySetByUID(
ownProps.studyMetadata,
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));
},
};

View File

@ -133,12 +133,23 @@ class ViewerMain extends Component {
StudyInstanceUID,
displaySetInstanceUID,
}) => {
const displaySet = this.findDisplaySet(
let displaySet = this.findDisplaySet(
this.props.studies,
StudyInstanceUID,
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);
};