diff --git a/extensions/dicom-rt/src/index.js b/extensions/dicom-rt/src/index.js index f6a278007..ff308e133 100644 --- a/extensions/dicom-rt/src/index.js +++ b/extensions/dicom-rt/src/index.js @@ -1,11 +1,12 @@ import React from 'react'; +import { utils } from '@ohif/core'; + import init from './init.js'; import sopClassHandlerModule from './OHIFDicomRTStructSopClassHandler'; import id from './id.js'; import RTPanel from './components/RTPanel/RTPanel'; import { version } from '../package.json'; -import { utils } from '@ohif/core'; const { studyMetadataManager } = utils; export default { @@ -42,12 +43,48 @@ export default { ); }; + const RTPanelTabChangedEvent = 'rt-panel-tab-updated'; + + /** + * Trigger's an event to update the state of the panel's RoundedButtonGroup. + * + * This is required to avoid extension state + * coupling with the viewer's ToolbarRow component. + * + * @param {object} data + */ + const triggerRTPanelUpdatedEvent = data => { + const event = new CustomEvent(RTPanelTabChangedEvent, { + detail: data, + }); + document.dispatchEvent(event); + }; + + const onRTStructsLoaded = ({ detail }) => { + const { rtStructDisplaySet, referencedDisplaySet } = detail; + + const studyMetadata = studyMetadataManager.get( + rtStructDisplaySet.StudyInstanceUID + ); + const referencedDisplaysets = studyMetadata.getDerivedDatasets({ + referencedSeriesInstanceUID: referencedDisplaySet.SeriesInstanceUID, + Modality: 'RTSTRUCT', + }); + triggerRTPanelUpdatedEvent({ + badgeNumber: referencedDisplaysets.length, + target: 'rt-panel', + }); + }; + + document.addEventListener('extensiondicomrtrtloaded', onRTStructsLoaded); + return { menuOptions: [ { icon: 'list', label: 'RTSTRUCT', target: 'rt-panel', + stateEvent: RTPanelTabChangedEvent, isDisabled: (studies, activeViewport) => { if (!studies) { return true; diff --git a/extensions/dicom-rt/src/loadRTStruct.js b/extensions/dicom-rt/src/loadRTStruct.js index a5323b7f2..d33492fcb 100644 --- a/extensions/dicom-rt/src/loadRTStruct.js +++ b/extensions/dicom-rt/src/loadRTStruct.js @@ -90,11 +90,11 @@ export default async function loadRTStruct( const sopInstanceUID = ContourImageSequence ? ContourImageSequence.ReferencedSOPInstanceUID : _getClosestSOPInstanceUID( - ContourData, - ContourGeometricType, - NumberOfContourPoints, - imageIdSopInstanceUidPairs - ); + ContourData, + ContourGeometricType, + NumberOfContourPoints, + imageIdSopInstanceUidPairs + ); const imageId = _getImageId(imageIdSopInstanceUidPairs, sopInstanceUID); if (!imageId) { @@ -171,7 +171,13 @@ export default async function loadRTStruct( * allows us to easily watch the module or the rtstruct loading process in any other component * without subscribing to external events. */ - const event = new CustomEvent('extensiondicomrtrtloaded'); + const event = new CustomEvent('extensiondicomrtrtloaded', { + detail: { + rtStructDisplaySet, + referencedDisplaySet, + studies, + }, + }); document.dispatchEvent(event); } @@ -299,7 +305,9 @@ const _getImageId = (imageIdSopInstanceUidPairs, sopInstanceUID) => { imageIdSopInstanceUidPairsEntry.sopInstanceUID === sopInstanceUID ); - return imageIdSopInstanceUidPairsEntry ? imageIdSopInstanceUidPairsEntry.imageId : null; + return imageIdSopInstanceUidPairsEntry + ? imageIdSopInstanceUidPairsEntry.imageId + : null; }; function _getImageIdSopInstanceUidPairsForDisplaySet( diff --git a/extensions/dicom-segmentation/src/components/SegmentationPanel/SegmentationPanel.js b/extensions/dicom-segmentation/src/components/SegmentationPanel/SegmentationPanel.js index 02ece632f..d143a3fdd 100644 --- a/extensions/dicom-segmentation/src/components/SegmentationPanel/SegmentationPanel.js +++ b/extensions/dicom-segmentation/src/components/SegmentationPanel/SegmentationPanel.js @@ -224,16 +224,20 @@ const SegmentationPanel = ({ ) ); }; - }, [activeIndex, viewports]); + }, [ + activeIndex, + updateSegmentationComboBox, + viewports, + ]); - const updateSegmentationComboBox = (e) => { + const updateSegmentationComboBox = e => { const index = e.detail.activatedLabelmapIndex; if (index !== -1) { setState(state => ({ ...state, selectedSegmentation: index })); } else { cleanSegmentationComboBox(); } - } + }; const cleanSegmentationComboBox = () => { setState(state => ({ @@ -245,7 +249,7 @@ const SegmentationPanel = ({ isDisabled: true, selectedSegmentation: -1, })); - } + }; const refreshSegmentations = () => { const activeViewport = getActiveViewport(); @@ -309,7 +313,8 @@ const SegmentationPanel = ({ ); const filteredReferencedSegDisplaysets = referencedSegDisplaysets.filter( - (segDisplay => segDisplay.loadError !== true)); + segDisplay => segDisplay.loadError !== true + ); return filteredReferencedSegDisplaysets.map((displaySet, index) => { const { diff --git a/extensions/dicom-segmentation/src/index.js b/extensions/dicom-segmentation/src/index.js index 33123b23e..c36d3fab7 100644 --- a/extensions/dicom-segmentation/src/index.js +++ b/extensions/dicom-segmentation/src/index.js @@ -1,9 +1,12 @@ import React from 'react'; +import OHIF from '@ohif/core'; + import init from './init.js'; import toolbarModule from './toolbarModule.js'; import getSopClassHandlerModule from './getOHIFDicomSegSopClassHandler.js'; import SegmentationPanel from './components/SegmentationPanel/SegmentationPanel.js'; import { version } from '../package.json'; +const { studyMetadataManager } = OHIF.utils; export default { /** @@ -79,12 +82,50 @@ export default { ); }; + const SegmentationPanelTabUpdatedEvent = 'segmentation-panel-tab-updated'; + + /** + * Trigger's an event to update the state of the panel's RoundedButtonGroup. + * + * This is required to avoid extension state + * coupling with the viewer's ToolbarRow component. + * + * @param {object} data + */ + const triggerSegmentationPanelTabUpdatedEvent = data => { + const event = new CustomEvent(SegmentationPanelTabUpdatedEvent, { + detail: data, + }); + document.dispatchEvent(event); + }; + + const onSegmentationsLoaded = ({ detail }) => { + const { segDisplaySet, segMetadata } = detail; + const studyMetadata = studyMetadataManager.get( + segDisplaySet.StudyInstanceUID + ); + const referencedDisplaysets = studyMetadata.getDerivedDatasets({ + referencedSeriesInstanceUID: segMetadata.seriesInstanceUid, + Modality: 'SEG', + }); + triggerSegmentationPanelTabUpdatedEvent({ + badgeNumber: referencedDisplaysets.length, + target: 'segmentation-panel', + }); + }; + + document.addEventListener( + 'extensiondicomsegmentationsegloaded', + onSegmentationsLoaded + ); + return { menuOptions: [ { icon: 'list', label: 'Segmentations', target: 'segmentation-panel', + stateEvent: SegmentationPanelTabUpdatedEvent, isDisabled: studies => { if (!studies) { return true; diff --git a/extensions/dicom-segmentation/src/loadSegmentation.js b/extensions/dicom-segmentation/src/loadSegmentation.js index a655c0687..e75ac1472 100644 --- a/extensions/dicom-segmentation/src/loadSegmentation.js +++ b/extensions/dicom-segmentation/src/loadSegmentation.js @@ -59,7 +59,16 @@ export default async function loadSegmentation( * without subscribing to external events. */ console.log('Segmentation loaded.'); - const event = new CustomEvent('extensiondicomsegmentationsegloaded'); + const event = new CustomEvent('extensiondicomsegmentationsegloaded', { + detail: { + imageIds, + segDisplaySet, + labelmapBuffer, + segMetadata, + segmentsOnFrame, + labelmapSegments, + }, + }); document.dispatchEvent(event); return labelmapIndex; diff --git a/platform/core/src/services/MeasurementService/MeasurementService.js b/platform/core/src/services/MeasurementService/MeasurementService.js index a21ea8837..ce58cff9e 100644 --- a/platform/core/src/services/MeasurementService/MeasurementService.js +++ b/platform/core/src/services/MeasurementService/MeasurementService.js @@ -260,7 +260,6 @@ class MeasurementService { const sourceInfo = this._getSourceInfo(source); if (!definition) { - console.log('TEST'); log.warn('No source definition provided. Exiting early.'); return; } diff --git a/platform/ui/src/components/roundedButtonGroup/RoundedButtonGroup.css b/platform/ui/src/components/roundedButtonGroup/RoundedButtonGroup.css index 3bf03e5e0..f5d96fd97 100644 --- a/platform/ui/src/components/roundedButtonGroup/RoundedButtonGroup.css +++ b/platform/ui/src/components/roundedButtonGroup/RoundedButtonGroup.css @@ -4,6 +4,29 @@ z-index: 0; } +.RoundedButtonGroup .badgeNumber-container { + background-color: var(--ui-border-color-dark); + width: 21px; + height: 21px; + justify-content: center; + align-items: center; + display: flex; + border-radius: 50%; + font-size: 12px; + font-weight: bold; + bottom: 10px; + left: 5px; + color: white; + position: absolute; +} + +.RoundedButtonGroup .badgeNumber { + font-size: 10px; + font-weight: bold; + display: block; + width: 25px; +} + .roundedButtonWrapper { cursor: pointer; display: inline-block; @@ -36,7 +59,12 @@ z-index: 1; } -.roundedButtonWrapper .roundedButton svg .roundedButtonWrapper .roundedButton span { +.roundedButtonWrapper + .roundedButton + svg + .roundedButtonWrapper + .roundedButton + span { margin: 0 2px; } diff --git a/platform/ui/src/components/roundedButtonGroup/RoundedButtonGroup.js b/platform/ui/src/components/roundedButtonGroup/RoundedButtonGroup.js index cd861f146..8c167df42 100644 --- a/platform/ui/src/components/roundedButtonGroup/RoundedButtonGroup.js +++ b/platform/ui/src/components/roundedButtonGroup/RoundedButtonGroup.js @@ -13,6 +13,7 @@ class RoundedButtonGroup extends Component { PropTypes.shape({ value: PropTypes.any, label: PropTypes.string, + stateEvent: PropTypes.string, icon: PropTypes.oneOfType([ PropTypes.string, PropTypes.shape({ @@ -30,6 +31,15 @@ class RoundedButtonGroup extends Component { value: null, }; + state = { + badgeNumbers: [], + }; + + constructor() { + super(); + this.onStateEvent = this.onStateEvent.bind(this); + } + onClickOption = value => { let newValue = value; if (this.props.value === value) { @@ -41,6 +51,48 @@ class RoundedButtonGroup extends Component { } }; + onStateEvent(event) { + const optionIndex = this.props.options.findIndex( + o => o.value === event.detail.target + ); + if (optionIndex > -1) { + const badgeNumbers = this.state.badgeNumbers; + badgeNumbers[optionIndex] = event.detail.badgeNumber; + this.setState({ badgeNumbers }); + } + } + + componentDidMount() { + this.props.options.forEach(option => { + if (option.stateEvent) { + document.addEventListener(option.stateEvent, this.onStateEvent); + } + }); + } + + componentDidUpdate(prevProps, prevState) { + this.props.options.forEach((option, index) => { + if ( + option.stateEvent && + option.stateEvent !== + (prevProps.options[index] + ? prevProps.options[index].stateEvent + : null) + ) { + document.removeEventListener(option.stateEvent, this.onStateEvent); + document.addEventListener(option.stateEvent, this.onStateEvent); + } + }); + } + + componentWillUnmount() { + this.props.options.forEach(option => { + if (option.stateEvent) { + document.removeEventListener(option.stateEvent, this.onStateEvent); + } + }); + } + render() { let className = classnames( RoundedButtonGroup.className, @@ -62,6 +114,14 @@ class RoundedButtonGroup extends Component {
{option.bottomLabel}
); + let badgeNumber = this.state.badgeNumbers[index]; + const badgeNumberOverflow = String(badgeNumber).length > 2; + badgeNumber = badgeNumber + ? badgeNumberOverflow + ? 99 + : badgeNumber + : null; + return (
{optionText} + {badgeNumber && ( +
+ + {badgeNumber} + {badgeNumberOverflow && '+'} + +
+ )} {iconProps && }
{bottomLabel} diff --git a/platform/viewer/src/connectedComponents/ToolbarRow.js b/platform/viewer/src/connectedComponents/ToolbarRow.js index 9d4745248..12acf31af 100644 --- a/platform/viewer/src/connectedComponents/ToolbarRow.js +++ b/platform/viewer/src/connectedComponents/ToolbarRow.js @@ -96,6 +96,8 @@ class ToolbarRow extends Component { value: menuOption.target, icon: menuOption.icon, bottomLabel: menuOption.label, + badgeNumber: menuOption.badgeNumber, + stateEvent: menuOption.stateEvent, }; const from = menuOption.from || 'right';