Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 50 additions & 10 deletions src/components/DicomTagBrowser/DicomTagBrowser.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { SearchOutlined } from '@ant-design/icons'
import { EyeOutlined, SearchOutlined } from '@ant-design/icons'
import { Input, Select, Slider, Table, Typography } from 'antd'
import { useEffect, useMemo, useState } from 'react'

import type DicomWebManager from '../../DicomWebManager'
import './DicomTagBrowser.css'
import { useActiveSeries } from '../../hooks/useActiveSeries'
import { useDebounce } from '../../hooks/useDebounce'
import { useSlides } from '../../hooks/useSlides'
import DicomMetadataStore, {
Expand Down Expand Up @@ -48,6 +49,7 @@ const DicomTagBrowser = ({
seriesInstanceUID = '',
}: DicomTagBrowserProps): JSX.Element => {
const { slides, isLoading } = useSlides({ clients, studyInstanceUID })
const activeSeriesUIDs = useActiveSeries()
const [study, setStudy] = useState<Study | undefined>(undefined)

const [displaySets, setDisplaySets] = useState<DisplaySet[]>([])
Expand Down Expand Up @@ -207,6 +209,7 @@ const DicomTagBrowser = ({
SeriesNumber = '',
SeriesDescription = '',
Modality = '',
SeriesInstanceUID,
} = displaySet

const dateStr = `${SeriesDate}:${SeriesTime}`.split('.')[0]
Expand All @@ -216,6 +219,7 @@ const DicomTagBrowser = ({
value: index,
label: `${SeriesNumber} (${Modality}): ${SeriesDescription}`,
description: displayDate,
seriesInstanceUID: SeriesInstanceUID ?? '',
}
})
}, [sortedDisplaySets])
Expand Down Expand Up @@ -452,18 +456,54 @@ const DicomTagBrowser = ({
optionLabelProp="label"
optionFilterProp="label"
>
{displaySetList.map((item) => (
<Option key={item.value} value={item.value} label={item.label}>
<div>
<div>{item.label}</div>
{displaySetList.map((item) => {
const isActive = item.seriesInstanceUID
? activeSeriesUIDs.has(item.seriesInstanceUID)
: false
return (
<Option
key={item.value}
value={item.value}
label={item.label}
>
<div
style={{ fontSize: '12px', color: 'rgba(0, 0, 0, 0.45)' }}
style={{
display: 'flex',
alignItems: 'flex-start',
gap: 8,
minWidth: 0,
}}
>
{item.description}
<span
style={{
width: 16,
flexShrink: 0,
display: 'flex',
justifyContent: 'center',
}}
title={isActive ? 'Active in viewport' : undefined}
>
{isActive ? (
<EyeOutlined
style={{ color: 'rgba(0, 0, 0, 0.65)' }}
/>
) : null}
</span>
<div style={{ minWidth: 0, flex: 1 }}>
<div>{item.label}</div>
<div
style={{
fontSize: '12px',
color: 'rgba(0, 0, 0, 0.45)',
}}
>
{item.description}
</div>
</div>
</div>
</div>
</Option>
))}
</Option>
)
})}
</Select>
</div>

Expand Down
36 changes: 36 additions & 0 deletions src/components/SlideViewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import {
} from 'react-icons/fa'
import { runValidations } from '../contexts/ValidationContext'
import { StorageClasses } from '../data/uids'
import { ActiveSeriesService } from '../services/ActiveSeriesService'
import DicomMetadataStore from '../services/DICOMMetadataStore'
import NotificationMiddleware, {
NotificationMiddlewareContext,
Expand Down Expand Up @@ -321,6 +322,37 @@ class SlideViewer extends React.Component<SlideViewerProps, SlideViewerState> {
})
}

/**
* Publish active series (image + visible derived data) to ActiveSeriesService
* for use by the DICOM Tag Browser to show eye icons.
*/
private publishActiveSeriesToService = (): void => {
try {
const activeImageSeriesUID = this.props.seriesInstanceUID ?? ''
const derivedSet = new Set<string>()

this.volumeViewer.getAllAnnotationGroups().forEach((ag) => {
if (this.state.visibleAnnotationGroupUIDs.has(ag.uid)) {
derivedSet.add(ag.seriesInstanceUID)
}
})
this.volumeViewer.getAllSegments().forEach((segment) => {
if (this.state.visibleSegmentUIDs.has(segment.uid)) {
derivedSet.add(segment.seriesInstanceUID)
}
})
this.volumeViewer.getAllParameterMappings().forEach((mapping) => {
if (this.state.visibleMappingUIDs.has(mapping.uid)) {
derivedSet.add(mapping.seriesInstanceUID)
}
})

ActiveSeriesService.setActiveSeries(activeImageSeriesUID, derivedSet)
} catch {
// volumeViewer may be in a transitional state
}
}

componentDidUpdate(
previousProps: SlideViewerProps,
_previousState: SlideViewerState,
Expand Down Expand Up @@ -391,6 +423,8 @@ class SlideViewer extends React.Component<SlideViewerProps, SlideViewerState> {
})
this.populateViewports()
}

this.publishActiveSeriesToService()
}

/**
Expand Down Expand Up @@ -2128,6 +2162,7 @@ class SlideViewer extends React.Component<SlideViewerProps, SlideViewerState> {
}

componentWillUnmount = (): void => {
ActiveSeriesService.clear()
this.volumeViewer.cleanup()
if (this.labelViewer !== null && this.labelViewer !== undefined) {
this.labelViewer.cleanup()
Expand Down Expand Up @@ -2198,6 +2233,7 @@ class SlideViewer extends React.Component<SlideViewerProps, SlideViewerState> {
componentDidMount = (): void => {
this.componentSetup()
this.populateViewports()
this.publishActiveSeriesToService()

if (!this.props.slide.areVolumeImagesMonochrome) {
let hasICCProfile = false
Expand Down
22 changes: 22 additions & 0 deletions src/hooks/useActiveSeries.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { useEffect, useState } from 'react'

import { ActiveSeriesService } from '../services/ActiveSeriesService'

/**
* Subscribe to active series (image + visible derived data) for the DICOM Tag Browser.
* Returns the set of SeriesInstanceUIDs that are currently active in the viewport.
*/
export function useActiveSeries(): Set<string> {
const [activeSeriesUIDs, setActiveSeriesUIDs] = useState<Set<string>>(() =>
ActiveSeriesService.getActiveSeriesUIDs(),
)

useEffect(() => {
const unsubscribe = ActiveSeriesService.subscribe(() => {
setActiveSeriesUIDs(ActiveSeriesService.getActiveSeriesUIDs())
})
return unsubscribe
}, [])

return activeSeriesUIDs
}
94 changes: 94 additions & 0 deletions src/services/ActiveSeriesService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
/**
* Centralized service for tracking which series are currently "active" in the
* viewport - i.e., the active image and any visible derived data (annotations,
* segmentations, parametric maps, etc.). Used by the DICOM Tag Browser to show
* an eye icon next to active series.
*/

export interface ActiveSeriesState {
/** The primary image series displayed in the viewport (from URL) */
activeImageSeriesUID: string
/** Derived series with visible overlays (annotations, segmentations, etc.) */
activeDerivedSeriesUIDs: Set<string>
}

type Listener = (state: ActiveSeriesState) => void

class ActiveSeriesServiceImpl {
private activeImageSeriesUID = ''
private activeDerivedSeriesUIDs = new Set<string>()
private listeners: Listener[] = []

/** Get all series UIDs that are currently active (image + derived) */
getActiveSeriesUIDs(): Set<string> {
const result = new Set(this.activeDerivedSeriesUIDs)
if (this.activeImageSeriesUID) {
result.add(this.activeImageSeriesUID)
}
return result
}

/** Get the current state */
getState(): ActiveSeriesState {
return {
activeImageSeriesUID: this.activeImageSeriesUID,
activeDerivedSeriesUIDs: new Set(this.activeDerivedSeriesUIDs),
}
}

/** Check if a series is active (image or has visible derived data) */
isSeriesActive(seriesInstanceUID: string): boolean {
return (
seriesInstanceUID === this.activeImageSeriesUID ||
this.activeDerivedSeriesUIDs.has(seriesInstanceUID)
)
}

/** Update the active series state. Called by SlideViewer and route-aware components. */
setActiveSeries(
activeImageSeriesUID: string,
activeDerivedSeriesUIDs: Iterable<string>,
): void {
const derivedSet = new Set(activeDerivedSeriesUIDs)
const imageChanged = this.activeImageSeriesUID !== activeImageSeriesUID
const derivedChanged =
derivedSet.size !== this.activeDerivedSeriesUIDs.size ||
[...derivedSet].some((uid) => !this.activeDerivedSeriesUIDs.has(uid))

if (!imageChanged && !derivedChanged) {
return
}

this.activeImageSeriesUID = activeImageSeriesUID
this.activeDerivedSeriesUIDs = derivedSet
this._notify()
}

/** Clear all active series (e.g. when navigating away from the viewer). */
clear(): void {
this.activeImageSeriesUID = ''
this.activeDerivedSeriesUIDs = new Set()
this._notify()
}

/** Subscribe to active series changes. Returns unsubscribe function. */
subscribe(listener: Listener): () => void {
this.listeners.push(listener)
return () => {
this.listeners = this.listeners.filter((l) => l !== listener)
}
}

private _notify(): void {
const state = this.getState()
this.listeners.forEach((listener) => {
try {
listener(state)
} catch (err) {
console.error('[ActiveSeriesService] Listener error:', err)
}
})
}
}

export const ActiveSeriesService = new ActiveSeriesServiceImpl()