diff --git a/Dockerfile b/Dockerfile index c87fcfa..29b0635 100644 --- a/Dockerfile +++ b/Dockerfile @@ -37,8 +37,11 @@ COPY craco.config.js . COPY tsconfig.json . COPY types ./types COPY public ./public +COPY scripts ./scripts COPY src ./src +RUN chmod +x scripts/*.sh + FROM lib AS app diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index d38dc61..9922e6c 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -1,3 +1,10 @@ +## [0.45.1](https://github.com/ImagingDataCommons/slim/compare/v0.45.0...v0.45.1) (2025-12-17) + + +### Bug Fixes + +* **about modal:** Address hash + disable select dialog for bulk ann + auto load ([#360](https://github.com/ImagingDataCommons/slim/issues/360)) ([2336601](https://github.com/ImagingDataCommons/slim/commit/2336601924fcc6c450b9478d16cec4d386419757)) + # [0.44.0](https://github.com/ImagingDataCommons/slim/compare/v0.43.1...v0.44.0) (2025-10-27) diff --git a/package.json b/package.json index 4c2f50c..09c5d73 100644 --- a/package.json +++ b/package.json @@ -1,16 +1,17 @@ { "name": "slim", - "version": "0.44.0", + "version": "0.45.1", "private": true, "author": "ImagingDataCommons", "scripts": { - "start": "rm -rf ./node_modules/.cache/default-development && craco start", - "build": "craco build", - "build:firebase": "REACT_APP_CONFIG=gcp PUBLIC_URL=/ craco build", + "start": "rm -rf ./node_modules/.cache/default-development && ./scripts/set-git-env.sh craco start", + "dev": "npm run start", + "build": "./scripts/set-git-env.sh craco build", + "build:firebase": "REACT_APP_CONFIG=gcp PUBLIC_URL=/ ./scripts/set-git-env.sh craco build", "lint": "ts-standard --env jest 'src/**/*.{tsx,ts}'", "fmt": "ts-standard --env jest 'src/**/*.{tsx,ts}' --fix", "test": "ts-standard --env jest 'src/**/*.{tsx,ts}' && craco test --watchAll=false", - "predeploy": "REACT_APP_CONFIG=demo PUBLIC_URL='https://imagingdatacommons.github.io/slim/' craco build", + "predeploy": "REACT_APP_CONFIG=demo PUBLIC_URL='https://imagingdatacommons.github.io/slim/' ./scripts/set-git-env.sh craco build", "deploy": "gh-pages -d build", "clean": "rm -rf ./build ./node_modules" }, @@ -57,7 +58,7 @@ "craco-less": "^2.0.0", "dcmjs": "^0.35.0", "detect-browser": "^5.2.1", - "dicom-microscopy-viewer": "^0.48.15", + "dicom-microscopy-viewer": "^0.48.16", "dicomweb-client": "^0.10.3", "gh-pages": "^5.0.0", "oidc-client": "^1.11.5", diff --git a/scripts/set-git-env.sh b/scripts/set-git-env.sh new file mode 100755 index 0000000..b0bb7a3 --- /dev/null +++ b/scripts/set-git-env.sh @@ -0,0 +1,20 @@ +#!/bin/bash +# Script to set git SHA environment variables for build + +# Get current repository git SHA +if [ -d .git ] || git rev-parse --git-dir > /dev/null 2>&1; then + export REACT_APP_GIT_SHA=$(git rev-parse HEAD 2>/dev/null || echo '') +else + export REACT_APP_GIT_SHA='' +fi + +# Get dicom-microscopy-viewer git SHA +if [ -d ../dicom-microscopy-viewer/.git ] || (cd ../dicom-microscopy-viewer && git rev-parse --git-dir > /dev/null 2>&1); then + export REACT_APP_DMV_GIT_SHA=$(cd ../dicom-microscopy-viewer && git rev-parse HEAD 2>/dev/null || echo '') +else + export REACT_APP_DMV_GIT_SHA='' +fi + +# Execute the command passed as arguments +exec "$@" + diff --git a/src/components/CaseViewer.tsx b/src/components/CaseViewer.tsx index 5dc79ba..5cd586c 100644 --- a/src/components/CaseViewer.tsx +++ b/src/components/CaseViewer.tsx @@ -23,6 +23,8 @@ const { naturalizeDataset } = dcmjs.data.DicomMetaDictionary interface NaturalizedInstance { SeriesInstanceUID: string SOPInstanceUID: string + FrameOfReferenceUID?: string + ContainerIdentifier?: string ReferencedSeriesSequence?: Array<{ SeriesInstanceUID: string }> @@ -40,11 +42,6 @@ interface NaturalizedInstance { }> } -interface ReferencedSlideResult { - slide: Slide | undefined - metadata: NaturalizedInstance -} - const findSeriesSlide = (slides: Slide[], seriesInstanceUID: string): Slide | undefined => { return slides.find((slide: Slide) => { return slide.seriesInstanceUIDs.find((uid: string) => { @@ -82,65 +79,57 @@ function ParametrizedSlideViewer ({ const [derivedDataset, setDerivedDataset] = useState(null) useEffect(() => { - const seriesSlide = findSeriesSlide(slides, seriesInstanceUID) - if (seriesSlide !== null) { - setSelectedSlide(seriesSlide) - } - }, [seriesInstanceUID, slides]) + const currentSlideMatchesSeries = selectedSlide?.seriesInstanceUIDs.some((uid: string) => uid === seriesInstanceUID) ?? false - useEffect(() => { - const findReferencedSlide = async ({ clients, studyInstanceUID, seriesInstanceUID }: { - clients: { [key: string]: DicomWebManager } - studyInstanceUID: string - seriesInstanceUID: string - }): Promise => await new Promise((resolve, reject) => { - try { - const allClients = Object.values(StorageClasses).map((storageClass) => clients[storageClass]) - Promise.all(allClients.map(async (client) => { - const seriesMetadata = await client.retrieveSeriesMetadata({ - studyInstanceUID: studyInstanceUID, - seriesInstanceUID: seriesInstanceUID - }) - const [naturalizedSeriesMetadata] = seriesMetadata.map((metadata) => naturalizeDataset(metadata)) as NaturalizedInstance[] + if (selectedSlide === null || selectedSlide === undefined || !currentSlideMatchesSeries) { + const imageSlide = findSeriesSlide(slides, seriesInstanceUID) + if (imageSlide !== null && imageSlide !== undefined) { + setSelectedSlide(imageSlide) + setDerivedDataset(null) + return + } - if (naturalizedSeriesMetadata.ReferencedSeriesSequence != null) { - const referencedSeriesInstanceUID = naturalizedSeriesMetadata.ReferencedSeriesSequence[0].SeriesInstanceUID + const findReferencedSlide = async (): Promise => { + const client = clients[StorageClasses.VL_WHOLE_SLIDE_MICROSCOPY_IMAGE] + const derivedSeriesMetadata = await client.retrieveSeriesMetadata({ + studyInstanceUID: studyInstanceUID, + seriesInstanceUID: seriesInstanceUID + }) + const naturalizedDerivedMetadata = naturalizeDataset(derivedSeriesMetadata[0]) as NaturalizedInstance + if ( + naturalizedDerivedMetadata.ReferencedSeriesSequence != null && + naturalizedDerivedMetadata.ReferencedSeriesSequence.length > 0 + ) { + for (const referencedSeries of naturalizedDerivedMetadata.ReferencedSeriesSequence) { + const referencedImageSeriesUID = referencedSeries.SeriesInstanceUID const referencedSlide = slides.find((slide: Slide) => { - return slide.seriesInstanceUIDs.find((uid: string) => { - return uid === referencedSeriesInstanceUID - }) + return slide.seriesInstanceUIDs.some((uid: string) => uid === referencedImageSeriesUID) }) - resolve({ slide: referencedSlide, metadata: naturalizedSeriesMetadata }) + if (referencedSlide !== null && referencedSlide !== undefined) { + setSelectedSlide(referencedSlide) + setDerivedDataset(naturalizedDerivedMetadata) + console.log('naturalizedDerivedMetadata', naturalizedDerivedMetadata) + return + } } - - const IMAGE_LIBRARY_CONCEPT_NAME_CODE = '111028' - const imageLibrary = naturalizedSeriesMetadata.ContentSequence?.find( - contentItem => contentItem.ConceptNameCodeSequence[0].CodeValue === IMAGE_LIBRARY_CONCEPT_NAME_CODE - ) - if ((imageLibrary?.ContentSequence?.[0]?.ContentSequence?.[0]?.ReferencedSOPSequence?.[0]) != null) { - const referencedSOPInstanceUID = imageLibrary.ContentSequence[0].ContentSequence[0].ReferencedSOPSequence[0].ReferencedSOPInstanceUID - const referencedSlide = slides.find((slide: Slide) => { - return slide.volumeImages.find((image: { SOPInstanceUID: string }) => { - return image.SOPInstanceUID === referencedSOPInstanceUID - }) + } + const IMAGE_LIBRARY_CONCEPT_NAME_CODE = '111028' + const imageLibrary = naturalizedDerivedMetadata.ContentSequence?.find( + contentItem => contentItem.ConceptNameCodeSequence[0].CodeValue === IMAGE_LIBRARY_CONCEPT_NAME_CODE + ) + if ((imageLibrary?.ContentSequence?.[0]?.ContentSequence?.[0]?.ReferencedSOPSequence?.[0]) != null) { + const referencedSOPInstanceUID = imageLibrary.ContentSequence[0].ContentSequence[0].ReferencedSOPSequence[0].ReferencedSOPInstanceUID + const referencedSlide = slides.find((slide: Slide) => { + return slide.volumeImages.find((image: { SOPInstanceUID: string }) => { + return image.SOPInstanceUID === referencedSOPInstanceUID }) - resolve({ slide: referencedSlide, metadata: naturalizedSeriesMetadata }) - } - })).catch(reject) - } catch (error) { - reject(error) + }) + setSelectedSlide(referencedSlide) + setDerivedDataset(naturalizedDerivedMetadata) + } } - }) - if (selectedSlide === null || selectedSlide === undefined) { - void findReferencedSlide({ clients, studyInstanceUID, seriesInstanceUID }).then((result: ReferencedSlideResult | null) => { - if (result !== null && result !== undefined) { - setSelectedSlide(result.slide) - setDerivedDataset(result.metadata) - } - }).catch(error => { - console.error('Error finding referenced slide:', error) - }) + void findReferencedSlide() } }, [slides, clients, studyInstanceUID, seriesInstanceUID, selectedSlide]) diff --git a/src/components/Header.tsx b/src/components/Header.tsx index 08f41a1..72a3b11 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -11,7 +11,8 @@ import { Badge, Collapse, Radio, - Tooltip + Tooltip, + Typography } from 'antd' import { ApiOutlined, @@ -37,6 +38,47 @@ import AppConfig from '../AppConfig' import DicomTagBrowser from './DicomTagBrowser/DicomTagBrowser' import DicomWebManager from '../DicomWebManager' +const aboutModalCopyTooltips: [React.ReactNode, React.ReactNode] = ['Copy hash', 'Copied!'] + +const aboutModalStyles: Record = { + container: { + textAlign: 'center', + lineHeight: 1.6, + paddingRight: 25, + fontSize: '1rem' + }, + title: { + fontSize: '1.4rem', + fontWeight: 700, + marginBottom: 4 + }, + subtitle: { + fontSize: '1.15rem', + fontWeight: 600, + marginBottom: 12 + }, + link: { + display: 'inline-block', + marginBottom: 16 + }, + section: { + marginBottom: 12 + }, + label: { + fontWeight: 600, + display: 'block', + marginBottom: 4 + }, + bodyText: { + marginBottom: 4 + }, + code: { + display: 'inline-block', + wordBreak: 'break-all', + fontSize: '0.85rem' + } +} + interface HeaderProps extends RouteComponentProps { app: { name: string @@ -182,27 +224,50 @@ class Header extends React.Component { 'unknown' const viewerVersion = viewerVersionRaw.replace(/^[^0-9]*/, '') + const renderHashText = (hash: string | null): JSX.Element => { + if (hash == null) { + return ( + + unknown + + ) + } + return ( + + {hash} + + ) + } + Modal.info({ width: 480, title: null, centered: true, content: ( -
-
{this.props.app.name}
-
{this.props.app.version}
- - -
-
Commit Hash
- {slimCommit ?? 'unknown'} +
+ + + {this.props.app.name} + + + + {this.props.app.version} + + +
+ Commit Hash + {renderHashText(slimCommit)}
-
-
+ -
Version {viewerVersion}
- {viewerCommit ?? 'unknown'} + + + Version {viewerVersion} + + {renderHashText(viewerCommit)}
-
-
Current Browser & OS
-
+
+ Current Browser & OS + {environment.browser.name ?? 'Unknown'} {environment.browser.version ?? ''} -
-
{environment.os.name ?? 'Unknown OS'}
+ + + {environment.os.name ?? 'Unknown OS'} +
), diff --git a/src/components/SlideViewer.tsx b/src/components/SlideViewer.tsx index bc34bda..0b9aecf 100644 --- a/src/components/SlideViewer.tsx +++ b/src/components/SlideViewer.tsx @@ -350,6 +350,7 @@ class SlideViewer extends React.Component { activeOpticalPathIdentifiers, presentationStates: [], loadingFrames: new Set(), + selectedSeriesInstanceUID: undefined, validXCoordinateRange: [offset[0], offset[0] + size[0]], validYCoordinateRange: [offset[1], offset[1] + size[1]] }) @@ -632,7 +633,8 @@ class SlideViewer extends React.Component { } loadDerivedDataset = (derivedDataset: dmv.metadata.Dataset): void => { - logger.debug('Loading derived dataset') + logger.debug('Loading derived dataset:', derivedDataset) + const Comprehensive3DSR = StorageClasses.COMPREHENSIVE_3D_SR const ComprehensiveSR = StorageClasses.COMPREHENSIVE_SR const MicroscopyBulkSimpleAnnotation = StorageClasses.MICROSCOPY_BULK_SIMPLE_ANNOTATION @@ -645,6 +647,8 @@ class SlideViewer extends React.Component { const PseudocolorSoftcopyPresentationState = StorageClasses.PSEUDOCOLOR_SOFTCOPY_PRESENTATION_STATE if ((derivedDataset as { SOPClassUID: string }).SOPClassUID === Comprehensive3DSR) { + // ROIs don't have seriesInstanceUID property, so we show all ROIs + // that match the frame of reference (already filtered during addAnnotations) const allRois = this.volumeViewer.getAllROIs() allRois.forEach((roi) => { this.handleAnnotationVisibilityChange({ roiUID: roi.uid, isVisible: true }) @@ -652,25 +656,40 @@ class SlideViewer extends React.Component { logger.debug('Loading Comprehensive 3D SR') } else if ((derivedDataset as { SOPClassUID: string }).SOPClassUID === MicroscopyBulkSimpleAnnotation) { const allAnnotationGroups = this.volumeViewer.getAllAnnotationGroups() - allAnnotationGroups.forEach((annotationGroup) => { - this.handleAnnotationGroupVisibilityChange({ annotationGroupUID: annotationGroup.uid, isVisible: true }) + const annotationGroup = allAnnotationGroups.find((annotationGroup) => { + return annotationGroup.seriesInstanceUID === (derivedDataset as { SeriesInstanceUID: string }).SeriesInstanceUID }) + if (annotationGroup !== undefined) { + this.handleAnnotationGroupVisibilityChange({ annotationGroupUID: annotationGroup.uid, isVisible: true }) + } logger.debug('Loading Microscopy Bulk Simple Annotation') } else if ((derivedDataset as { SOPClassUID: string }).SOPClassUID === Segmentation) { const allSegments = this.volumeViewer.getAllSegments() - allSegments.forEach((segment) => { + const derivedSeriesInstanceUID = (derivedDataset as { SeriesInstanceUID: string }).SeriesInstanceUID + const matchingSegments = allSegments.filter((segment) => { + return segment.seriesInstanceUID === derivedSeriesInstanceUID + }) + matchingSegments.forEach((segment) => { this.handleSegmentVisibilityChange({ segmentUID: segment.uid, isVisible: true }) }) logger.debug('Loading Segmentation') } else if ((derivedDataset as { SOPClassUID: string }).SOPClassUID === ParametricMap) { const allParameterMappings = this.volumeViewer.getAllParameterMappings() - allParameterMappings.forEach((parameterMapping) => { + const derivedSeriesInstanceUID = (derivedDataset as { SeriesInstanceUID: string }).SeriesInstanceUID + const matchingMappings = allParameterMappings.filter((parameterMapping) => { + return parameterMapping.seriesInstanceUID === derivedSeriesInstanceUID + }) + matchingMappings.forEach((parameterMapping) => { this.handleMappingVisibilityChange({ mappingUID: parameterMapping.uid, isVisible: true }) }) logger.debug('Loading Parametric Map') } else if ((derivedDataset as { SOPClassUID: string }).SOPClassUID === OpticalPath) { const allOpticalPaths = this.volumeViewer.getAllOpticalPaths() - allOpticalPaths.forEach((opticalPath) => { + const derivedSeriesInstanceUID = (derivedDataset as { SeriesInstanceUID: string }).SeriesInstanceUID + const matchingOpticalPaths = allOpticalPaths.filter((opticalPath) => { + return opticalPath.seriesInstanceUID === derivedSeriesInstanceUID + }) + matchingOpticalPaths.forEach((opticalPath) => { this.handleOpticalPathVisibilityChange({ opticalPathIdentifier: opticalPath.identifier, isVisible: true }) }) logger.debug('Loading Optical Path') @@ -705,6 +724,10 @@ class SlideViewer extends React.Component { if (matchedInstances === null || matchedInstances === undefined) { matchedInstances = [] } + if (matchedInstances.length === 0) { + resolve() + return + } matchedInstances.forEach(i => { const { dataset } = dmv.metadata.formatMetadata(i) const instance = dataset as dmv.metadata.Instance @@ -785,7 +808,6 @@ class SlideViewer extends React.Component { ) } }) - resolve() }).catch((error) => { // eslint-disable-next-line @typescript-eslint/no-floating-promises @@ -847,6 +869,10 @@ class SlideViewer extends React.Component { if (matchedSeries === null || matchedSeries === undefined) { matchedSeries = [] } + if (matchedSeries.length === 0) { + resolve() + return + } matchedSeries.forEach(s => { const { dataset } = dmv.metadata.formatMetadata(s) const series = dataset as dmv.metadata.Series @@ -869,6 +895,7 @@ class SlideViewer extends React.Component { annotations.forEach(ann => { try { this.volumeViewer.addAnnotationGroups(ann) + resolve() } catch (error: unknown) { // eslint-disable-next-line @typescript-eslint/no-floating-promises NotificationMiddleware.onError( @@ -902,7 +929,6 @@ class SlideViewer extends React.Component { * interface unless an update is forced. */ this.forceUpdate() - resolve() }).catch((error) => { console.error(error) // eslint-disable-next-line @typescript-eslint/no-floating-promises @@ -949,6 +975,10 @@ class SlideViewer extends React.Component { if (matchedSeries === null || matchedSeries === undefined) { matchedSeries = [] } + if (matchedSeries.length === 0) { + resolve() + return + } matchedSeries.forEach((s, i) => { const { dataset } = dmv.metadata.formatMetadata(s) const series = dataset as dmv.metadata.Series @@ -970,6 +1000,7 @@ class SlideViewer extends React.Component { if (segmentations.length > 0) { try { this.volumeViewer.addSegments(segmentations) + resolve() } catch (error: unknown) { // eslint-disable-next-line @typescript-eslint/no-floating-promises NotificationMiddleware.onError( @@ -989,8 +1020,6 @@ class SlideViewer extends React.Component { */ this.forceUpdate() } - - resolve() }).catch((error) => { console.error(error) // eslint-disable-next-line @typescript-eslint/no-floating-promises @@ -1036,6 +1065,10 @@ class SlideViewer extends React.Component { if (matchedSeries === null || matchedSeries === undefined) { matchedSeries = [] } + if (matchedSeries.length === 0) { + resolve() + return + } matchedSeries.forEach(s => { const { dataset } = dmv.metadata.formatMetadata(s) const series = dataset as dmv.metadata.Series @@ -1061,6 +1094,7 @@ class SlideViewer extends React.Component { if (parametricMaps.length > 0) { try { this.volumeViewer.addParameterMappings(parametricMaps) + resolve() } catch (error: unknown) { // eslint-disable-next-line @typescript-eslint/no-floating-promises NotificationMiddleware.onError( @@ -1080,7 +1114,6 @@ class SlideViewer extends React.Component { */ this.forceUpdate() } - resolve() }).catch((error) => { console.error(error) // eslint-disable-next-line @typescript-eslint/no-floating-promises @@ -1128,51 +1161,25 @@ class SlideViewer extends React.Component { this.labelViewer.render({ container: this.labelViewportRef.current }) } - // State update will also ensure that the component is re-rendered. this.setState({ isLoading: false }) this.setDefaultPresentationState() this.loadPresentationStates() - // Handle promises properly with catch blocks - void this.addAnnotations() - .then(() => { - if (this.props.derivedDataset !== null && this.props.derivedDataset !== undefined) { - this.loadDerivedDataset(this.props.derivedDataset) - } - }) - .catch(error => { - console.error('Failed to add annotations:', error) - }) - - void this.addAnnotationGroups() - .then(() => { - if (this.props.derivedDataset !== null && this.props.derivedDataset !== undefined) { - this.loadDerivedDataset(this.props.derivedDataset) - } - }) - .catch(error => { - console.error('Failed to add annotation groups:', error) - }) - - void this.addSegmentations() - .then(() => { - if (this.props.derivedDataset !== null && this.props.derivedDataset !== undefined) { - this.loadDerivedDataset(this.props.derivedDataset) - } - }) - .catch(error => { - console.error('Failed to add segmentations:', error) - }) - - void this.addParametricMaps() + void Promise.allSettled([ + this.addAnnotations(), + this.addAnnotationGroups(), + this.addSegmentations(), + this.addParametricMaps() + ]) .then(() => { + console.debug('Loaded annotations, annotation groups, segmentations, and parametric maps!') if (this.props.derivedDataset !== null && this.props.derivedDataset !== undefined) { this.loadDerivedDataset(this.props.derivedDataset) } }) .catch(error => { - console.error('Failed to add parametric maps:', error) + console.error('Failed to add derived data:', error) }) } @@ -1231,11 +1238,25 @@ class SlideViewer extends React.Component { onRoiDoubleClicked = (event: CustomEventInit): void => { const selectedRoi = event.detail.payload as dmv.roi.ROI if (selectedRoi !== null) { + // Check if this is a bulk annotation by checking if the ROI UID starts with any annotation group UID + const roiUid = selectedRoi.uid + const allAnnotationGroups = this.volumeViewer.getAllAnnotationGroups() + const isBulkAnnotation = allAnnotationGroups.some( + annotationGroup => roiUid?.startsWith(`${String(annotationGroup.uid)}-`) + ) + + // Don't show modal for bulk annotations + if (isBulkAnnotation) { + return + } + this.setState({ + selectedRoi, isSelectedRoiModalVisible: true }) } else { this.setState({ + selectedRoi: undefined, isSelectedRoiModalVisible: false }) } @@ -1501,7 +1522,9 @@ class SlideViewer extends React.Component { } } - getUpdatedSelectedRois = (newSelectedRoiUid?: string): { selectedRoiUIDs: Set, selectedRoi?: dmv.roi.ROI} => { + getUpdatedSelectedRois = ( + newSelectedRoiUid?: string + ): { selectedRoiUIDs: Set, selectedRoi?: dmv.roi.ROI } => { const selectedRoiUid = newSelectedRoiUid const emptySelection = { selectedRoiUIDs: new Set(), @@ -1562,11 +1585,29 @@ class SlideViewer extends React.Component { } onRoiSelected = (event: CustomEventInit): void => { - const selectedRoiUid = event.detail?.payload?.uid as string - const updatedSelectedRois = this.getUpdatedSelectedRois(selectedRoiUid) - this.setState(updatedSelectedRois) - - this.resetUnselectedRoiStyles(updatedSelectedRois) + const payload = event.detail?.payload + const roiPayload = payload as dmv.roi.ROI | { uid?: string } | undefined + const isRoiObject = (roiPayload != null) && typeof roiPayload === 'object' && 'uid' in roiPayload && 'scoord3d' in roiPayload + + if (isRoiObject) { + const selectedRoi = roiPayload + const updatedSelectedRois = !this.keysDown.has('Shift') + ? { + selectedRoiUIDs: new Set([selectedRoi.uid]), + selectedRoi + } + : { + selectedRoiUIDs: new Set([...Array.from(this.state.selectedRoiUIDs), selectedRoi.uid]), + selectedRoi + } + this.setState(updatedSelectedRois) + this.resetUnselectedRoiStyles(updatedSelectedRois) + } else { + const selectedRoiUid = (payload as { uid?: string } | undefined)?.uid + const updatedSelectedRois = this.getUpdatedSelectedRois(selectedRoiUid) + this.setState(updatedSelectedRois) + this.resetUnselectedRoiStyles(updatedSelectedRois) + } } handleAnnotationSelection = (uid: string): void => { @@ -3068,7 +3109,7 @@ class SlideViewer extends React.Component { mappings.push(...this.volumeViewer.getAllParameterMappings()) const allAnnotationGroups = this.volumeViewer.getAllAnnotationGroups() const filteredAnnotationGroups = allAnnotationGroups?.filter((annotationGroup) => - annotationGroup.referencedSeriesInstanceUID === this.props.seriesInstanceUID + this.props.slide.seriesInstanceUIDs.includes(annotationGroup.referencedSeriesInstanceUID) ) annotationGroups.push(...filteredAnnotationGroups) @@ -3661,14 +3702,17 @@ class SlideViewer extends React.Component { private readonly getSelectedRoiInformation = (): React.ReactNode => { if (this.state.selectedRoi !== null && this.state.selectedRoi !== undefined) { + const allRois = this.volumeViewer.getAllROIs() + const roiIndex = allRois.findIndex(roi => roi.uid === this.state.selectedRoi?.uid) + const roiAttributes: Array<{ name: string value: string unit?: string }> = [ { - name: 'UID', - value: this.state.selectedRoi.uid + name: '', + value: `ROI ${roiIndex >= 0 ? roiIndex + 1 : 'N/A'}` } ] const roiScoordAttributes: Array<{ diff --git a/src/components/SpecimenItem.tsx b/src/components/SpecimenItem.tsx index ec88e89..7dc53be 100644 --- a/src/components/SpecimenItem.tsx +++ b/src/components/SpecimenItem.tsx @@ -48,14 +48,16 @@ class SpecimenItem extends React.Component { } const structures = specimenDescription.PrimaryAnatomicStructureSequence - const modifierSequence = structures.find(s => (s as any).PrimaryAnatomicStructureModifierSequence !== undefined) - if (modifierSequence != null) { - const modifiers: dcmjs.sr.coding.CodedConcept[] = (modifierSequence as any).PrimaryAnatomicStructureModifierSequence - if (modifiers.length > 0) { - attributes.push({ - name: 'Primary Anatomic Structure Modifier', - value: modifiers.map((item: dcmjs.sr.coding.CodedConcept) => item.CodeMeaning).join(', ') - }) + if (structures !== undefined && structures.length > 0) { + const modifierSequence = structures.find(s => (s as any).PrimaryAnatomicStructureModifierSequence !== undefined) + if (modifierSequence != null) { + const modifiers: dcmjs.sr.coding.CodedConcept[] = (modifierSequence as any).PrimaryAnatomicStructureModifierSequence + if (modifiers.length > 0) { + attributes.push({ + name: 'Primary Anatomic Structure Modifier', + value: modifiers.map((item: dcmjs.sr.coding.CodedConcept) => item.CodeMeaning).join(', ') + }) + } } } diff --git a/yarn.lock b/yarn.lock index 8805339..f4cb036 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5609,10 +5609,10 @@ detective@^5.2.1: defined "^1.0.0" minimist "^1.2.6" -dicom-microscopy-viewer@^0.48.15: - version "0.48.15" - resolved "https://registry.yarnpkg.com/dicom-microscopy-viewer/-/dicom-microscopy-viewer-0.48.15.tgz#229248892ee7a3fc53d99cd38ae300152368105e" - integrity sha512-oDJLII8Gokh/Iz/GMsn3uh4+CgJwhBFJLi6TX/akmCMeZ+CMvXGC3lA/rrSBxX5K91+aZJd90Lpd5XtmO5TVAQ== +dicom-microscopy-viewer@^0.48.16: + version "0.48.16" + resolved "https://registry.yarnpkg.com/dicom-microscopy-viewer/-/dicom-microscopy-viewer-0.48.16.tgz#ed497abb01776c1ec3a274f51b86e8964904e2e6" + integrity sha512-KjO/OJtB5th1BiCeJ448Je+dDXoW/jydGTXpP/I+4/xzmKso9Gu3aDEJLMJ//YOldMJsJzT1cNO8j430Eid2eA== dependencies: "@cornerstonejs/codec-charls" "^1.2.3" "@cornerstonejs/codec-libjpeg-turbo-8bit" "^1.2.2"