From 0f9b36906f6f67580879d76cb12f2a320883d4a9 Mon Sep 17 00:00:00 2001 From: Joe Boccanfuso <109477394+jbocce@users.noreply.github.com> Date: Wed, 16 Aug 2023 12:51:26 -0400 Subject: [PATCH] feat(thumbnail highlight): Thumbnails of hydrated series are now highlighted (#3594) --- .../src/Viewport/OHIFCornerstoneViewport.tsx | 10 +++++++- .../SegmentationService.ts | 16 ++++++++++++ .../default/src/Panels/PanelStudyBrowser.tsx | 14 +++++++++++ .../PanelStudyBrowserTracking.tsx | 21 ++++++++++++++++ .../DisplaySetService/DisplaySetService.ts | 24 +++++++----------- platform/core/src/types/DisplaySet.ts | 15 +++++++++++ platform/core/src/types/index.ts | 11 ++------ .../configuration/dataSources/dicom-web.md | 7 ++++++ .../services/data/DisplaySetService.md | 3 +++ .../ThumbnailList/ThumbnailList.tsx | 4 +++ .../ThumbnailNoImage/ThumbnailNoImage.tsx | 25 ++++++++++++++++--- 11 files changed, 121 insertions(+), 29 deletions(-) create mode 100644 platform/core/src/types/DisplaySet.ts diff --git a/extensions/cornerstone/src/Viewport/OHIFCornerstoneViewport.tsx b/extensions/cornerstone/src/Viewport/OHIFCornerstoneViewport.tsx index b1c98c2fb..732752476 100644 --- a/extensions/cornerstone/src/Viewport/OHIFCornerstoneViewport.tsx +++ b/extensions/cornerstone/src/Viewport/OHIFCornerstoneViewport.tsx @@ -23,6 +23,7 @@ import CornerstoneOverlays from './Overlays/CornerstoneOverlays'; import getSOPInstanceAttributes from '../utils/measurementServiceMappings/utils/getSOPInstanceAttributes'; import CornerstoneServices from '../types/CornerstoneServices'; import CinePlayer from '../components/CinePlayer'; +import { Types } from '@ohif/core'; const STACK = 'stack'; @@ -264,7 +265,14 @@ const OHIFCornerstoneViewport = React.memo(props => { useEffect(() => { const { unsubscribe } = displaySetService.subscribe( displaySetService.EVENTS.DISPLAY_SET_SERIES_METADATA_INVALIDATED, - async invalidatedDisplaySetInstanceUID => { + async ({ + displaySetInstanceUID: invalidatedDisplaySetInstanceUID, + invalidateData, + }: Types.DisplaySetSeriesMetadataInvalidatedEvent) => { + if (!invalidateData) { + return; + } + const viewportInfo = cornerstoneViewportService.getViewportInfoByIndex( viewportIndex ); diff --git a/extensions/cornerstone/src/services/SegmentationService/SegmentationService.ts b/extensions/cornerstone/src/services/SegmentationService/SegmentationService.ts index eac898ab0..03a4e8461 100644 --- a/extensions/cornerstone/src/services/SegmentationService/SegmentationService.ts +++ b/extensions/cornerstone/src/services/SegmentationService/SegmentationService.ts @@ -1123,6 +1123,8 @@ class SegmentationService extends PubSubService { ); } + this._setDisplaySetIsHydrated(segmentationId, true); + segmentation.hydrated = true; if (!suppressEvents) { @@ -1132,6 +1134,18 @@ class SegmentationService extends PubSubService { } }; + private _setDisplaySetIsHydrated( + displaySetUID: string, + isHydrated: boolean + ): void { + const { + DisplaySetService: displaySetService, + } = this.servicesManager.services; + const displaySet = displaySetService.getDisplaySetByUID(displaySetUID); + displaySet.isHydrated = isHydrated; + displaySetService.setDisplaySetMetadataInvalidated(displaySetUID, false); + } + private _highlightLabelmap( segmentIndex: number, alpha: number, @@ -1303,6 +1317,8 @@ class SegmentationService extends PubSubService { } } + this._setDisplaySetIsHydrated(segmentationId, false); + this._broadcastEvent(this.EVENTS.SEGMENTATION_REMOVED, { segmentationId, }); diff --git a/extensions/default/src/Panels/PanelStudyBrowser.tsx b/extensions/default/src/Panels/PanelStudyBrowser.tsx index 4fe6882ef..512b47d05 100644 --- a/extensions/default/src/Panels/PanelStudyBrowser.tsx +++ b/extensions/default/src/Panels/PanelStudyBrowser.tsx @@ -210,9 +210,22 @@ function PanelStudyBrowser({ } ); + const SubscriptionDisplaySetMetaDataInvalidated = displaySetService.subscribe( + displaySetService.EVENTS.DISPLAY_SET_SERIES_METADATA_INVALIDATED, + () => { + const mappedDisplaySets = _mapDisplaySets( + displaySetService.getActiveDisplaySets(), + thumbnailImageSrcMap + ); + + setDisplaySets(mappedDisplaySets); + } + ); + return () => { SubscriptionDisplaySetsAdded.unsubscribe(); SubscriptionDisplaySetsChanged.unsubscribe(); + SubscriptionDisplaySetMetaDataInvalidated.unsubscribe(); }; // eslint-disable-next-line react-hooks/exhaustive-deps }, []); @@ -335,6 +348,7 @@ function _mapDisplaySets(displaySets, thumbnailImageSrcMap) { displaySetInstanceUID: ds.displaySetInstanceUID, // .. Any other data to pass }, + isHydratedForDerivedDisplaySet: ds.isHydrated, }); }); diff --git a/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/PanelStudyBrowserTracking.tsx b/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/PanelStudyBrowserTracking.tsx index f999f9e9b..46c74526d 100644 --- a/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/PanelStudyBrowserTracking.tsx +++ b/extensions/measurement-tracking/src/panels/PanelStudyBrowserTracking/PanelStudyBrowserTracking.tsx @@ -256,9 +256,29 @@ function PanelStudyBrowserTracking({ } ); + const SubscriptionDisplaySetMetaDataInvalidated = displaySetService.subscribe( + displaySetService.EVENTS.DISPLAY_SET_SERIES_METADATA_INVALIDATED, + () => { + const mappedDisplaySets = _mapDisplaySets( + displaySetService.getActiveDisplaySets(), + thumbnailImageSrcMap, + trackedSeries, + viewports, + viewportGridService, + dataSource, + displaySetService, + uiDialogService, + uiNotificationService + ); + + setDisplaySets(mappedDisplaySets); + } + ); + return () => { SubscriptionDisplaySetsAdded.unsubscribe(); SubscriptionDisplaySetsChanged.unsubscribe(); + SubscriptionDisplaySetMetaDataInvalidated.unsubscribe(); }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [ @@ -467,6 +487,7 @@ function _mapDisplaySets( // .. Any other data to pass }, isTracked: trackedSeriesInstanceUIDs.includes(ds.SeriesInstanceUID), + isHydratedForDerivedDisplaySet: ds.isHydrated, viewportIdentificator, }; diff --git a/platform/core/src/services/DisplaySetService/DisplaySetService.ts b/platform/core/src/services/DisplaySetService/DisplaySetService.ts index a3251dc65..6c4e98abd 100644 --- a/platform/core/src/services/DisplaySetService/DisplaySetService.ts +++ b/platform/core/src/services/DisplaySetService/DisplaySetService.ts @@ -1,17 +1,8 @@ import { ExtensionManager } from '../../extensions'; -import { InstanceMetadata } from '../../types'; +import { DisplaySet, InstanceMetadata } from '../../types'; import { PubSubService } from '../_shared/pubSubServiceInterface'; import EVENTS from './EVENTS'; -export type DisplaySet = { - displaySetInstanceUID: string; - instances: InstanceMetadata[]; - StudyInstanceUID: string; - SeriesInstanceUID?: string; - numImages?: number; - unsupported?: boolean; -}; - const displaySetCache = new Map(); /** @@ -154,7 +145,10 @@ export default class DisplaySetService extends PubSubService { return displaySet; } - public setDisplaySetMetadataInvalidated(displaySetInstanceUID: string): void { + public setDisplaySetMetadataInvalidated( + displaySetInstanceUID: string, + invalidateData = true + ): void { const displaySet = this.getDisplaySetByUID(displaySetInstanceUID); if (!displaySet) { @@ -162,10 +156,10 @@ export default class DisplaySetService extends PubSubService { } // broadcast event to update listeners with the new displaySets - this._broadcastEvent( - EVENTS.DISPLAY_SET_SERIES_METADATA_INVALIDATED, - displaySetInstanceUID - ); + this._broadcastEvent(EVENTS.DISPLAY_SET_SERIES_METADATA_INVALIDATED, { + displaySetInstanceUID, + invalidateData, + }); } public deleteDisplaySet(displaySetInstanceUID) { diff --git a/platform/core/src/types/DisplaySet.ts b/platform/core/src/types/DisplaySet.ts new file mode 100644 index 000000000..3bc1cb516 --- /dev/null +++ b/platform/core/src/types/DisplaySet.ts @@ -0,0 +1,15 @@ +import { InstanceMetadata } from './StudyMetadata'; + +export type DisplaySet = { + displaySetInstanceUID: string; + instances: InstanceMetadata[]; + StudyInstanceUID: string; + SeriesInstanceUID?: string; + numImages?: number; + unsupported?: boolean; +}; + +export type DisplaySetSeriesMetadataInvalidatedEvent = { + displaySetInstanceUID: string; + invalidateData: boolean; +}; diff --git a/platform/core/src/types/index.ts b/platform/core/src/types/index.ts index f126cd1ac..cdbce68e7 100644 --- a/platform/core/src/types/index.ts +++ b/platform/core/src/types/index.ts @@ -2,7 +2,6 @@ import * as Extensions from '../extensions/ExtensionManager'; import * as HangingProtocol from './HangingProtocol'; import Services from './Services'; import Hotkey from '../classes/Hotkey'; -import { DisplaySet } from '../services/DisplaySetService/DisplaySetService'; import { DataSourceDefinition } from './DataSource'; export * from '../services/CustomizationService/types'; @@ -10,6 +9,7 @@ export * from '../services/CustomizationService/types'; export * from './AppConfig'; export * from './Consumer'; export * from './Command'; +export * from './DisplaySet'; export * from './StudyMetadata'; export * from './PanelModule'; export * from './IPubSub'; @@ -19,11 +19,4 @@ export * from './Color'; * Export the types used within the various services and managers, but * not the services/managers themselves, which are exported at the top level. */ -export { - Extensions, - HangingProtocol, - Services, - Hotkey, - DisplaySet, - DataSourceDefinition, -}; +export { Extensions, HangingProtocol, Services, Hotkey, DataSourceDefinition }; diff --git a/platform/docs/docs/configuration/dataSources/dicom-web.md b/platform/docs/docs/configuration/dataSources/dicom-web.md index d4c44a6ba..ddc812536 100644 --- a/platform/docs/docs/configuration/dataSources/dicom-web.md +++ b/platform/docs/docs/configuration/dataSources/dicom-web.md @@ -160,6 +160,13 @@ The following properties can be added to the `configuration` property of each da ##### `dicomUploadEnabled` A boolean indicating if the DICOM upload to the data source is permitted/accepted or not. A value of true provides a link on the OHIF work list page that allows for DICOM files from the local file system to be uploaded to the data source +:::tip +The [OHIF plugin for Orthanc](https://book.orthanc-server.com/plugins/ohif.html) by default utilizes the DICOM JSON data +source and it has been discovered that only those studies uploaded to Orthanc AFTER the plugin has been installed are +available as DICOM JSON. As such, if the OHIF plugin for Orthanc is desired for studies uploaded prior to installing the plugin, +then consider switching to using [DICOMweb instead](https://book.orthanc-server.com/plugins/ohif.html#using-dicomweb). +::: + ![toolbarModule-layout](../../assets/img/uploader.gif) #### `singlepart` diff --git a/platform/docs/docs/platform/services/data/DisplaySetService.md b/platform/docs/docs/platform/services/data/DisplaySetService.md index 12136e987..898ad1388 100644 --- a/platform/docs/docs/platform/services/data/DisplaySetService.md +++ b/platform/docs/docs/platform/services/data/DisplaySetService.md @@ -33,6 +33,7 @@ There are three events that get broadcasted in `DisplaySetService`: | DISPLAY_SETS_ADDED | Fires a displayset is added to the displaysets cache | | DISPLAY_SETS_CHANGED | Fires when a displayset is changed | | DISPLAY_SETS_REMOVED | Fires when a displayset is removed | +| DISPLAY_SET_SERIES_METADATA_INVALIDATED | Fires when a displayset's series metadata has been altered. An object payload for the event is sent with properties: `displaySetInstanceUID` - the UID of the display set affected; `invalidateData` - boolean indicating if data should be invalidated ## API @@ -60,3 +61,5 @@ Let's find out about the public API for `DisplaySetService`. - `deleteDisplaySet`: Deletes the displaySets from the displaySets cache - `addActiveDisplaySets`: Adds a new display set independently of the make operation. + +- `setDisplaySetMetadataInvalidated`: Fires the `DISPLAY_SET_SERIES_METADATA_INVALIDATED` event. diff --git a/platform/ui/src/components/ThumbnailList/ThumbnailList.tsx b/platform/ui/src/components/ThumbnailList/ThumbnailList.tsx index 8b55c8a70..15ff2749a 100644 --- a/platform/ui/src/components/ThumbnailList/ThumbnailList.tsx +++ b/platform/ui/src/components/ThumbnailList/ThumbnailList.tsx @@ -36,6 +36,7 @@ const ThumbnailList = ({ imageSrc, messages, imageAltText, + isHydratedForDerivedDisplaySet, }) => { const isActive = activeDisplaySetInstanceUIDs.includes( displaySetInstanceUID @@ -104,6 +105,9 @@ const ThumbnailList = ({ onThumbnailDoubleClick(displaySetInstanceUID) } viewportIdentificator={viewportIdentificator} + isHydratedForDerivedDisplaySet={ + isHydratedForDerivedDisplaySet + } /> ); default: diff --git a/platform/ui/src/components/ThumbnailNoImage/ThumbnailNoImage.tsx b/platform/ui/src/components/ThumbnailNoImage/ThumbnailNoImage.tsx index 52ccba654..e6afd0c4e 100644 --- a/platform/ui/src/components/ThumbnailNoImage/ThumbnailNoImage.tsx +++ b/platform/ui/src/components/ThumbnailNoImage/ThumbnailNoImage.tsx @@ -20,6 +20,7 @@ const ThumbnailNoImage = ({ messages, dragData, isActive, + isHydratedForDerivedDisplaySet, }) => { const [collectedProps, drag, dragPreview] = useDrag({ type: 'displayset', @@ -32,8 +33,8 @@ const ThumbnailNoImage = ({ return (
- + {modalityTooltip}} > -
+
{modality}
@@ -104,6 +120,7 @@ ThumbnailNoImage.propTypes = { onDoubleClick: PropTypes.func.isRequired, messages: PropTypes.object, isActive: PropTypes.bool.isRequired, + isHydratedForDerivedDisplaySet: PropTypes.bool, }; export default ThumbnailNoImage;