diff --git a/craco.config.js b/craco.config.js index 71623cf7..c2b93724 100644 --- a/craco.config.js +++ b/craco.config.js @@ -79,6 +79,8 @@ module.exports = { '@cornerstonejs/codec-openjpeg/decodewasmjs': '@cornerstonejs/codec-openjpeg/dist/openjpegwasm_decode.js', '@cornerstonejs/codec-openjpeg/decodewasm': '@cornerstonejs/codec-openjpeg/dist/openjpegwasm_decode.wasm' } + config.setupFilesAfterEnv = ['/src/setupTests.tsx'] + config.testEnvironment = 'jsdom' return config } } diff --git a/package.json b/package.json index c3bac8eb..d8b5750f 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "build:firebase": "REACT_APP_CONFIG=gcp PUBLIC_URL=/ 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 --setupFiles ./src/setupTests.tsx --watchAll=false", + "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", "deploy": "gh-pages -d build", "clean": "rm -rf ./build ./node_modules" diff --git a/src/App.tsx b/src/App.tsx index 579075a5..187fd421 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -8,6 +8,7 @@ import { } from 'react-router-dom' import { Layout, message } from 'antd' import { FaSpinner } from 'react-icons/fa' +// skipcq: JS-C1003 import * as dwc from 'dicomweb-client' import AppConfig, { ServerSettings, ErrorMessageSettings } from './AppConfig' @@ -396,14 +397,14 @@ class App extends React.Component { componentDidMount (): void { const path = window.localStorage.getItem('slim_path') - if (path === null || path === '') { + if (path === null || path === undefined || path === '') { window.localStorage.setItem('slim_path', window.location.pathname) window.localStorage.setItem('slim_search', window.location.search) } // Restore cached server selection if it exists const cachedServerUrl = window.localStorage.getItem('slim_selected_server') - if (cachedServerUrl !== null && cachedServerUrl !== '') { + if (cachedServerUrl !== null && cachedServerUrl !== undefined && cachedServerUrl !== '') { this.handleServerSelection({ url: cachedServerUrl }) } diff --git a/src/AppConfig.d.ts b/src/AppConfig.d.ts index 08c51e31..62376298 100644 --- a/src/AppConfig.d.ts +++ b/src/AppConfig.d.ts @@ -1,3 +1,4 @@ +// skipcq: JS-C1003 import * as dcmjs from 'dcmjs' export type DicomWebManagerErrorHandler = ( diff --git a/src/DicomWebManager.ts b/src/DicomWebManager.ts index 4c5838c8..d01b0edd 100644 --- a/src/DicomWebManager.ts +++ b/src/DicomWebManager.ts @@ -1,5 +1,8 @@ +// skipcq: JS-C1003 import * as dwc from 'dicomweb-client' +// skipcq: JS-C1003 import * as dcmjs from 'dcmjs' +// skipcq: JS-C1003 import * as dmv from 'dicom-microscopy-viewer' import { ServerSettings, DicomWebManagerErrorHandler } from './AppConfig' diff --git a/src/auth/OidcManager.tsx b/src/auth/OidcManager.tsx index fb511f08..1a05bf17 100644 --- a/src/auth/OidcManager.tsx +++ b/src/auth/OidcManager.tsx @@ -133,7 +133,7 @@ export default class OidcManager implements AuthManager { * the DICOMweb server. */ const userData = await this._oidc.getUser() - if (userData === null || userData.expired) { + if (userData === null || userData === undefined || userData.expired) { console.info('authenticating user') await this._oidc.signinRedirect() } else { @@ -156,7 +156,7 @@ export default class OidcManager implements AuthManager { */ getAuthorization = async (): Promise => { return await this._oidc.getUser().then((userData) => { - if (userData !== null) { + if (userData !== null && userData !== undefined) { return userData.access_token } else { NotificationMiddleware.onError( @@ -175,7 +175,7 @@ export default class OidcManager implements AuthManager { */ getUser = async (): Promise => { return await this._oidc.getUser().then((userData) => { - if (userData === null) { + if (userData === null || userData === undefined) { NotificationMiddleware.onError( NotificationMiddlewareContext.AUTH, new CustomError( diff --git a/src/components/AnnotationGroupItem.tsx b/src/components/AnnotationGroupItem.tsx index 6f07a554..acb3d790 100644 --- a/src/components/AnnotationGroupItem.tsx +++ b/src/components/AnnotationGroupItem.tsx @@ -15,7 +15,9 @@ import { } from 'antd' import { SettingOutlined } from '@ant-design/icons' import { FaEye, FaEyeSlash } from 'react-icons/fa' +// skipcq: JS-C1003 import * as dmv from 'dicom-microscopy-viewer' +// skipcq: JS-C1003 import * as dcmjs from 'dcmjs' import Description from './Description' @@ -451,7 +453,7 @@ AnnotationGroupItemState size='small' disabled={!this.props.isVisible} > - <> + {null} ) diff --git a/src/components/AnnotationGroupList.tsx b/src/components/AnnotationGroupList.tsx index 80256544..d43b75c2 100644 --- a/src/components/AnnotationGroupList.tsx +++ b/src/components/AnnotationGroupList.tsx @@ -1,6 +1,8 @@ import React from 'react' import { Menu, Switch } from 'antd' +// skipcq: JS-C1003 import * as dmv from 'dicom-microscopy-viewer' +// skipcq: JS-C1003 import * as dcmjs from 'dcmjs' import AnnotationGroupItem from './AnnotationGroupItem' diff --git a/src/components/AnnotationItem.tsx b/src/components/AnnotationItem.tsx index 4f0081c1..fb43f33e 100644 --- a/src/components/AnnotationItem.tsx +++ b/src/components/AnnotationItem.tsx @@ -1,5 +1,7 @@ import React from 'react' +// skipcq: JS-C1003 import * as dmv from 'dicom-microscopy-viewer' +// skipcq: JS-C1003 import * as dcmjs from 'dcmjs' import { Menu, Space, Switch } from 'antd' import { FaEye, FaEyeSlash } from 'react-icons/fa' diff --git a/src/components/AnnotationList.tsx b/src/components/AnnotationList.tsx index 8e56b945..ccad0fdc 100644 --- a/src/components/AnnotationList.tsx +++ b/src/components/AnnotationList.tsx @@ -1,4 +1,5 @@ import React from 'react' +// skipcq: JS-C1003 import * as dmv from 'dicom-microscopy-viewer' import { Menu, Switch } from 'antd' import { FaEye, FaEyeSlash } from 'react-icons/fa' diff --git a/src/components/Button.tsx b/src/components/Button.tsx index 08ab50d8..3de8170b 100644 --- a/src/components/Button.tsx +++ b/src/components/Button.tsx @@ -1,11 +1,12 @@ import React from 'react' import { Button as Btn, Divider, Tooltip } from 'antd' +import { IconType } from 'react-icons' interface ButtonProps { - icon: any + icon: IconType | React.ComponentType> | React.ForwardRefExoticComponent tooltip?: string label?: string - onClick?: (options: any) => void + onClick?: (options: React.SyntheticEvent) => void isSelected?: boolean } diff --git a/src/components/CaseViewer.tsx b/src/components/CaseViewer.tsx index f4a18d87..5dc79baf 100644 --- a/src/components/CaseViewer.tsx +++ b/src/components/CaseViewer.tsx @@ -1,5 +1,6 @@ import { Routes, Route, useLocation, useParams } from 'react-router-dom' import { Layout, Menu } from 'antd' +// skipcq: JS-C1003 import * as dcmjs from 'dcmjs' import { useEffect, useState } from 'react' @@ -133,7 +134,7 @@ function ParametrizedSlideViewer ({ if (selectedSlide === null || selectedSlide === undefined) { void findReferencedSlide({ clients, studyInstanceUID, seriesInstanceUID }).then((result: ReferencedSlideResult | null) => { - if (result !== null) { + if (result !== null && result !== undefined) { setSelectedSlide(result.slide) setDerivedDataset(result.metadata) } @@ -144,16 +145,14 @@ function ParametrizedSlideViewer ({ }, [slides, clients, studyInstanceUID, seriesInstanceUID, selectedSlide]) const searchParams = new URLSearchParams(location.search) - let presentationStateUID: string | null | undefined + let presentationStateUID: string | undefined if (!searchParams.has('access_token')) { - presentationStateUID = searchParams.get('state') - if (presentationStateUID === null) { - presentationStateUID = undefined - } + const stateParam = searchParams.get('state') + presentationStateUID = stateParam !== null ? stateParam : undefined } let viewer = null - if (selectedSlide != null) { + if (selectedSlide != null && selectedSlide !== undefined) { viewer = ( ) } diff --git a/src/components/ClinicalTrial.tsx b/src/components/ClinicalTrial.tsx index 102262a9..e32607a5 100644 --- a/src/components/ClinicalTrial.tsx +++ b/src/components/ClinicalTrial.tsx @@ -1,4 +1,5 @@ import React from 'react' +// skipcq: JS-C1003 import * as dmv from 'dicom-microscopy-viewer' import Description from './Description' diff --git a/src/components/DicomTagBrowser/DicomTagBrowser.tsx b/src/components/DicomTagBrowser/DicomTagBrowser.tsx index b06ec444..2f3c1d72 100644 --- a/src/components/DicomTagBrowser/DicomTagBrowser.tsx +++ b/src/components/DicomTagBrowser/DicomTagBrowser.tsx @@ -112,7 +112,7 @@ const DicomTagBrowser = ({ clients, studyInstanceUID }: DicomTagBrowserProps): J index++ return ds }) - .filter((set): set is DisplaySet => set !== null) + .filter((set): set is DisplaySet => set !== null && set !== undefined) } if (study !== undefined && study.series?.length > 0) { diff --git a/src/components/Equipment.tsx b/src/components/Equipment.tsx index 2223825c..c6d81473 100644 --- a/src/components/Equipment.tsx +++ b/src/components/Equipment.tsx @@ -1,4 +1,5 @@ import React from 'react' +// skipcq: JS-C1003 import * as dmv from 'dicom-microscopy-viewer' import Description from './Description' diff --git a/src/components/Header.tsx b/src/components/Header.tsx index 083b0bb8..3c2cf56e 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -84,7 +84,7 @@ class Header extends React.Component { selectedServerUrl: cachedServerUrl ?? '', isServerSelectionModalVisible: false, isServerSelectionDisabled: !this.isValidServerUrl(cachedServerUrl), - serverSelectionMode: cachedMode === 'custom' && cachedServerUrl !== null && cachedServerUrl !== '' ? 'custom' : 'default' + serverSelectionMode: cachedMode === 'custom' && cachedServerUrl !== null && cachedServerUrl !== undefined && cachedServerUrl !== '' ? 'custom' : 'default' } const onErrorHandler = ({ source, error }: { @@ -328,7 +328,7 @@ class Header extends React.Component { handleServerSelectionCancellation = (): void => { const cachedServerUrl = window.localStorage.getItem('slim_selected_server') this.setState({ - serverSelectionMode: cachedServerUrl !== null && cachedServerUrl !== '' ? 'custom' : 'default', + serverSelectionMode: cachedServerUrl !== null && cachedServerUrl !== undefined && cachedServerUrl !== '' ? 'custom' : 'default', selectedServerUrl: cachedServerUrl ?? undefined, isServerSelectionModalVisible: false, isServerSelectionDisabled: !this.isValidServerUrl(cachedServerUrl) diff --git a/src/components/MappingItem.tsx b/src/components/MappingItem.tsx index 936ffb77..23923005 100644 --- a/src/components/MappingItem.tsx +++ b/src/components/MappingItem.tsx @@ -1,4 +1,5 @@ import React from 'react' +// skipcq: JS-C1003 import * as dmv from 'dicom-microscopy-viewer' import { Button, diff --git a/src/components/MappingList.tsx b/src/components/MappingList.tsx index c4a438b9..4e570992 100644 --- a/src/components/MappingList.tsx +++ b/src/components/MappingList.tsx @@ -1,4 +1,5 @@ import React from 'react' +// skipcq: JS-C1003 import * as dmv from 'dicom-microscopy-viewer' import { Menu } from 'antd' diff --git a/src/components/OpticalPathItem.tsx b/src/components/OpticalPathItem.tsx index d664a4eb..3d11de38 100644 --- a/src/components/OpticalPathItem.tsx +++ b/src/components/OpticalPathItem.tsx @@ -20,7 +20,9 @@ import { SettingOutlined } from '@ant-design/icons' import Description from './Description' +// skipcq: JS-C1003 import * as dmv from 'dicom-microscopy-viewer' +// skipcq: JS-C1003 import * as dcmjs from 'dcmjs' import { SpecimenPreparationStepItems } from '../data/specimens' diff --git a/src/components/OpticalPathList.tsx b/src/components/OpticalPathList.tsx index 40b47a18..554a1e71 100644 --- a/src/components/OpticalPathList.tsx +++ b/src/components/OpticalPathList.tsx @@ -1,4 +1,5 @@ import React from 'react' +// skipcq: JS-C1003 import * as dmv from 'dicom-microscopy-viewer' import { Button as Btn, Menu, Select, Space, Tooltip } from 'antd' import { AppstoreAddOutlined } from '@ant-design/icons' diff --git a/src/components/Patient.tsx b/src/components/Patient.tsx index cf5b7743..3d1a1a82 100644 --- a/src/components/Patient.tsx +++ b/src/components/Patient.tsx @@ -1,4 +1,5 @@ import React from 'react' +// skipcq: JS-C1003 import * as dmv from 'dicom-microscopy-viewer' import Description from './Description' diff --git a/src/components/Report.tsx b/src/components/Report.tsx index f7fd9170..b7028c01 100644 --- a/src/components/Report.tsx +++ b/src/components/Report.tsx @@ -1,5 +1,7 @@ import React from 'react' +// skipcq: JS-C1003 import * as dmv from 'dicom-microscopy-viewer' +// skipcq: JS-C1003 import * as dcmjs from 'dcmjs' import { Divider } from 'antd' import { v4 as generateUUID } from 'uuid' diff --git a/src/components/SegmentItem.tsx b/src/components/SegmentItem.tsx index 5f942603..d0d3cbda 100644 --- a/src/components/SegmentItem.tsx +++ b/src/components/SegmentItem.tsx @@ -1,4 +1,5 @@ import React from 'react' +// skipcq: JS-C1003 import * as dmv from 'dicom-microscopy-viewer' import { Button, diff --git a/src/components/SegmentList.tsx b/src/components/SegmentList.tsx index e987266b..4d322929 100644 --- a/src/components/SegmentList.tsx +++ b/src/components/SegmentList.tsx @@ -1,4 +1,5 @@ import React from 'react' +// skipcq: JS-C1003 import * as dmv from 'dicom-microscopy-viewer' import { Menu } from 'antd' diff --git a/src/components/SlideItem.tsx b/src/components/SlideItem.tsx index f36fd1c3..38d977cf 100644 --- a/src/components/SlideItem.tsx +++ b/src/components/SlideItem.tsx @@ -1,5 +1,6 @@ import React from 'react' import { FaSpinner } from 'react-icons/fa' +// skipcq: JS-C1003 import * as dmv from 'dicom-microscopy-viewer' import { Menu } from 'antd' @@ -45,7 +46,7 @@ class SlideItem extends React.Component { this.setState({ isLoading: true }) if (this.props.slide.overviewImages.length > 0) { const metadata = this.props.slide.overviewImages[0] - if (this.overviewViewportRef.current !== null) { + if (this.overviewViewportRef.current !== null && this.overviewViewportRef.current !== undefined) { this.overviewViewportRef.current.innerHTML = '' console.info( 'instantiate viewer for OVERVIEW image of slide ' + @@ -81,7 +82,7 @@ class SlideItem extends React.Component { const attributes = [] const description = this.props.slide.description - if (description !== null && description !== '') { + if (description !== null && description !== undefined && description !== '') { attributes.push({ name: 'Description', value: description diff --git a/src/components/SlideList.tsx b/src/components/SlideList.tsx index 06a3c3d6..8c8f3910 100644 --- a/src/components/SlideList.tsx +++ b/src/components/SlideList.tsx @@ -60,8 +60,7 @@ class SlideList extends React.Component { } let selectedKeys - if (this.state.selectedSeriesInstanceUID !== undefined && - this.state.selectedSeriesInstanceUID !== null) { + if (this.state.selectedSeriesInstanceUID !== null && this.state.selectedSeriesInstanceUID !== undefined) { selectedKeys = [this.state.selectedSeriesInstanceUID] } diff --git a/src/components/SlideViewer.tsx b/src/components/SlideViewer.tsx index 958568d5..a27063bf 100644 --- a/src/components/SlideViewer.tsx +++ b/src/components/SlideViewer.tsx @@ -1,4 +1,7 @@ import React from 'react' +import { Layout, Space, Checkbox, Descriptions, Divider, Select, Tooltip, message, Menu, Row } from 'antd' +import { CheckboxChangeEvent } from 'antd/es/checkbox' +import { UndoOutlined } from '@ant-design/icons' import { FaCrosshairs, FaDrawPolygon, @@ -9,430 +12,66 @@ import { FaTrash, FaSave } from 'react-icons/fa' -import { - Button as Btn, - Checkbox, - Descriptions, - Divider, - InputNumber, - message, - Menu, - Modal, - Layout, - Row, - Select, - Space, - Tooltip -} from 'antd' -import { UndoOutlined, CheckOutlined, StopOutlined } from '@ant-design/icons' +// skipcq: JS-C1003 import * as dmv from 'dicom-microscopy-viewer' -import * as dcmjs from 'dcmjs' +// skipcq: JS-C1003 import * as dwc from 'dicomweb-client' -import type { CheckboxChangeEvent } from 'antd/es/checkbox' - -import DicomWebManager from '../DicomWebManager' -import AnnotationList from './AnnotationList' -import AnnotationGroupList from './AnnotationGroupList' -import Button from './Button' -import Equipment from './Equipment' -import Report, { MeasurementReport } from './Report' -import SpecimenList from './SpecimenList' -import OpticalPathList from './OpticalPathList' -import MappingList from './MappingList' -import SegmentList from './SegmentList' -import { AnnotationSettings } from '../AppConfig' -import { Slide } from '../data/slides' +// skipcq: JS-C1003 +import * as dcmjs from 'dcmjs' +import { withRouter } from '../utils/router' import { StorageClasses } from '../data/uids' -import { findContentItemsByName } from '../utils/sr' -import { RouteComponentProps, withRouter } from '../utils/router' import { CustomError, errorTypes } from '../utils/CustomError' +import { findContentItemsByName } from '../utils/sr' import NotificationMiddleware, { NotificationMiddlewareContext } from '../services/NotificationMiddleware' -import AnnotationCategoryList, { AnnotationCategoryAndType } from './AnnotationCategoryList' -import HoveredRoiTooltip from './HoveredRoiTooltip' import { adaptRoiToAnnotation } from '../services/RoiToAnnotationAdapter' +import { AnnotationSettings, AnnotationCategoryAndType } from '../types/annotations' +import { + SlideViewerProps, + SlideViewerState, + StyleOptions, + EvaluationOptions, + Evaluation, + Measurement +} from './SlideViewer/types' +import SlideViewerModals from './SlideViewer/SlideViewerModals' +import SlideViewerSidebar from './SlideViewer/SlideViewerSidebar' +import SlideViewerContent from './SlideViewer/SlideViewerContent' +import { + buildKey, + getRoiKey, + areROIsEqual, + formatRoiStyle +} from './SlideViewer/utils/roiUtils' +import { + constructViewers, + implementsTID1500, + describesSpecimenSubject, + containsROIAnnotations +} from './SlideViewer/utils/viewerUtils' +import { + DEFAULT_ROI_STROKE_COLOR, + DEFAULT_ROI_FILL_COLOR, + DEFAULT_ROI_STROKE_WIDTH, + DEFAULT_ROI_RADIUS, + DEFAULT_ANNOTATION_OPACITY, + DEFAULT_ANNOTATION_STROKE_COLOR, + DEFAULT_ANNOTATION_COLOR_PALETTE +} from './SlideViewer/constants' +import AnnotationList from './AnnotationList' +import AnnotationGroupList from './AnnotationGroupList' +import Report, { MeasurementReport } from './Report' +import HoveredRoiTooltip from './HoveredRoiTooltip' import generateReport from '../utils/generateReport' import { runValidations } from '../contexts/ValidationContext' - -const DEFAULT_ROI_STROKE_COLOR: number[] = [255, 234, 0] // [0, 126, 163] -const DEFAULT_ROI_FILL_COLOR: number[] = [255, 234, 0, 0.2] // [0, 126, 163, 0.2] -const DEFAULT_ROI_STROKE_WIDTH: number = 2 -const DEFAULT_ROI_RADIUS: number = 5 - -const DEFAULT_ANNOTATION_OPACITY = 0.4 -const DEFAULT_ANNOTATION_STROKE_COLOR = [0, 0, 0] -const DEFAULT_ANNOTATION_COLOR_PALETTE = [ - [255, 0, 0], - [0, 255, 0], - [0, 0, 255], - [255, 255, 0], - [0, 255, 255], - [0, 0, 0] -] - -interface StyleOptions { - opacity: number - color: number[] - contourOnly: boolean -} - -const _buildKey = (concept: { - CodeValue: string - CodeMeaning: string - CodingSchemeDesignator: string - CodingSchemeVersion?: string -}): string => { - const codingScheme = concept.CodingSchemeDesignator - const codeValue = concept.CodeValue - return `${codingScheme}-${codeValue}` -} - -const _getRoiKey = (roi: dmv.roi.ROI): string | undefined => { - const matches = findContentItemsByName({ - content: roi.evaluations, - name: new dcmjs.sr.coding.CodedConcept({ - value: '121071', - meaning: 'Finding', - schemeDesignator: 'DCM' - }) - }) - if (matches.length === 0) { - console.warn(`no finding found for ROI ${roi.uid}`) - return - } - const finding = matches[0] as dcmjs.sr.valueTypes.CodeContentItem - const findingName = finding.ConceptCodeSequence[0] - return _buildKey(findingName) -} - -const _areROIsEqual = (a: dmv.roi.ROI, b: dmv.roi.ROI): boolean => { - if (a.scoord3d.graphicType !== b.scoord3d.graphicType) { - return false - } - if (a.scoord3d.frameOfReferenceUID !== b.scoord3d.frameOfReferenceUID) { - return false - } - if (a.scoord3d.graphicData.length !== b.scoord3d.graphicData.length) { - return false - } - - const decimals = 6 - for (let i = 0; i < a.scoord3d.graphicData.length; ++i) { - if (a.scoord3d.graphicType === 'POINT') { - const s1 = a.scoord3d as dmv.scoord3d.Point - const s2 = b.scoord3d as dmv.scoord3d.Point - const c1 = s1.graphicData[i].toPrecision(decimals) - const c2 = s2.graphicData[i].toPrecision(decimals) - if (c1 !== c2) { - return false - } - } else { - const s1 = a.scoord3d as dmv.scoord3d.Polygon - const s2 = b.scoord3d as dmv.scoord3d.Polygon - for (let j = 0; j < s1.graphicData[i].length; ++j) { - const c1 = s1.graphicData[i][j].toPrecision(decimals) - const c2 = s2.graphicData[i][j].toPrecision(decimals) - if (c1 !== c2) { - return false - } - } - } - } - return true -} - -const _formatRoiStyle = (style: { - stroke?: { - color?: number[] - width?: number - } - fill?: { - color?: number[] - } - radius?: number -}): dmv.viewer.ROIStyleOptions => { - const stroke = { - color: DEFAULT_ROI_STROKE_COLOR, - width: DEFAULT_ROI_STROKE_WIDTH - } - if (style.stroke !== null && style.stroke !== undefined) { - if (style.stroke.color !== null && style.stroke.color !== undefined) { - stroke.color = style.stroke.color - } - if (style.stroke.width !== null && style.stroke.width !== undefined) { - stroke.width = style.stroke.width - } - } - const fill = { - color: DEFAULT_ROI_FILL_COLOR - } - if (style.fill !== null && style.fill !== undefined) { - if (style.fill.color !== null && style.fill.color !== undefined) { - fill.color = style.fill.color - } - } - return { - stroke, - fill, - image: { - circle: { - radius: style.radius !== null && style.radius !== undefined - ? style.radius - : Math.max(5 - stroke.width, 1), - stroke, - fill - } - } - } -} - -const _constructViewers = ({ clients, slide, preload }: { - clients: { [key: string]: dwc.api.DICOMwebClient } - slide: Slide - preload?: boolean -}): { - volumeViewer: dmv.viewer.VolumeImageViewer - labelViewer?: dmv.viewer.LabelImageViewer -} => { - console.info( - 'instantiate viewer for VOLUME images of slide ' + - `"${slide.volumeImages[0].ContainerIdentifier}"` - ) - try { - const volumeViewer = new dmv.viewer.VolumeImageViewer({ - clientMapping: clients, - metadata: slide.volumeImages, - controls: ['overview', 'position'], - preload: preload, - errorInterceptor: (error: CustomError) => { - NotificationMiddleware.onError( - NotificationMiddlewareContext.DMV, error - ) - } - }) - volumeViewer.activateSelectInteraction({}) - - let labelViewer - if (slide.labelImages.length > 0) { - console.info( - 'instantiate viewer for LABEL image of slide ' + - `"${slide.labelImages[0].ContainerIdentifier}"` - ) - labelViewer = new dmv.viewer.LabelImageViewer({ - client: clients[StorageClasses.VL_WHOLE_SLIDE_MICROSCOPY_IMAGE], - metadata: slide.labelImages[0], - resizeFactor: 1, - orientation: 'vertical', - errorInterceptor: (error: CustomError) => { - NotificationMiddleware.onError( - NotificationMiddlewareContext.DMV, - error - ) - } - }) - } - - return { volumeViewer, labelViewer } - } catch (error) { - // eslint-disable-next-line @typescript-eslint/no-floating-promises - NotificationMiddleware.onError( - NotificationMiddlewareContext.SLIM, - new CustomError( - errorTypes.VISUALIZATION, - 'Failed to instantiate viewer' - ) - ) - throw error - } -} - -/* - * Check whether the report is structured according to template - * TID 1500 "MeasurementReport". - */ -const _implementsTID1500 = ( - report: dmv.metadata.Comprehensive3DSR -): boolean => { - const templateSeq = report.ContentTemplateSequence - if (templateSeq.length > 0) { - const tid = templateSeq[0].TemplateIdentifier - if (tid === '1500') { - return true - } - } - return false -} - -/* - * Check whether the subject described in the report is a specimen as compared - * to a patient, fetus, or device. - */ -const _describesSpecimenSubject = ( - report: dmv.metadata.Comprehensive3DSR -): boolean => { - const items = findContentItemsByName({ - content: report.ContentSequence, - name: new dcmjs.sr.coding.CodedConcept({ - value: '121024', - schemeDesignator: 'DCM', - meaning: 'Subject Class' - }) - }) - if (items.length === 0) { - return false - } - const subjectClassItem = items[0] as dcmjs.sr.valueTypes.CodeContentItem - const subjectClassValue = subjectClassItem.ConceptCodeSequence[0] - const retrievedConcept = new dcmjs.sr.coding.CodedConcept({ - value: subjectClassValue.CodeValue, - meaning: subjectClassValue.CodeMeaning, - schemeDesignator: subjectClassValue.CodingSchemeDesignator - }) - const expectedConcept = new dcmjs.sr.coding.CodedConcept({ - value: '121027', - meaning: 'Specimen', - schemeDesignator: 'DCM' - }) - if (retrievedConcept.equals(expectedConcept)) { - return true - } - return false -} - -/* - * Check whether the report contains appropriate graphic ROI annotations. - */ -const _containsROIAnnotations = ( - report: dmv.metadata.Comprehensive3DSR -): boolean => { - const measurements = findContentItemsByName({ - content: report.ContentSequence, - name: new dcmjs.sr.coding.CodedConcept({ - value: '126010', - schemeDesignator: 'DCM', - meaning: 'Imaging Measurements' - }) - }) - if (measurements.length === 0) { - return false - } - const container = measurements[0] as dcmjs.sr.valueTypes.ContainerContentItem - const measurementGroups = findContentItemsByName({ - content: container.ContentSequence, - name: new dcmjs.sr.coding.CodedConcept({ - value: '125007', - schemeDesignator: 'DCM', - meaning: 'Measurement Group' - }) - }) - - let foundRegion = false - measurementGroups.forEach((group) => { - const container = group as dcmjs.sr.valueTypes.ContainerContentItem - const regions = findContentItemsByName({ - content: container.ContentSequence, - name: new dcmjs.sr.coding.CodedConcept({ - value: '111030', - schemeDesignator: 'DCM', - meaning: 'Image Region' - }) - }) - if (regions.length > 0) { - if (regions[0].ValueType === dcmjs.sr.valueTypes.ValueTypes.SCOORD3D) { - foundRegion = true - } - } - }) - - return foundRegion -} - -interface EvaluationOptions { - name: dcmjs.sr.coding.CodedConcept - values: dcmjs.sr.coding.CodedConcept[] -} - -interface Evaluation { - name: dcmjs.sr.coding.CodedConcept - value: dcmjs.sr.coding.CodedConcept -} - -interface Measurement { - name: dcmjs.sr.coding.CodedConcept - value?: number - unit: dcmjs.sr.coding.CodedConcept -} - -interface SlideViewerProps extends RouteComponentProps { - slide: Slide - clients: { [key: string]: DicomWebManager } - studyInstanceUID: string - seriesInstanceUID: string - app: { - name: string - version: string - uid: string - organization?: string - } - annotations: AnnotationSettings[] - enableAnnotationTools: boolean - preload: boolean - user?: { - name: string - email: string - } - selectedPresentationStateUID?: string - derivedDataset?: dmv.metadata.Dataset // Add this line -} - -interface SlideViewerState { - visibleRoiUIDs: Set - visibleSegmentUIDs: Set - visibleMappingUIDs: Set - visibleAnnotationGroupUIDs: Set - visibleOpticalPathIdentifiers: Set - activeOpticalPathIdentifiers: Set - presentationStates: dmv.metadata.AdvancedBlendingPresentationState[] - selectedPresentationStateUID?: string - selectedFinding?: dcmjs.sr.coding.CodedConcept - selectedEvaluations: Evaluation[] - selectedGeometryType?: string - selectedMarkup?: string - selectedRoi?: dmv.roi.ROI - selectedRoiUIDs: Set - generatedReport?: dmv.metadata.Comprehensive3DSR - isLoading: boolean - isAnnotationModalVisible: boolean - isSelectedRoiModalVisible: boolean - isHoveredRoiTooltipVisible: boolean - hoveredRoiAttributes: Array<{index: number, roiUid: string, attributes: Array<{ name: string, value: string }>}> - hoveredRoiTooltipX: number - hoveredRoiTooltipY: number - isReportModalVisible: boolean - isRoiDrawingActive: boolean - isRoiModificationActive: boolean - isRoiTranslationActive: boolean - isGoToModalVisible: boolean - isSelectedMagnificationValid: boolean - isSelectedXCoordinateValid: boolean - isSelectedYCoordinateValid: boolean - selectedXCoordinate?: number - validXCoordinateRange: number[] - selectedYCoordinate?: number - validYCoordinateRange: number[] - selectedMagnification?: number - areRoisHidden: boolean - pixelDataStatistics: { - [opticalPathIdentifier: string]: { - min: number - max: number - numFramesSampled: number - } - } - loadingFrames: Set - isICCProfilesEnabled: boolean -} +import DicomMetadataStore from '../services/DICOMMetadataStore' +import SpecimenList from './SpecimenList' +import Equipment from './Equipment' +import OpticalPathList from './OpticalPathList' +import SegmentList from './SegmentList' +import MappingList from './MappingList' +import Btn from './Button' /** * React component for interactive viewing of an individual digital slide, @@ -518,7 +157,7 @@ class SlideViewer extends React.Component { props.annotations.forEach((annotation: AnnotationSettings) => { const finding = new dcmjs.sr.coding.CodedConcept(annotation.finding) this.findingOptions.push(finding) - const key = _buildKey(finding) + const key = buildKey(finding) if (annotation.geometryTypes !== undefined) { this.geometryTypeOptions[key] = annotation.geometryTypes } else { @@ -545,54 +184,13 @@ class SlideViewer extends React.Component { }) } if (annotation.style !== null && annotation.style !== undefined) { - this.roiStyles[key] = _formatRoiStyle(annotation.style) + this.roiStyles[key] = formatRoiStyle(annotation.style) } else { this.roiStyles[key] = this.defaultRoiStyle } }) - this.componentSetup = this.componentSetup.bind(this) - this.componentCleanup = this.componentCleanup.bind(this) - this.onWindowResize = this.onWindowResize.bind(this) - this.handleRoiDrawing = this.handleRoiDrawing.bind(this) - this.handleRoiTranslation = this.handleRoiTranslation.bind(this) - this.handleRoiModification = this.handleRoiModification.bind(this) - this.handleRoiVisibilityChange = this.handleRoiVisibilityChange.bind(this) - this.handleRoiRemoval = this.handleRoiRemoval.bind(this) - this.handleRoiSelectionCancellation = this.handleRoiSelectionCancellation.bind(this) - this.handleAnnotationConfigurationCancellation = this.handleAnnotationConfigurationCancellation.bind(this) - this.handleAnnotationGeometryTypeSelection = this.handleAnnotationGeometryTypeSelection.bind(this) - this.handleAnnotationMeasurementActivation = this.handleAnnotationMeasurementActivation.bind(this) - this.handleAnnotationFindingSelection = this.handleAnnotationFindingSelection.bind(this) - this.handleAnnotationEvaluationSelection = this.handleAnnotationEvaluationSelection.bind(this) - this.handleAnnotationEvaluationClearance = this.handleAnnotationEvaluationClearance.bind(this) - this.handleAnnotationConfigurationCompletion = this.handleAnnotationConfigurationCompletion.bind(this) - this.handleAnnotationVisibilityChange = this.handleAnnotationVisibilityChange.bind(this) - this.handleAnnotationGroupVisibilityChange = this.handleAnnotationGroupVisibilityChange.bind(this) - this.handleAnnotationGroupStyleChange = this.handleAnnotationGroupStyleChange.bind(this) - this.handleRoiStyleChange = this.handleRoiStyleChange.bind(this) - this.handleGoTo = this.handleGoTo.bind(this) - this.handleXCoordinateSelection = this.handleXCoordinateSelection.bind(this) - this.handleYCoordinateSelection = this.handleYCoordinateSelection.bind(this) - this.handleMagnificationSelection = this.handleMagnificationSelection.bind(this) - this.handleSlidePositionSelection = this.handleSlidePositionSelection.bind(this) - this.handleSlidePositionSelectionCancellation = this.handleSlidePositionSelectionCancellation.bind(this) - this.handleReportGeneration = this.handleReportGeneration.bind(this) - this.handleReportVerification = this.handleReportVerification.bind(this) - this.handleReportCancellation = this.handleReportCancellation.bind(this) - this.handleSegmentVisibilityChange = this.handleSegmentVisibilityChange.bind(this) - this.handleSegmentStyleChange = this.handleSegmentStyleChange.bind(this) - this.handleMappingVisibilityChange = this.handleMappingVisibilityChange.bind(this) - this.handleMappingStyleChange = this.handleMappingStyleChange.bind(this) - this.handleOpticalPathVisibilityChange = this.handleOpticalPathVisibilityChange.bind(this) - this.handleOpticalPathStyleChange = this.handleOpticalPathStyleChange.bind(this) - this.handleOpticalPathActivityChange = this.handleOpticalPathActivityChange.bind(this) - this.handlePresentationStateSelection = this.handlePresentationStateSelection.bind(this) - this.handlePresentationStateReset = this.handlePresentationStateReset.bind(this) - this.handleICCProfilesToggle = this.handleICCProfilesToggle.bind(this) - this.handleAnnotationSelection = this.handleAnnotationSelection.bind(this) - - const { volumeViewer, labelViewer } = _constructViewers({ + const { volumeViewer, labelViewer } = constructViewers({ clients: this.props.clients, slide: this.props.slide, preload: this.props.preload @@ -645,6 +243,7 @@ class SlideViewer extends React.Component { validYCoordinateRange: [offset[1], offset[1] + size[1]], selectedMagnification: undefined, areRoisHidden: false, + selectedSeriesInstanceUID: undefined, pixelDataStatistics: {}, selectedPresentationStateUID: this.props.selectedPresentationStateUID, loadingFrames: new Set(), @@ -676,7 +275,7 @@ class SlideViewer extends React.Component { } this.labelViewer.cleanup() } - const { volumeViewer, labelViewer } = _constructViewers({ + const { volumeViewer, labelViewer } = constructViewers({ clients: this.props.clients, slide: this.props.slide, preload: this.props.preload @@ -728,11 +327,11 @@ class SlideViewer extends React.Component { queryParams: { Modality: 'PR' } - }).then((matchedInstances): void => { - if (matchedInstances === null) { + }).then((matchedInstances: dwc.api.Instance[] | null): void => { + if (matchedInstances === null || matchedInstances === undefined) { matchedInstances = [] } - matchedInstances.forEach((rawInstance, index) => { + matchedInstances.forEach((rawInstance: dwc.api.Instance, index: number) => { const { dataset } = dmv.metadata.formatMetadata(rawInstance) const instance = dataset as dmv.metadata.Instance console.info(`retrieve PR instance "${instance.SOPInstanceUID}"`) @@ -740,7 +339,7 @@ class SlideViewer extends React.Component { studyInstanceUID: this.props.studyInstanceUID, seriesInstanceUID: instance.SeriesInstanceUID, sopInstanceUID: instance.SOPInstanceUID - }).then((retrievedInstance): void => { + }).then((retrievedInstance: dwc.api.Dataset): void => { const data = dcmjs.data.DicomMessage.readFile(retrievedInstance) const { dataset } = dmv.metadata.formatMetadata(data.dict) if (this.props.slide.areVolumeImagesMonochrome) { @@ -763,7 +362,8 @@ class SlideViewer extends React.Component { ) if ( index === 0 && - this.props.selectedPresentationStateUID === null + (this.props.selectedPresentationStateUID === null || + this.props.selectedPresentationStateUID === undefined) ) { this.setPresentationState(presentationState) } else { @@ -868,11 +468,11 @@ class SlideViewer extends React.Component { ) as boolean if (isReferenced) { let paletteColorLUT - if (blendingItem.PaletteColorLookupTableSequence != null) { + if (blendingItem.PaletteColorLookupTableSequence !== null && blendingItem.PaletteColorLookupTableSequence !== undefined) { const cpLUTItem = blendingItem.PaletteColorLookupTableSequence[0] paletteColorLUT = new dmv.color.PaletteColorLookupTable({ uid: ( - cpLUTItem.PaletteColorLookupTableUID != null + cpLUTItem.PaletteColorLookupTableUID !== null && cpLUTItem.PaletteColorLookupTableUID !== undefined ? cpLUTItem.PaletteColorLookupTableUID : '' ), @@ -883,42 +483,42 @@ class SlideViewer extends React.Component { blueDescriptor: cpLUTItem.BluePaletteColorLookupTableDescriptor, redData: ( - (cpLUTItem.RedPaletteColorLookupTableData != null) + (cpLUTItem.RedPaletteColorLookupTableData !== null && cpLUTItem.RedPaletteColorLookupTableData !== undefined) ? new Uint16Array( cpLUTItem.RedPaletteColorLookupTableData ) : undefined ), greenData: ( - (cpLUTItem.GreenPaletteColorLookupTableData != null) + (cpLUTItem.GreenPaletteColorLookupTableData !== null && cpLUTItem.GreenPaletteColorLookupTableData !== undefined) ? new Uint16Array( cpLUTItem.GreenPaletteColorLookupTableData ) : undefined ), blueData: ( - (cpLUTItem.BluePaletteColorLookupTableData != null) + (cpLUTItem.BluePaletteColorLookupTableData !== null && cpLUTItem.BluePaletteColorLookupTableData !== undefined) ? new Uint16Array( cpLUTItem.BluePaletteColorLookupTableData ) : undefined ), redSegmentedData: ( - (cpLUTItem.SegmentedRedPaletteColorLookupTableData != null) + (cpLUTItem.SegmentedRedPaletteColorLookupTableData !== null && cpLUTItem.SegmentedRedPaletteColorLookupTableData !== undefined) ? new Uint16Array( cpLUTItem.SegmentedRedPaletteColorLookupTableData ) : undefined ), greenSegmentedData: ( - (cpLUTItem.SegmentedGreenPaletteColorLookupTableData != null) + (cpLUTItem.SegmentedGreenPaletteColorLookupTableData !== null && cpLUTItem.SegmentedGreenPaletteColorLookupTableData !== undefined) ? new Uint16Array( cpLUTItem.SegmentedGreenPaletteColorLookupTableData ) : undefined ), blueSegmentedData: ( - (cpLUTItem.SegmentedBluePaletteColorLookupTableData != null) + (cpLUTItem.SegmentedBluePaletteColorLookupTableData !== null && cpLUTItem.SegmentedBluePaletteColorLookupTableData !== undefined) ? new Uint16Array( cpLUTItem.SegmentedBluePaletteColorLookupTableData ) @@ -928,7 +528,7 @@ class SlideViewer extends React.Component { } let limitValues - if (blendingItem.SoftcopyVOILUTSequence != null) { + if (blendingItem.SoftcopyVOILUTSequence !== null && blendingItem.SoftcopyVOILUTSequence !== undefined) { const voiLUTItem = blendingItem.SoftcopyVOILUTSequence[0] const windowCenter = voiLUTItem.WindowCenter const windowWidth = voiLUTItem.WindowWidth @@ -951,7 +551,7 @@ class SlideViewer extends React.Component { const selectedOpticalPathIdentifiers: Set = new Set() Object.keys(opticalPathStyles).forEach(identifier => { const styleOptions = opticalPathStyles[identifier] - if (styleOptions != null) { + if (styleOptions !== null) { this.volumeViewer.setOpticalPathStyle(identifier, styleOptions) this.volumeViewer.activateOpticalPath(identifier) this.volumeViewer.showOpticalPath(identifier) @@ -1042,7 +642,7 @@ class SlideViewer extends React.Component { Modality: 'SR' } }).then((matchedInstances): void => { - if (matchedInstances === null) { + if (matchedInstances === null || matchedInstances === undefined) { matchedInstances = [] } matchedInstances.forEach(i => { @@ -1062,7 +662,7 @@ class SlideViewer extends React.Component { * Perform a couple of checks to ensure the document content of the * report fullfils the requirements of the application. */ - if (!_implementsTID1500(report)) { + if (!implementsTID1500(report)) { console.debug( `ignore SR document "${report.SOPInstanceUID}" ` + 'because it is not structured according to template ' + @@ -1070,14 +670,14 @@ class SlideViewer extends React.Component { ) return } - if (!_describesSpecimenSubject(report)) { + if (!describesSpecimenSubject(report)) { console.debug( `ignore SR document "${report.SOPInstanceUID}" ` + 'because it does not describe a specimen subject' ) return } - if (!_containsROIAnnotations(report)) { + if (!containsROIAnnotations(report)) { console.debug( `ignore SR document "${report.SOPInstanceUID}" ` + 'because it does not contain any suitable ROI annotations' @@ -1102,7 +702,7 @@ class SlideViewer extends React.Component { */ const doesROIExist = this.volumeViewer.getAllROIs().some( (otherROI: dmv.roi.ROI): boolean => { - return _areROIsEqual(otherROI, roi) + return areROIsEqual(otherROI, roi) } ) if (!doesROIExist) { @@ -1184,7 +784,7 @@ class SlideViewer extends React.Component { Modality: 'ANN' } }).then((matchedSeries): void => { - if (matchedSeries === null) { + if (matchedSeries === null || matchedSeries === undefined) { matchedSeries = [] } matchedSeries.forEach(s => { @@ -1209,7 +809,7 @@ class SlideViewer extends React.Component { annotations.forEach(ann => { try { this.volumeViewer.addAnnotationGroups(ann) - } catch (error: any) { + } catch (error: unknown) { // eslint-disable-next-line @typescript-eslint/no-floating-promises NotificationMiddleware.onError( NotificationMiddlewareContext.SLIM, @@ -1224,10 +824,10 @@ class SlideViewer extends React.Component { ann.AnnotationGroupSequence.forEach(item => { const annotationGroupUID = item.AnnotationGroupUID const finding = item.AnnotationPropertyTypeCodeSequence[0] - const key = _buildKey(finding) + const key = buildKey(finding) const style = this.roiStyles[key] // eslint-disable-next-line @typescript-eslint/prefer-optional-chain - if (style != null && style.fill != null) { + if (style !== null && style !== undefined && style.fill !== null && style.fill !== undefined) { this.volumeViewer.setAnnotationGroupStyle( annotationGroupUID, { color: style.fill.color } @@ -1286,7 +886,7 @@ class SlideViewer extends React.Component { Modality: 'SEG' } }).then((matchedSeries): void => { - if (matchedSeries === null) { + if (matchedSeries === null || matchedSeries === undefined) { matchedSeries = [] } matchedSeries.forEach((s, i) => { @@ -1310,7 +910,7 @@ class SlideViewer extends React.Component { if (segmentations.length > 0) { try { this.volumeViewer.addSegments(segmentations) - } catch (error: any) { + } catch (error: unknown) { // eslint-disable-next-line @typescript-eslint/no-floating-promises NotificationMiddleware.onError( NotificationMiddlewareContext.SLIM, @@ -1373,7 +973,7 @@ class SlideViewer extends React.Component { Modality: 'OT' } }).then((matchedSeries): void => { - if (matchedSeries === null) { + if (matchedSeries === null || matchedSeries === undefined) { matchedSeries = [] } matchedSeries.forEach(s => { @@ -1401,7 +1001,7 @@ class SlideViewer extends React.Component { if (parametricMaps.length > 0) { try { this.volumeViewer.addParameterMappings(parametricMaps) - } catch (error: any) { + } catch (error: unknown) { // eslint-disable-next-line @typescript-eslint/no-floating-promises NotificationMiddleware.onError( NotificationMiddlewareContext.SLIM, @@ -1458,12 +1058,12 @@ class SlideViewer extends React.Component { presentationStates: [] }) - if (this.volumeViewportRef.current != null) { + if (this.volumeViewportRef.current !== null) { this.volumeViewer.render({ container: this.volumeViewportRef.current }) } if ( - this.labelViewportRef.current != null && - this.labelViewer != null + this.labelViewportRef.current !== null && + this.labelViewer !== null && this.labelViewer !== undefined ) { this.labelViewer.render({ container: this.labelViewportRef.current }) } @@ -1477,7 +1077,7 @@ class SlideViewer extends React.Component { // Handle promises properly with catch blocks void this.addAnnotations() .then(() => { - if (this.props.derivedDataset != null) { + if (this.props.derivedDataset !== null && this.props.derivedDataset !== undefined) { this.loadDerivedDataset(this.props.derivedDataset) } }) @@ -1487,7 +1087,7 @@ class SlideViewer extends React.Component { void this.addAnnotationGroups() .then(() => { - if (this.props.derivedDataset != null) { + if (this.props.derivedDataset !== null && this.props.derivedDataset !== undefined) { this.loadDerivedDataset(this.props.derivedDataset) } }) @@ -1497,7 +1097,7 @@ class SlideViewer extends React.Component { void this.addSegmentations() .then(() => { - if (this.props.derivedDataset != null) { + if (this.props.derivedDataset !== null && this.props.derivedDataset !== undefined) { this.loadDerivedDataset(this.props.derivedDataset) } }) @@ -1507,7 +1107,7 @@ class SlideViewer extends React.Component { void this.addParametricMaps() .then(() => { - if (this.props.derivedDataset != null) { + if (this.props.derivedDataset !== null && this.props.derivedDataset !== undefined) { this.loadDerivedDataset(this.props.derivedDataset) } }) @@ -1526,7 +1126,7 @@ class SlideViewer extends React.Component { onWindowResize = (event: Event): void => { console.info('resize viewports') this.volumeViewer.resize() - if (this.labelViewer != null) { + if (this.labelViewer !== null && this.labelViewer !== undefined) { this.labelViewer.resize() } } @@ -1555,7 +1155,7 @@ class SlideViewer extends React.Component { }) roi.addEvaluation(item) }) - const key = _buildKey(selectedFinding) + const key = buildKey(selectedFinding) const style = this.getRoiStyle(key) this.volumeViewer.addROI(roi, style) this.setState(state => { @@ -1570,7 +1170,7 @@ class SlideViewer extends React.Component { onRoiDoubleClicked = (event: CustomEventInit): void => { const selectedRoi = event.detail.payload as dmv.roi.ROI - if (selectedRoi != null) { + if (selectedRoi !== null) { this.setState({ isSelectedRoiModalVisible: true }) @@ -1646,8 +1246,8 @@ class SlideViewer extends React.Component { } getUniqueHoveredRois = (newRoi: dmv.roi.ROI | null): dmv.roi.ROI[] => { - if (newRoi === null) { - return [] + if (newRoi === null || newRoi === undefined) { + return this.hoveredRois } const allRois = [...this.hoveredRois, newRoi] const uniqueIds = Array.from(new Set(allRois.map(roi => roi.uid))) @@ -1655,7 +1255,7 @@ class SlideViewer extends React.Component { .filter((roi): roi is dmv.roi.ROI => roi !== undefined) } - isSamePixelAsLast = (event: any): boolean => { + isSamePixelAsLast = (event: MouseEvent): boolean => { return event.clientX === this.lastPixel[0] && event.clientY === this.lastPixel[1] } @@ -1724,7 +1324,7 @@ class SlideViewer extends React.Component { return } - const key = _getRoiKey(roi) + const key = getRoiKey(roi) const style = this.getRoiStyle(key) this.volumeViewer.setROIStyle(uid, style) }) @@ -1754,7 +1354,7 @@ class SlideViewer extends React.Component { this.resetUnselectedRoiStyles(updatedSelectedRois) } - handleAnnotationSelection (uid: string): void { + handleAnnotationSelection = (uid: string): void => { // @ts-expect-error this.volumeViewer.clearSelections() @@ -1771,7 +1371,7 @@ class SlideViewer extends React.Component { }) } else { if (this.state.visibleRoiUIDs.has(roi.uid)) { - const key = _getRoiKey(roi) + const key = getRoiKey(roi) style = this.getRoiStyle(key) } } @@ -1779,7 +1379,8 @@ class SlideViewer extends React.Component { }) } - handleRoiSelectionCancellation (): void { + handleRoiSelectionCancellation = (): void => { + console.info('cancel ROI selection') this.setState({ isSelectedRoiModalVisible: false }) @@ -1854,7 +1455,7 @@ class SlideViewer extends React.Component { const opticalPathIdentifier = frameInfo.channelIdentifier if ( !(opticalPathIdentifier in this.state.pixelDataStatistics) && - frameInfo.pixelArray != null + frameInfo.pixelArray !== null ) { /* * There are limits on the number of arguments Math.min and Math.max @@ -1875,7 +1476,7 @@ class SlideViewer extends React.Component { const max = Math.max(...maxValues) this.setState(state => { const stats = state.pixelDataStatistics - if (stats[opticalPathIdentifier] != null) { + if (stats[opticalPathIdentifier] !== null && stats[opticalPathIdentifier] !== undefined) { stats[opticalPathIdentifier] = { min: Math.min(stats[opticalPathIdentifier].min, min), max: Math.max(stats[opticalPathIdentifier].max, max), @@ -1888,7 +1489,7 @@ class SlideViewer extends React.Component { numFramesSampled: 1 } } - if (state.selectedPresentationStateUID === null) { + if (state.selectedPresentationStateUID === null || state.selectedPresentationStateUID === undefined) { const style = { ...this.volumeViewer.getOpticalPathStyle(opticalPathIdentifier) } @@ -1909,7 +1510,7 @@ class SlideViewer extends React.Component { console.debug(`removed ROI "${roi.uid}"`) } - componentCleanup (): void { + componentCleanup = (): void => { document.body.removeEventListener( 'dicommicroscopyviewer_roi_drawn', this.onRoiDrawn @@ -1965,7 +1566,7 @@ class SlideViewer extends React.Component { window.removeEventListener('resize', this.onWindowResize) this.volumeViewer.cleanup() - if (this.labelViewer != null) { + if (this.labelViewer !== null && this.labelViewer !== undefined) { this.labelViewer.cleanup() } /* @@ -2025,15 +1626,15 @@ class SlideViewer extends React.Component { } } - componentWillUnmount (): void { + componentWillUnmount = (): void => { this.volumeViewer.cleanup() - if (this.labelViewer != null) { + if (this.labelViewer !== null && this.labelViewer !== undefined) { this.labelViewer.cleanup() } window.removeEventListener('beforeunload', this.componentCleanup) } - componentSetup (): void { + componentSetup = (): void => { document.body.addEventListener( 'dicommicroscopyviewer_roi_drawn', this.onRoiDrawn @@ -2098,7 +1699,7 @@ class SlideViewer extends React.Component { window.addEventListener('resize', this.onWindowResize) } - componentDidMount (): void { + componentDidMount = (): void => { this.componentSetup() this.populateViewports() @@ -2106,7 +1707,7 @@ class SlideViewer extends React.Component { let hasICCProfile = false const image = this.props.slide.volumeImages[0] const metadataItem = image.OpticalPathSequence[0] - if (metadataItem.ICCProfile === null) { + if (metadataItem.ICCProfile === null || metadataItem.ICCProfile === undefined) { if ('OpticalPathSequence' in image.bulkdataReferences) { // @ts-expect-error const bulkdataItem = image.bulkdataReferences.OpticalPathSequence[0] @@ -2132,7 +1733,7 @@ class SlideViewer extends React.Component { */ handleAnnotationFindingSelection ( value: string, - option: any + _option: { label: React.ReactNode } ): void { this.findingOptions.forEach(finding => { if (finding.CodeValue === value) { @@ -2152,7 +1753,7 @@ class SlideViewer extends React.Component { * @param value - Code value of the coded finding that got selected * @param option - Option that got selected */ - handleAnnotationGeometryTypeSelection (value: string, option: any): void { + handleAnnotationGeometryTypeSelection = (value: string, _option: { label: string }): void => { this.setState({ selectedGeometryType: value }) } @@ -2160,7 +1761,7 @@ class SlideViewer extends React.Component { * Handler that gets called when measurements have been selected for * annotation. */ - handleAnnotationMeasurementActivation (event: any): void { + handleAnnotationMeasurementActivation = (event: CheckboxChangeEvent): void => { const active: boolean = event.target.checked if (active) { this.setState({ selectedMarkup: 'measurement' }) @@ -2176,13 +1777,13 @@ class SlideViewer extends React.Component { * @param value - Code value of the coded evaluation that got selected * @param option - Option that got selected */ - handleAnnotationEvaluationSelection ( + handleAnnotationEvaluationSelection = ( value: string, - option: any - ): void { + option: { label: dcmjs.sr.coding.CodedConcept } + ): void => { const selectedFinding = this.state.selectedFinding if (selectedFinding !== undefined) { - const key = _buildKey(selectedFinding) + const key = buildKey(selectedFinding) const name = option.label this.evaluationOptions[key].forEach(evaluation => { if ( @@ -2211,78 +1812,78 @@ class SlideViewer extends React.Component { * Handler that gets called when an evaluation has been cleared for an * annotation. */ - handleAnnotationEvaluationClearance (): void { + handleAnnotationEvaluationClearance = (): void => { this.setState({ selectedEvaluations: [] }) } - handleXCoordinateSelection (value: any): void { - if (value != null) { + handleXCoordinateSelection = (value: number | string | null): void => { + if (value !== null && value !== undefined) { const x = Number(value) - const start = this.state.validXCoordinateRange[0] - const end = this.state.validXCoordinateRange[1] - if (x >= start && x <= end) { - this.setState({ + this.setState(state => { + const isValid = x >= state.validXCoordinateRange[0] && x <= state.validXCoordinateRange[1] + return { selectedXCoordinate: x, - isSelectedXCoordinateValid: true - }) - return - } + isSelectedXCoordinateValid: isValid + } + }) + } else { + this.setState({ + selectedXCoordinate: undefined, + isSelectedXCoordinateValid: false + }) } - this.setState({ - selectedXCoordinate: undefined, - isSelectedXCoordinateValid: false - }) } - handleYCoordinateSelection (value: any): void { - if (value != null) { + handleYCoordinateSelection = (value: number | string | null): void => { + if (value !== null && value !== undefined) { const y = Number(value) - const start = this.state.validYCoordinateRange[0] - const end = this.state.validYCoordinateRange[1] - if (y >= start && y <= end) { - this.setState({ + this.setState(state => { + const isValid = y >= state.validYCoordinateRange[0] && y <= state.validYCoordinateRange[1] + return { selectedYCoordinate: y, - isSelectedYCoordinateValid: true - }) - return - } + isSelectedYCoordinateValid: isValid + } + }) + } else { + this.setState({ + selectedYCoordinate: undefined, + isSelectedYCoordinateValid: false + }) } - this.setState({ - selectedYCoordinate: undefined, - isSelectedYCoordinateValid: false - }) } - handleMagnificationSelection (value: any): void { - if (value != null) { - if (value > 0 && value <= 40) { - this.setState({ - selectedMagnification: Number(value), - isSelectedMagnificationValid: true - }) - return - } + handleMagnificationSelection = (value: number | string | null): void => { + if (value !== null && value !== undefined) { + const magnification = Number(value) + this.setState(() => { + const isValid = magnification >= 0 && magnification <= 40 + return { + selectedMagnification: magnification, + isSelectedMagnificationValid: isValid + } + }) + } else { + this.setState({ + selectedMagnification: undefined, + isSelectedMagnificationValid: false + }) } - this.setState({ - selectedMagnification: undefined, - isSelectedMagnificationValid: false - }) } /** * Handler that gets called when the selection of slide position was * completed. */ - handleSlidePositionSelection (): void { + handleSlidePositionSelection = (): void => { if ( this.state.isSelectedXCoordinateValid && this.state.isSelectedYCoordinateValid && this.state.isSelectedMagnificationValid && - this.state.selectedXCoordinate != null && - this.state.selectedYCoordinate != null && - this.state.selectedMagnification != null + this.state.selectedXCoordinate !== null && this.state.selectedXCoordinate !== undefined && + this.state.selectedYCoordinate !== null && this.state.selectedYCoordinate !== undefined && + this.state.selectedMagnification !== null && this.state.selectedMagnification !== undefined ) { console.info( 'select slide position ' + @@ -2336,13 +1937,10 @@ class SlideViewer extends React.Component { * Handler that gets called when the selection of a slide position was * canceled. */ - handleSlidePositionSelectionCancellation (): void { - console.log('cancel slide position selection') + handleSlidePositionSelectionCancellation = (): void => { + console.info('cancel slide position selection') this.setState({ isGoToModalVisible: false, - isSelectedXCoordinateValid: false, - isSelectedYCoordinateValid: false, - isSelectedMagnificationValid: false, selectedXCoordinate: undefined, selectedYCoordinate: undefined, selectedMagnification: undefined @@ -2352,7 +1950,7 @@ class SlideViewer extends React.Component { /** * Handler that gets called when annotation configuration has been completed. */ - handleAnnotationConfigurationCompletion (): void { + handleAnnotationConfigurationCompletion = (): void => { console.debug('complete annotation configuration') const finding = this.state.selectedFinding const geometryType = this.state.selectedGeometryType @@ -2377,8 +1975,9 @@ class SlideViewer extends React.Component { /** * Handler that gets called when annotation configuration has been cancelled. */ - handleAnnotationConfigurationCancellation (): void { - console.debug('cancel annotation configuration') + handleAnnotationConfigurationCancellation = (): void => { + console.info('cancel annotation configuration') + this.volumeViewer.activateSelectInteraction({}) this.setState({ isAnnotationModalVisible: false, isRoiDrawingActive: false @@ -2389,7 +1988,7 @@ class SlideViewer extends React.Component { * Handler that gets called when a report should be generated for the current * set of annotations. */ - handleReportGeneration (): void { + handleReportGeneration = (): void => { console.info('save ROIs') const rois = this.volumeViewer.getAllROIs() const opticalPaths = this.volumeViewer.getAllOpticalPaths() @@ -2416,50 +2015,14 @@ class SlideViewer extends React.Component { * list of annotations will be presented to the user together with other * pertinent metadata about the patient, study, and specimen. */ - handleReportVerification (): void { - console.info('verfied report') - - const report = this.state.generatedReport - if (report !== undefined) { - const dataset = report as unknown as dmv.metadata.Comprehensive3DSR - console.debug('create File Meta Information') - const fileMetaInformationVersionArray = new Uint8Array(2) - fileMetaInformationVersionArray[1] = 1 - const fileMeta = { - // FileMetaInformationVersion - '00020001': { - Value: [fileMetaInformationVersionArray.buffer], - vr: 'OB' - }, - // MediaStorageSOPClassUID - '00020002': { - Value: [dataset.SOPClassUID], - vr: 'UI' - }, - // MediaStorageSOPInstanceUID - '00020003': { - Value: [dataset.SOPInstanceUID], - vr: 'UI' - }, - // TransferSyntaxUID - '00020010': { - Value: ['1.2.840.10008.1.2.1'], - vr: 'UI' - }, - // ImplementationClassUID - '00020012': { - Value: [this.props.app.uid], - vr: 'UI' - } - } - - console.info('store Comprehensive 3D SR document') - const writer = new dcmjs.data.DicomDict(fileMeta) - writer.dict = dcmjs.data.DicomMetaDictionary.denaturalizeDataset(dataset) - const buffer = writer.write() + handleReportVerification = (): void => { + console.info('verify report generation') + if (this.state.generatedReport !== undefined) { const client = this.props.clients[StorageClasses.COMPREHENSIVE_3D_SR] - client.storeInstances({ datasets: [buffer] }).then( - (response: any) => message.info('Annotations were saved.') + // The Comprehensive3DSR object should have a write method or similar + // For now, let's try to access it as an ArrayBuffer directly + client.storeInstances({ datasets: [(this.state.generatedReport as unknown as dcmjs.data.DicomDict).write()] }).then( + () => message.info('Annotations were saved.') ).catch((error) => { console.error(error) // eslint-disable-next-line @typescript-eslint/no-floating-promises @@ -2481,7 +2044,7 @@ class SlideViewer extends React.Component { /** * Handler that gets called when report generation has been cancelled. */ - handleReportCancellation (): void { + handleReportCancellation = (): void => { this.setState({ isReportModalVisible: false, generatedReport: undefined @@ -2492,14 +2055,14 @@ class SlideViewer extends React.Component { * Handle toggling of annotation visibility, i.e., whether a given * annotation should be either displayed or hidden by the viewer. */ - handleAnnotationVisibilityChange ({ roiUID, isVisible }: { + handleAnnotationVisibilityChange = ({ roiUID, isVisible }: { roiUID: string isVisible: boolean - }): void { + }): void => { if (isVisible) { console.info(`show ROI ${roiUID}`) const roi = this.volumeViewer.getROI(roiUID) - const key = _getRoiKey(roi) + const key = getRoiKey(roi) const style = this.getRoiStyle(key) this.volumeViewer.setROIStyle(roi.uid, style) this.setState(state => { @@ -2524,13 +2087,13 @@ class SlideViewer extends React.Component { * Handle toggling of annotation group visibility, i.e., whether a given * annotation group should be either displayed or hidden by the viewer. */ - handleAnnotationGroupVisibilityChange ({ annotationGroupUID, isVisible }: { + handleAnnotationGroupVisibilityChange = ({ annotationGroupUID, isVisible }: { annotationGroupUID: string isVisible: boolean - }): void { + }): void => { const allAnnotationGroups = this.volumeViewer.getAllAnnotationGroups() const annotationGroup = allAnnotationGroups.find(ag => ag.uid === annotationGroupUID) - if (annotationGroup !== null) { + if (annotationGroup !== null && annotationGroup !== undefined) { runValidations({ dialog: true, context: { annotationGroup, slide: this.props.slide } @@ -2576,14 +2139,14 @@ class SlideViewer extends React.Component { /** * Handle change of annotation group style. */ - handleAnnotationGroupStyleChange ({ uid, styleOptions }: { + handleAnnotationGroupStyleChange = ({ uid, styleOptions }: { uid: string styleOptions: { opacity?: number color?: number[] measurement?: dcmjs.sr.coding.CodedConcept } - }): void { + }): void => { console.log(`change style of annotation group ${uid}`) try { this.volumeViewer.setAnnotationGroupStyle( @@ -2603,12 +2166,12 @@ class SlideViewer extends React.Component { } } - generateRoiStyle ( - styleOptions: StyleOptions): dmv.viewer.ROIStyleOptions { + generateRoiStyle = ( + styleOptions: StyleOptions): dmv.viewer.ROIStyleOptions => { const opacity = styleOptions.opacity ?? DEFAULT_ANNOTATION_OPACITY const strokeColor = styleOptions.color ?? DEFAULT_ANNOTATION_STROKE_COLOR const fillColor = styleOptions.contourOnly ? [0, 0, 0, 0] : strokeColor.map((c) => Math.min(c + 25, 255)) - const style = _formatRoiStyle({ + const style = formatRoiStyle({ fill: { color: [...fillColor, opacity] }, stroke: { color: [...strokeColor, opacity] }, radius: this.defaultRoiStyle.stroke?.width @@ -2616,17 +2179,17 @@ class SlideViewer extends React.Component { return style } - handleRoiStyleChange ({ uid, styleOptions }: { + handleRoiStyleChange = ({ uid, styleOptions }: { uid: string styleOptions: StyleOptions - }): void { + }): void => { console.log(`change style of ROI ${uid}`) try { this.defaultAnnotationStyles[uid] = styleOptions const style = this.generateRoiStyle(styleOptions) const roi = this.volumeViewer.getROI(uid) - const key = _getRoiKey(roi) as string + const key = getRoiKey(roi) as string this.roiStyles[key] = style this.volumeViewer.setROIStyle(uid, style) this.state.visibleRoiUIDs.add(uid) @@ -2647,10 +2210,10 @@ class SlideViewer extends React.Component { * Handle toggling of segment visibility, i.e., whether a given * segment should be either displayed or hidden by the viewer. */ - handleSegmentVisibilityChange ({ segmentUID, isVisible }: { + handleSegmentVisibilityChange = ({ segmentUID, isVisible }: { segmentUID: string isVisible: boolean - }): void { + }): void => { console.log(`change visibility of segment ${segmentUID}`) if (isVisible) { console.info(`show segment ${segmentUID}`) @@ -2674,12 +2237,12 @@ class SlideViewer extends React.Component { /** * Handle change of segment style. */ - handleSegmentStyleChange ({ segmentUID, styleOptions }: { + handleSegmentStyleChange = ({ segmentUID, styleOptions }: { segmentUID: string styleOptions: { opacity?: number } - }): void { + }): void => { console.log(`change style of segment ${segmentUID}`) this.volumeViewer.setSegmentStyle(segmentUID, styleOptions) } @@ -2688,10 +2251,10 @@ class SlideViewer extends React.Component { * Handle toggling of mapping visibility, i.e., whether a given * mapping should be either displayed or hidden by the viewer. */ - handleMappingVisibilityChange ({ mappingUID, isVisible }: { + handleMappingVisibilityChange = ({ mappingUID, isVisible }: { mappingUID: string isVisible: boolean - }): void { + }): void => { console.log(`change visibility of mapping ${mappingUID}`) if (isVisible) { console.info(`show mapping ${mappingUID}`) @@ -2715,12 +2278,12 @@ class SlideViewer extends React.Component { /** * Handle change of mapping style. */ - handleMappingStyleChange ({ mappingUID, styleOptions }: { + handleMappingStyleChange = ({ mappingUID, styleOptions }: { mappingUID: string styleOptions: { opacity?: number } - }): void { + }): void => { console.log(`change style of mapping ${mappingUID}`) this.volumeViewer.setParameterMappingStyle(mappingUID, styleOptions) } @@ -2729,10 +2292,10 @@ class SlideViewer extends React.Component { * Handle toggling of optical path visibility, i.e., whether a given * optical path should be either displayed or hidden by the viewer. */ - handleOpticalPathVisibilityChange ({ opticalPathIdentifier, isVisible }: { + handleOpticalPathVisibilityChange = ({ opticalPathIdentifier, isVisible }: { opticalPathIdentifier: string isVisible: boolean - }): void { + }): void => { console.log(`change visibility of optical path ${opticalPathIdentifier}`) if (isVisible) { console.info(`show optical path ${opticalPathIdentifier}`) @@ -2760,14 +2323,14 @@ class SlideViewer extends React.Component { /** * Handle change of optical path style. */ - handleOpticalPathStyleChange ({ opticalPathIdentifier, styleOptions }: { + handleOpticalPathStyleChange = ({ opticalPathIdentifier, styleOptions }: { opticalPathIdentifier: string styleOptions: { opacity?: number color?: number[] limitValues?: number[] } - }): void { + }): void => { console.log(`change style of optical path ${opticalPathIdentifier}`) this.volumeViewer.setOpticalPathStyle(opticalPathIdentifier, styleOptions) } @@ -2776,10 +2339,10 @@ class SlideViewer extends React.Component { * Handle toggling of optical path activity, i.e., whether a given * optical path should be either added or removed from the viewport. */ - handleOpticalPathActivityChange ({ opticalPathIdentifier, isActive }: { + handleOpticalPathActivityChange = ({ opticalPathIdentifier, isActive }: { opticalPathIdentifier: string isActive: boolean - }): void { + }): void => { console.log(`change activity of optical path ${opticalPathIdentifier}`) if (isActive) { console.info(`activate optical path ${opticalPathIdentifier}`) @@ -2808,7 +2371,7 @@ class SlideViewer extends React.Component { * Set default presentation state that is either defined by metadata included * in the DICOM Slide Microscopy instance or by the viewer. */ - setDefaultPresentationState (): void { + setDefaultPresentationState = (): void => { const visibleOpticalPathIdentifiers: Set = new Set() const opticalPaths = this.volumeViewer.getAllOpticalPaths() opticalPaths.sort((a, b) => { @@ -2830,7 +2393,7 @@ class SlideViewer extends React.Component { * If the image metadata contains a palette color lookup table for the * optical path, then it will be displayed by default. */ - if (item.paletteColorLookupTableUID != null) { + if (item.paletteColorLookupTableUID !== null) { visibleOpticalPathIdentifiers.add(identifier) } } else { @@ -2859,7 +2422,7 @@ class SlideViewer extends React.Component { const index = numVisible style.color = defaultColors[index] const stats = this.state.pixelDataStatistics[item.identifier] - if (stats != null) { + if (stats !== null) { style.limitValues = [stats.min, stats.max] } this.volumeViewer.setOpticalPathStyle(item.identifier, style) @@ -2886,7 +2449,7 @@ class SlideViewer extends React.Component { * Handler that gets called when a presentation state has been selected from * the current list of available presentation states. */ - handlePresentationStateReset (): void { + handlePresentationStateReset = (): void => { this.setState({ selectedPresentationStateUID: undefined }) const urlPath = this.props.location.pathname this.props.navigate(urlPath) @@ -2897,21 +2460,21 @@ class SlideViewer extends React.Component { * Handler that gets called when a presentation state has been selected from * the current list of available presentation states. */ - handlePresentationStateSelection ( + handlePresentationStateSelection = ( value?: string, - option?: any - ): void { - if (value != null) { - console.info(`select Presentation State instance "${value}"`) + _option?: unknown + ): void => { + if (value !== null) { + console.info(`select Presentation State instance "${value ?? 'undefined'}"`) let presentationState this.state.presentationStates.forEach(instance => { if (instance.SOPInstanceUID === value) { presentationState = instance } }) - if (presentationState != null) { + if (presentationState !== null && presentationState !== undefined) { let urlPath = this.props.location.pathname - urlPath += `?state=${value}` + urlPath += `?state=${value ?? ''}` this.props.navigate(urlPath) this.setPresentationState(presentationState) } else { @@ -2925,7 +2488,7 @@ class SlideViewer extends React.Component { ) console.log( 'failed to handle section of presentation state: ' + - `could not find instance "${value}"` + `could not find instance "${value ?? 'undefined'}"` ) } } else { @@ -2938,7 +2501,7 @@ class SlideViewer extends React.Component { * Handler that will toggle the ROI drawing tool, i.e., either activate or * de-activate it, depending on its current state. */ - handleRoiDrawing (): void { + handleRoiDrawing = (): void => { if (this.state.isRoiDrawingActive) { console.info('deactivate drawing of ROIs') this.volumeViewer.deactivateDrawInteraction() @@ -2972,7 +2535,7 @@ class SlideViewer extends React.Component { * Handler that will toggle the ROI modification tool, i.e., either activate * or de-activate it, depending on its current state. */ - handleRoiModification (): void { + handleRoiModification = (): void => { console.info('toggle modification of ROIs') if (this.volumeViewer.isModifyInteractionActive) { this.volumeViewer.deactivateModifyInteraction() @@ -3001,7 +2564,7 @@ class SlideViewer extends React.Component { * Handler that will toggle the ROI translation tool, i.e., either activate * or de-activate it, depending on its current state. */ - handleRoiTranslation (): void { + handleRoiTranslation = (): void => { console.info('toggle translation of ROIs') if (this.volumeViewer.isTranslateInteractionActive) { this.volumeViewer.deactivateTranslateInteraction() @@ -3024,7 +2587,7 @@ class SlideViewer extends React.Component { } } - handleGoTo (): void { + handleGoTo = (): void => { this.volumeViewer.deactivateDrawInteraction() this.volumeViewer.deactivateModifyInteraction() this.volumeViewer.deactivateSnapInteraction() @@ -3045,7 +2608,7 @@ class SlideViewer extends React.Component { * Handler that will toggle the ROI removal tool, i.e., either activate * or de-activate it, depending on its current state. */ - handleRoiRemoval (): void { + handleRoiRemoval = (): void => { this.volumeViewer.deactivateDrawInteraction() this.volumeViewer.deactivateSnapInteraction() this.volumeViewer.deactivateTranslateInteraction() @@ -3087,7 +2650,7 @@ class SlideViewer extends React.Component { * Handler that will toggle the ROI visibility tool, i.e., either activate * or de-activate it, depending on its current state. */ - handleRoiVisibilityChange (): void { + handleRoiVisibilityChange = (): void => { console.info('toggle visibility of ROIs') if (!this.state.areRoisHidden) { this.volumeViewer.deactivateDrawInteraction() @@ -3118,6 +2681,27 @@ class SlideViewer extends React.Component { this.volumeViewer.zoomToROI(annotationGroupUID) } + handleAnnotationGroupSelection = (value: string): void => { + this.setState({ selectedSeriesInstanceUID: value }) + } + + getSeriesDescription = (seriesInstanceUID: string): string => { + // Get the study from DicomMetadataStore + const study = DicomMetadataStore.getStudy(this.props.studyInstanceUID) + + if ((study?.series) !== null && study !== null && study !== undefined) { + // Find the series that matches this series instance UID + const series = study.series.find(s => s.SeriesInstanceUID === seriesInstanceUID) + + if (series?.SeriesDescription !== undefined && series.SeriesDescription !== '') { + return series.SeriesDescription + } + } + + // Fallback to truncated UID if no description found + return `Series ${seriesInstanceUID.slice(0, 8)}...` + } + /** * Handler that will toggle the ICC profile color management, i.e., either * enable or disable it, depending on its current state. @@ -3128,9 +2712,9 @@ class SlideViewer extends React.Component { this.volumeViewer.toggleICCProfiles() } - formatAnnotation = (annotation: AnnotationCategoryAndType | dmv.roi.ROI): void => { + formatAnnotation = (annotation: AnnotationCategoryAndType): void => { const roi = this.volumeViewer.getROI(annotation.uid) - const key = _getRoiKey(roi) as string + const key = getRoiKey(roi) as string const color = this.roiStyles[key] !== undefined ? this.roiStyles[key].stroke?.color.slice(0, 3) : DEFAULT_ANNOTATION_COLOR_PALETTE[ @@ -3147,7 +2731,14 @@ class SlideViewer extends React.Component { ) } - render (): React.ReactNode { + // Helper functions to extract render logic + private readonly getDataFromViewer = (): { + rois: dmv.roi.ROI[] + segments: dmv.segment.Segment[] + mappings: dmv.mapping.ParameterMapping[] + annotationGroups: dmv.annotation.AnnotationGroup[] + annotations: AnnotationCategoryAndType[] + } => { const rois: dmv.roi.ROI[] = [] const segments: dmv.segment.Segment[] = [] const mappings: dmv.mapping.ParameterMapping[] = [] @@ -3163,19 +2754,24 @@ class SlideViewer extends React.Component { const annotations = rois.map(roi => adaptRoiToAnnotation(roi)) - const openSubMenuItems = [ - 'specimens', 'optical-paths', 'annotations', 'presentation-states' - ] + return { rois, segments, mappings, annotationGroups, annotations } + } + + private static getOpenSubMenuItems (): string[] { + return ['specimens', 'optical-paths', 'annotations', 'presentation-states'] + } - let report: React.ReactNode + private readonly getReport = (): React.ReactNode => { const dataset = this.state.generatedReport if (dataset !== undefined) { - report = + return } + return undefined + } - let annotationMenuItems: React.ReactNode + private readonly getAnnotationMenuItems = (rois: dmv.roi.ROI[]): React.ReactNode => { if (rois.length > 0) { - annotationMenuItems = ( + return ( { /> ) } + return undefined + } - const findingOptions = this.findingOptions.map((finding, index) => { + private readonly getFindingOptions = (): React.ReactNode[] => { + return this.findingOptions.map((finding, index) => { return ( { ) }) + } - const geometryTypeOptionsMapping: { [key: string]: React.ReactNode } = { + private static getGeometryTypeOptionsMapping (): { [key: string]: React.ReactNode } { + return { point: Point, circle: Circle, box: Box, @@ -3214,6 +2815,11 @@ class SlideViewer extends React.Component { ) } + } + + private readonly getAnnotationConfigurations = (): React.ReactNode[] => { + const findingOptions = this.getFindingOptions() + const geometryTypeOptionsMapping = SlideViewer.getGeometryTypeOptionsMapping() const annotationConfigurations: React.ReactNode[] = [ ( @@ -3230,7 +2836,7 @@ class SlideViewer extends React.Component { ] const selectedFinding = this.state.selectedFinding if (selectedFinding !== undefined) { - const key = _buildKey(selectedFinding) + const key = buildKey(selectedFinding) this.evaluationOptions[key].forEach((evaluation, index) => { const evaluationOptions = evaluation.values.map(code => { return ( @@ -3284,7 +2890,11 @@ class SlideViewer extends React.Component { ) } - const specimenMenu = ( + return annotationConfigurations + } + + private readonly getSpecimenMenu = (): React.ReactNode => { + return ( { /> ) + } - const equipmentMenu = ( + private readonly getEquipmentMenu = (): React.ReactNode => { + return ( ) + } + private readonly getOpticalPathMenu = (): React.ReactNode => { const opticalPaths = this.volumeViewer.getAllOpticalPaths() opticalPaths.sort((a, b) => { if (a.identifier.localeCompare(b.identifier) === 1) { @@ -3328,7 +2942,7 @@ class SlideViewer extends React.Component { } opticalPathStyles[identifier] = style }) - const opticalPathMenu = ( + return ( { /> ) + } - let presentationStateMenu + private readonly getPresentationStateMenu = (): React.ReactNode => { if (this.state.presentationStates.length > 0) { const presentationStateOptions = [] this.state.presentationStates.forEach((instance, index) => { @@ -3366,10 +2981,10 @@ class SlideViewer extends React.Component { dropdownMatchSelectWidth={false} size='small' > - <> + {null} ) - presentationStateMenu = ( + return ( } - type='primary' + icon={UndoOutlined} onClick={this.handlePresentationStateReset} /> @@ -3391,8 +3005,10 @@ class SlideViewer extends React.Component { ) } + return undefined + } - let segmentationMenu + private readonly getSegmentationMenu = (segments: dmv.segment.Segment[]): React.ReactNode => { if (segments.length > 0) { const defaultSegmentStyles: { [segmentUID: string]: { @@ -3402,7 +3018,6 @@ class SlideViewer extends React.Component { const segmentMetadata: { [segmentUID: string]: dmv.metadata.Segmentation[] } = {} - const segments = this.volumeViewer.getAllSegments() segments.forEach(segment => { defaultSegmentStyles[segment.uid] = this.volumeViewer.getSegmentStyle( segment.uid @@ -3411,7 +3026,7 @@ class SlideViewer extends React.Component { segment.uid ) }) - segmentationMenu = ( + return ( { /> ) - openSubMenuItems.push('segmentations') } + return undefined + } - let parametricMapMenu + private readonly getParametricMapMenu = (mappings: dmv.mapping.ParameterMapping[]): React.ReactNode => { if (mappings.length > 0) { const defaultMappingStyles: { [mappingUID: string]: { @@ -3444,7 +3060,7 @@ class SlideViewer extends React.Component { mapping.uid ) }) - parametricMapMenu = ( + return ( { /> ) - openSubMenuItems.push('parametric-maps') } + return undefined + } - let annotationGroupMenu - - annotations?.forEach?.(this.formatAnnotation.bind(this)) - + private readonly getAnnotationGroupMenu = (annotationGroups: dmv.annotation.AnnotationGroup[]): React.ReactNode => { if (annotationGroups.length > 0) { const annotationGroupMetadata: { [annotationGroupUID: string]: dmv.metadata.MicroscopyBulkSimpleAnnotations @@ -3481,62 +3095,118 @@ class SlideViewer extends React.Component { annotationGroup.uid ) }) - annotationGroupMenu = ( + + // Group annotation groups by seriesInstanceUID + const annotationGroupsBySeries: { [seriesInstanceUID: string]: dmv.annotation.AnnotationGroup[] } = {} + annotationGroups.forEach(annotationGroup => { + const seriesUID = annotationGroup.seriesInstanceUID + if (!(seriesUID in annotationGroupsBySeries)) { + annotationGroupsBySeries[seriesUID] = [] + } + annotationGroupsBySeries[seriesUID].push(annotationGroup) + }) + + // Initialize selected series if not set + if (this.state.selectedSeriesInstanceUID === undefined && annotationGroups.length !== 0) { + this.setState({ selectedSeriesInstanceUID: 'all' }) + } + + // Create dropdown options for series + const dropdownOptions = [ + { + value: 'all', + label: 'All' + }, + ...Object.keys(annotationGroupsBySeries).map((seriesUID) => ({ + value: seriesUID, + label: `${this.getSeriesDescription(seriesUID)} (${annotationGroupsBySeries[seriesUID]?.length ?? 0} groups)` + })) + ] + + // Get annotation groups for the selected series or all series + const selectedSeriesAnnotationGroups = this.state.selectedSeriesInstanceUID === 'all' + ? annotationGroups + : (this.state.selectedSeriesInstanceUID !== undefined + ? annotationGroupsBySeries[this.state.selectedSeriesInstanceUID] ?? [] + : []) + + return ( - + {/* Series Selection Dropdown */} +
+ setSelectedKeys( - e.target.value !== undefined ? [e.target.value] : [] - )} - onPressEnter={() => this.handleSearch(selectedKeys, confirm, dataIndex)} - style={{ width: 188, marginBottom: 8, display: 'block' }} + getColumnSearchProps = (dataIndex: string): object => { + return { + filterDropdown: ({ setSelectedKeys, selectedKeys, confirm, clearFilters }: { + setSelectedKeys: (selectedKeys: React.Key[]) => void + selectedKeys: React.Key[] + confirm: (params?: FilterConfirmProps) => void + clearFilters: () => void + }) => ( +
+ + + + + +
+ ), + filterIcon: (filtered: boolean) => ( + - - - - -
- ), - filterIcon: (filtered: boolean) => ( - - ) - }) + ) + } + } } export default withRouter(Worklist) diff --git a/src/components/__tests__/Worklist.test.tsx b/src/components/__tests__/Worklist.test.tsx index 8e22b802..d454aaac 100644 --- a/src/components/__tests__/Worklist.test.tsx +++ b/src/components/__tests__/Worklist.test.tsx @@ -1,6 +1,7 @@ import React from 'react' import { BrowserRouter } from 'react-router-dom' import { cleanup, render } from '@testing-library/react' +// skipcq: JS-C1003 import * as dwc from 'dicomweb-client' import DicomWebManager from '../../DicomWebManager' diff --git a/src/contexts/ValidationContext.tsx b/src/contexts/ValidationContext.tsx index 11a13554..943b31c0 100644 --- a/src/contexts/ValidationContext.tsx +++ b/src/contexts/ValidationContext.tsx @@ -211,7 +211,7 @@ export const useValidation = (): ValidationContextType => { } export const runValidations = (options: { dialog?: boolean, context: { annotationGroup?: dmv.annotation.AnnotationGroup, slide?: Slide } }): ValidationResult => { - if (globalValidationContext === null) { + if (globalValidationContext === null || globalValidationContext === undefined) { console.warn('Validation context not available. Make sure ValidationProvider is mounted.') return { isValid: true, type: 'info' } } diff --git a/src/data/slides.tsx b/src/data/slides.tsx index d007614e..63b4eaa6 100644 --- a/src/data/slides.tsx +++ b/src/data/slides.tsx @@ -1,3 +1,4 @@ +// skipcq: JS-C1003 import * as dmv from 'dicom-microscopy-viewer' import { CustomError, errorTypes } from '../utils/CustomError' import NotificationMiddleware, { @@ -22,7 +23,7 @@ const areSameAcquisition = ( image: dmv.metadata.VLWholeSlideMicroscopyImage, refImage: dmv.metadata.VLWholeSlideMicroscopyImage ): boolean => { - if (image.AcquisitionUID != null) { + if (image.AcquisitionUID !== null && image.AcquisitionUID !== undefined) { return image.AcquisitionUID === refImage.AcquisitionUID } return false @@ -100,7 +101,7 @@ class Slide { image.OpticalPathSequence.forEach(item => { opticalPathIdentifiers.add(item.OpticalPathIdentifier) }) - if (image.AcquisitionUID != null) { + if (image.AcquisitionUID !== null && image.AcquisitionUID !== undefined) { acquisitionUIDs.add(image.AcquisitionUID) } if (hasImageFlavor(image, ImageFlavors.VOLUME) || hasImageFlavor(image, ImageFlavors.THUMBNAIL)) { diff --git a/src/data/specimens.tsx b/src/data/specimens.tsx index 3f719cc8..72bd55e3 100644 --- a/src/data/specimens.tsx +++ b/src/data/specimens.tsx @@ -1,3 +1,4 @@ +// skipcq: JS-C1003 import * as dcmjs from 'dcmjs' export const SpecimenPreparationTypes: { diff --git a/src/index.tsx b/src/index.tsx index 585644f9..327fef9e 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -41,7 +41,7 @@ const originalMessage = { ...message } const createMessageConfig = (content: string | object): object => { const duration = config.messages?.duration ?? 5 - if (typeof content === 'object' && content !== null) { + if (typeof content === 'object' && content !== null && content !== undefined) { return { ...content, duration diff --git a/src/services/RoiToAnnotationAdapter.ts b/src/services/RoiToAnnotationAdapter.ts index 5a4bbf7b..399dd617 100644 --- a/src/services/RoiToAnnotationAdapter.ts +++ b/src/services/RoiToAnnotationAdapter.ts @@ -1,4 +1,6 @@ +// skipcq: JS-C1003 import * as dmv from 'dicom-microscopy-viewer' +// skipcq: JS-C1003 import * as dcmjs from 'dcmjs' import { AnnotationCategoryAndType } from '../components/AnnotationCategoryList' diff --git a/src/services/fetchImageMetadata.ts b/src/services/fetchImageMetadata.ts index e59af78a..03bd96f2 100644 --- a/src/services/fetchImageMetadata.ts +++ b/src/services/fetchImageMetadata.ts @@ -1,3 +1,4 @@ +// skipcq: JS-C1003 import * as dmv from 'dicom-microscopy-viewer' import DicomWebManager from '../DicomWebManager' diff --git a/src/setupTests.tsx b/src/setupTests.tsx index 2432ec6f..76a65db6 100644 --- a/src/setupTests.tsx +++ b/src/setupTests.tsx @@ -1,7 +1,30 @@ -// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions -global.matchMedia = global.matchMedia || function () { - return { - addListener: jest.fn(), - removeListener: jest.fn() +// jest-dom adds custom jest matchers for asserting on DOM nodes. +// allows you to do things like: +// expect(element).toHaveTextContent(/react/i) +// learn more: https://github.com/testing-library/jest-dom +import '@testing-library/jest-dom' + +global.matchMedia = global.matchMedia !== undefined + ? global.matchMedia + : function (query: string): MediaQueryList { + return { + media: query, + matches: false, + onchange: null, + addListener () { + // Mock implementation - intentionally empty + }, + removeListener () { + // Mock implementation - intentionally empty + }, + addEventListener () { + // Mock implementation - intentionally empty + }, + removeEventListener () { + // Mock implementation - intentionally empty + }, + dispatchEvent () { return false } + } } -} + +export {} diff --git a/src/types/annotations.ts b/src/types/annotations.ts new file mode 100644 index 00000000..d6bfbdfe --- /dev/null +++ b/src/types/annotations.ts @@ -0,0 +1,69 @@ +// skipcq: JS-C1003 +import * as dcmjs from 'dcmjs' + +/** + * Annotation settings configuration + */ +export interface AnnotationSettings { + finding: dcmjs.sr.coding.CodeOptions + findingCategory?: dcmjs.sr.coding.CodeOptions + evaluations?: EvaluationSetting[] + measurements?: MeasurementSetting[] + geometryTypes?: string[] + style?: { + stroke: { + color: number[] + width: number + } + fill: { + color: number[] + } + radius?: number + } +} + +/** + * Evaluation setting for annotations + */ +export interface EvaluationSetting { + name: dcmjs.sr.coding.CodeOptions + values: dcmjs.sr.coding.CodeOptions[] +} + +/** + * Measurement setting for annotations + */ +export interface MeasurementSetting { + name: dcmjs.sr.coding.CodeOptions + unit: dcmjs.sr.coding.CodeOptions +} + +/** + * Annotation category and type information + * Used by AnnotationCategoryList and related components + */ +export interface AnnotationCategoryAndType { + uid: string + type: Omit + category: Omit +} + +/** + * Type information for annotations + */ +export interface Type { + CodeValue: string + CodeMeaning: string + CodingSchemeDesignator: string + uids: string[] +} + +/** + * Category information for annotations + */ +export interface Category { + CodeValue: string + CodeMeaning: string + CodingSchemeDesignator: string + types: Type[] +} diff --git a/src/types/index.ts b/src/types/index.ts new file mode 100644 index 00000000..9db4abd0 --- /dev/null +++ b/src/types/index.ts @@ -0,0 +1,2 @@ +// Re-export all global types +export * from './annotations' diff --git a/src/utils/sr.tsx b/src/utils/sr.tsx index d22ba23c..3dba426e 100644 --- a/src/utils/sr.tsx +++ b/src/utils/sr.tsx @@ -1,3 +1,4 @@ +// skipcq: JS-C1003 import * as dcmjs from 'dcmjs' /** diff --git a/src/utils/values.ts b/src/utils/values.ts index 15668dbf..a45d2b1b 100644 --- a/src/utils/values.ts +++ b/src/utils/values.ts @@ -1,3 +1,4 @@ +// skipcq: JS-C1003 import * as dmv from 'dicom-microscopy-viewer' function parseName (value: dmv.metadata.PersonName|null|undefined): string { diff --git a/types/dicom-microscopy-viewer/index.d.ts b/types/dicom-microscopy-viewer/index.d.ts index 25c778a1..c9b7f333 100644 --- a/types/dicom-microscopy-viewer/index.d.ts +++ b/types/dicom-microscopy-viewer/index.d.ts @@ -1,7 +1,9 @@ declare module 'dicom-microscopy-viewer' { - import * as dwc from 'dicomweb-client' - import * as dcmjs from 'dcmjs' + // skipcq: JS-C1003 +import * as dwc from 'dicomweb-client' +// skipcq: JS-C1003 +import * as dcmjs from 'dcmjs' import { CustomError } from '../../src/utils/CustomError' declare namespace viewer {