import React from 'react' import { RouteComponentProps, withRouter } from 'react-router-dom' import { FaDrawPolygon, FaEye, FaEyeSlash, FaHandPaper, FaHandPointer, FaTrash, FaSave } from 'react-icons/fa' import { Button as Btn, Checkbox, message, Menu, Modal, Layout, Row, Select, Space, Tooltip } from 'antd' import { UndoOutlined } from '@ant-design/icons' import * as dmv from 'dicom-microscopy-viewer' import * as dcmjs from 'dcmjs' import * as dwc from 'dicomweb-client' 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 { findContentItemsByName } from '../utils/sr' import { Slide } from '../data/slides' import { SOPClassUIDs } from '../data/uids' const _buildKey = (concept: dcmjs.sr.coding.CodedConcept): string => { const codingScheme = concept.CodingSchemeDesignator const codeValue = concept.CodeValue return `${codingScheme}-${codeValue}` } const _getRoiKey = (roi: dmv.roi.ROI): string => { const matches = findContentItemsByName({ content: roi.evaluations, name: new dcmjs.sr.coding.CodedConcept({ value: '121071', meaning: 'Finding', schemeDesignator: 'DCM' }) }) if (matches.length === 0) { throw new Error(`No finding found for ROI ${roi.uid}`) } 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 _constructViewers = ({ client, slide, preload }: { client: dwc.api.DICOMwebClient slide: Slide preload?: boolean }): { volumeViewer: dmv.viewer.VolumeImageViewer labelViewer?: dmv.viewer.LabelImageViewer } => { const volumeViewer = new dmv.viewer.VolumeImageViewer({ client: client, metadata: slide.volumeImages, controls: ['overview'], preload: preload }) volumeViewer.activateSelectInteraction({}) let labelViewer if (slide.labelImages.length > 0) { labelViewer = new dmv.viewer.LabelImageViewer({ client: client, metadata: slide.labelImages[0], resizeFactor: 1, orientation: 'vertical' }) } return { volumeViewer, labelViewer } } /* * 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 SlideViewerProps extends RouteComponentProps { slide: Slide client: DicomWebManager studyInstanceUID: string seriesInstanceUID: string app: { name: string version: string uid: string organization?: string } preload?: boolean annotations: AnnotationSettings[] enableAnnotationTools: boolean user?: { name: string email: string } selectedPresentationStateUID?: string } interface SlideViewerState { selectedRoiUIDs: string[] visibleRoiUIDs: string[] visibleSegmentUIDs: string[] visibleMappingUIDs: string[] visibleAnnotationGroupUIDs: string[] visibleOpticalPathIdentifiers: string[] activeOpticalPathIdentifiers: string[] presentationStates: dmv.metadata.AdvancedBlendingPresentationState[] selectedPresentationStateUID?: string selectedFinding?: dcmjs.sr.coding.CodedConcept selectedEvaluations: Evaluation[] selectedGeometryType?: string selectedMarkup?: string generatedReport?: dmv.metadata.Comprehensive3DSR isLoading: boolean isAnnotationModalVisible: boolean isReportModalVisible: boolean isRoiDrawingActive: boolean isRoiModificationActive: boolean isRoiTranslationActive: boolean areRoisHidden: boolean pixelDataStatistics: { [opticalPathIdentifier: string]: { min: number max: number numFramesSampled: number } } defaultOpticalPathStyles: { [opticalPathIdentifier: string]: { color?: number[] paletteColorLookupTable?: dmv.color.PaletteColorLookupTable opacity?: number limitValues?: number[] } } } /** * React component for interactive viewing of an individual digital slide, * which corresponds to one DICOM Series of DICOM Slide Microscopy images and * potentially one or more associated DICOM Series of DICOM SR documents. */ class SlideViewer extends React.Component<SlideViewerProps, SlideViewerState> { private readonly findingOptions: dcmjs.sr.coding.CodedConcept[] = [] private readonly evaluationOptions: { [key: string]: EvaluationOptions[] } = {} private readonly geometryTypeOptions: { [key: string]: string[] } = {} private readonly volumeViewportRef: React.RefObject<HTMLDivElement> private readonly labelViewportRef: React.RefObject<HTMLDivElement> private volumeViewer: dmv.viewer.VolumeImageViewer private labelViewer?: dmv.viewer.LabelImageViewer private readonly defaultRoiStyle: dmv.viewer.ROIStyleOptions = { stroke: { color: [0, 126, 163], width: 2 }, fill: { color: [0, 126, 163, 0.1] } } private roiStyles: {[key: string]: dmv.viewer.ROIStyleOptions} = {} private readonly selectionColor: number[] = [140, 184, 198] private readonly selectedRoiStyle: { stroke?: { color: number[], width: number } fill?: { color: number[] } } = { stroke: { color: [...this.selectionColor, 1], width: 3 }, fill: { color: [...this.selectionColor, 0.2] } } constructor (props: SlideViewerProps) { super(props) console.info( `view slide "${this.props.slide.containerIdentifier}": `, this.props.slide ) const geometryTypeOptions = [ 'point', 'circle', 'box', 'polygon', 'line', 'freehandpolygon', 'freehandline' ] props.annotations.forEach((annotation: AnnotationSettings) => { const finding = new dcmjs.sr.coding.CodedConcept(annotation.finding) this.findingOptions.push(finding) const key = _buildKey(finding) if (annotation.geometryTypes !== undefined) { this.geometryTypeOptions[key] = annotation.geometryTypes } else { this.geometryTypeOptions[key] = geometryTypeOptions } this.evaluationOptions[key] = [] if (annotation.evaluations !== undefined) { annotation.evaluations.forEach(evaluation => { this.evaluationOptions[key].push({ name: new dcmjs.sr.coding.CodedConcept(evaluation.name), values: evaluation.values.map(value => { return new dcmjs.sr.coding.CodedConcept(value) }) }) }) } if (annotation.style != null) { this.roiStyles[key] = annotation.style } else { this.roiStyles[key] = this.defaultRoiStyle } }) this.componentSetup = this.componentSetup.bind(this) this.componentCleanup = this.componentCleanup.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.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.handleAnnotationSelection = this.handleAnnotationSelection.bind(this) this.handleAnnotationVisibilityChange = this.handleAnnotationVisibilityChange.bind(this) this.handleAnnotationGroupVisibilityChange = this.handleAnnotationGroupVisibilityChange.bind(this) this.handleAnnotationGroupStyleChange = this.handleAnnotationGroupStyleChange.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) console.info( 'instantiate viewers for slide of series ' + this.props.seriesInstanceUID ) const { volumeViewer, labelViewer } = _constructViewers({ client: this.props.client, slide: this.props.slide, preload: this.props.preload }) this.volumeViewer = volumeViewer this.labelViewer = labelViewer this.volumeViewportRef = React.createRef<HTMLDivElement>() this.labelViewportRef = React.createRef<HTMLDivElement>() /** * Deactivate all optical paths. Visibility will later, potentially using * available presentation state instances. */ const activeOpticalPathIdentifiers: string[] = [] const visibleOpticalPathIdentifiers: string[] = [] this.volumeViewer.getAllOpticalPaths().forEach(opticalPath => { this.volumeViewer.deactivateOpticalPath(opticalPath.identifier) }) this.state = { selectedRoiUIDs: [], visibleRoiUIDs: [], visibleSegmentUIDs: [], visibleMappingUIDs: [], visibleAnnotationGroupUIDs: [], visibleOpticalPathIdentifiers, activeOpticalPathIdentifiers, presentationStates: [], selectedFinding: undefined, selectedEvaluations: [], generatedReport: undefined, isLoading: false, isAnnotationModalVisible: false, isReportModalVisible: false, isRoiDrawingActive: false, isRoiTranslationActive: false, isRoiModificationActive: false, areRoisHidden: false, pixelDataStatistics: {}, defaultOpticalPathStyles: {} } } componentDidUpdate ( previousProps: SlideViewerProps, previousState: SlideViewerState ): void { /** Fetch data and update the viewports if the route has changed ( * i.e., if another series has been selected) or if the client has changed. */ if ( this.props.location.pathname !== previousProps.location.pathname || this.props.studyInstanceUID !== previousProps.studyInstanceUID || this.props.seriesInstanceUID !== previousProps.seriesInstanceUID || this.props.slide !== previousProps.slide || this.props.client !== previousProps.client ) { this.volumeViewer.cleanup() if (this.labelViewer != null) { this.labelViewer.cleanup() } const { volumeViewer, labelViewer } = _constructViewers({ client: this.props.client, slide: this.props.slide, preload: this.props.preload }) this.volumeViewer = volumeViewer this.labelViewer = labelViewer const activeOpticalPathIdentifiers: string[] = [] const visibleOpticalPathIdentifiers: string[] = [] this.volumeViewer.getAllOpticalPaths().forEach(opticalPath => { const identifier = opticalPath.identifier if (this.volumeViewer.isOpticalPathVisible(identifier)) { visibleOpticalPathIdentifiers.push(identifier) } if (this.volumeViewer.isOpticalPathActive(identifier)) { activeOpticalPathIdentifiers.push(identifier) } }) this.setState({ visibleRoiUIDs: [], visibleSegmentUIDs: [], visibleMappingUIDs: [], visibleAnnotationGroupUIDs: [], visibleOpticalPathIdentifiers, activeOpticalPathIdentifiers }) this.populateViewports() } } /** * Retrieve Presentation State instances that reference the any images of * the currently selected series. */ loadPresentationStates = (): void => { console.info('search for Presentation State instances') this.props.client.searchForInstances({ studyInstanceUID: this.props.studyInstanceUID, queryParams: { Modality: 'PR' } }).then((matchedInstances): void => { if (matchedInstances == null) { matchedInstances = [] } matchedInstances.forEach(i => { const { dataset } = dmv.metadata.formatMetadata(i) const instance = dataset as dmv.metadata.Instance console.info(`retrieve PR instance "${instance.SOPInstanceUID}"`) this.props.client.retrieveInstance({ studyInstanceUID: this.props.studyInstanceUID, seriesInstanceUID: instance.SeriesInstanceUID, sopInstanceUID: instance.SOPInstanceUID }).then((retrievedInstance): void => { const data = dcmjs.data.DicomMessage.readFile(retrievedInstance) const { dataset } = dmv.metadata.formatMetadata(data.dict) if (this.props.slide.areVolumeImagesMonochrome) { const presentationState = ( dataset as unknown as dmv.metadata.AdvancedBlendingPresentationState ) let doesMatch = false presentationState.AdvancedBlendingSequence.forEach(blendingItem => { doesMatch = this.props.slide.seriesInstanceUIDs.includes( blendingItem.SeriesInstanceUID ) } ) if (doesMatch) { console.info( 'include Advanced Blending Presentation State instance ' + `"${presentationState.SOPInstanceUID}"` ) if ( presentationState.SOPInstanceUID === this.props.selectedPresentationStateUID || ( this.props.selectedPresentationStateUID === undefined && this.state.presentationStates.length === 0 ) ) { this.setPresentationState(presentationState) this.setState(state => ({ presentationStates: [ ...state.presentationStates, presentationState ] })) } else { this.setState(state => ({ presentationStates: [ ...state.presentationStates, presentationState ] })) } } } else { console.info( `ignore presentation state "${instance.SOPInstanceUID}", ` + 'application of presentation states for color images ' + 'has not (yet) been implemented' ) } }).catch((error) => { // eslint-disable-next-line @typescript-eslint/no-floating-promises message.error('Presentation State could not be loaded') console.error( 'failed to load presentation state ' + `of SOP instance "${instance.SOPInstanceUID}" ` + `of series "${instance.SeriesInstanceUID}" ` + `of study "${this.props.studyInstanceUID}": `, error ) }) }) }).catch((error) => { // eslint-disable-next-line @typescript-eslint/no-floating-promises message.error('Presentation State could not be loaded') console.error(error) }) } setPresentationState = ( presentationState: dmv.metadata.AdvancedBlendingPresentationState ): void => { const opticalPaths = this.volumeViewer.getAllOpticalPaths() console.info( `apply Presentation State instance "${presentationState.SOPInstanceUID}"` ) const opticalPathStyles: { [opticalPathIdentifier: string]: { opacity: number paletteColorLookupTable: dmv.color.PaletteColorLookupTable limitValues?: number[] } | null } = {} opticalPaths.forEach(opticalPath => { // First, deactivate and hide all optical paths const identifier = opticalPath.identifier this.volumeViewer.hideOpticalPath(identifier) this.volumeViewer.deactivateOpticalPath(identifier) presentationState.AdvancedBlendingSequence.forEach(blendingItem => { blendingItem.ReferencedImageSequence.forEach(imageItem => { const index = opticalPath.sopInstanceUIDs.indexOf( imageItem.ReferencedSOPInstanceUID ) if (index >= 0) { const paletteColorLUT = new dmv.color.PaletteColorLookupTable({ uid: ( blendingItem.PaletteColorLookupTableUID != null ? blendingItem.PaletteColorLookupTableUID : '' ), redDescriptor: blendingItem.RedPaletteColorLookupTableDescriptor, greenDescriptor: blendingItem.GreenPaletteColorLookupTableDescriptor, blueDescriptor: blendingItem.BluePaletteColorLookupTableDescriptor, redData: ( (blendingItem.RedPaletteColorLookupTableData != null) ? new Uint16Array( blendingItem.RedPaletteColorLookupTableData ) : undefined ), greenData: ( (blendingItem.GreenPaletteColorLookupTableData != null) ? new Uint16Array( blendingItem.GreenPaletteColorLookupTableData ) : undefined ), blueData: ( (blendingItem.BluePaletteColorLookupTableData != null) ? new Uint16Array( blendingItem.BluePaletteColorLookupTableData ) : undefined ), redSegmentedData: ( (blendingItem.SegmentedRedPaletteColorLookupTableData != null) ? new Uint16Array( blendingItem.SegmentedRedPaletteColorLookupTableData ) : undefined ), greenSegmentedData: ( (blendingItem.SegmentedGreenPaletteColorLookupTableData != null) ? new Uint16Array( blendingItem.SegmentedGreenPaletteColorLookupTableData ) : undefined ), blueSegmentedData: ( (blendingItem.SegmentedBluePaletteColorLookupTableData != null) ? new Uint16Array( blendingItem.SegmentedBluePaletteColorLookupTableData ) : undefined ) }) let limitValues if (blendingItem.SoftcopyVOILUTSequence != null) { const voiLUTItem = blendingItem.SoftcopyVOILUTSequence[0] const windowCenter = voiLUTItem.WindowCenter const windowWidth = voiLUTItem.WindowWidth limitValues = [ windowCenter - windowWidth * 0.5, windowCenter + windowWidth * 0.5 ] } opticalPathStyles[identifier] = { opacity: 1, paletteColorLookupTable: paletteColorLUT, limitValues: limitValues } } }) }) }) const selectedOpticalPathIdentifiers: string[] = [] Object.keys(opticalPathStyles).forEach(identifier => { const styleOptions = opticalPathStyles[identifier] if (styleOptions != null) { this.volumeViewer.setOpticalPathStyle(identifier, styleOptions) this.volumeViewer.activateOpticalPath(identifier) this.volumeViewer.showOpticalPath(identifier) selectedOpticalPathIdentifiers.push(identifier) } else { this.volumeViewer.hideOpticalPath(identifier) this.volumeViewer.deactivateOpticalPath(identifier) } }) this.setState(state => ({ activeOpticalPathIdentifiers: selectedOpticalPathIdentifiers, visibleOpticalPathIdentifiers: selectedOpticalPathIdentifiers, selectedPresentationStateUID: presentationState.SOPInstanceUID })) } getRoiStyle = (key: string): dmv.viewer.ROIStyleOptions => { if (this.roiStyles[key] !== undefined) { return this.roiStyles[key] } return this.defaultRoiStyle } /** * Retrieve Structured Report instances that contain regions of interests * with 3D spatial coordinates defined in the same frame of reference as the * currently selected series and add them to the VOLUME image viewer. */ addAnnotations = (): void => { console.info('search for Comprehensive 3D SR instances') this.props.client.searchForInstances({ studyInstanceUID: this.props.studyInstanceUID, queryParams: { Modality: 'SR' } }).then((matchedInstances): void => { if (matchedInstances == null) { matchedInstances = [] } matchedInstances.forEach(i => { const { dataset } = dmv.metadata.formatMetadata(i) const instance = dataset as dmv.metadata.Instance if (instance.SOPClassUID === SOPClassUIDs.COMPREHENSIVE_3D_SR) { console.info(`retrieve SR instance "${instance.SOPInstanceUID}"`) this.props.client.retrieveInstance({ studyInstanceUID: this.props.studyInstanceUID, seriesInstanceUID: instance.SeriesInstanceUID, sopInstanceUID: instance.SOPInstanceUID }).then((retrievedInstance): void => { const data = dcmjs.data.DicomMessage.readFile(retrievedInstance) const { dataset } = dmv.metadata.formatMetadata(data.dict) const report = dataset as unknown as dmv.metadata.Comprehensive3DSR /* * Perform a couple of checks to ensure the document content of the * report fullfils the requirements of the application. */ if (!_implementsTID1500(report)) { console.debug( `ignore SR document "${report.SOPInstanceUID}" ` + 'because it is not structured according to template ' + 'TID 1500 "MeasurementReport"' ) return } if (!_describesSpecimenSubject(report)) { console.debug( `ignore SR document "${report.SOPInstanceUID}" ` + 'because it does not describe a specimen subject' ) return } if (!_containsROIAnnotations(report)) { console.debug( `ignore SR document "${report.SOPInstanceUID}" ` + 'because it does not contain any suitable ROI annotations' ) return } const content = new MeasurementReport(report) content.ROIs.forEach(roi => { console.info(`add ROI "${roi.uid}"`) const scoord3d = roi.scoord3d const image = this.props.slide.volumeImages[0] if (scoord3d.frameOfReferenceUID === image.FrameOfReferenceUID) { /* * ROIs may get assigned new UIDs upon re-rendering of the * page and we need to ensure that we don't add them twice. * The same ROI may be stored in multiple SR documents and * we don't want them to show up twice. * TODO: We should probably either "merge" measurements and * quantitative evaluations or pick the ROI from the "best" * available report (COMPLETE and VERIFIED). */ const doesROIExist = this.volumeViewer.getAllROIs().some( (otherROI: dmv.roi.ROI): boolean => { return _areROIsEqual(otherROI, roi) } ) if (!doesROIExist) { try { // Add ROI without style such that it won't be visible. this.volumeViewer.addROI(roi, {}) } catch { console.error(`could not add ROI "${roi.uid}"`) } } else { console.debug(`skip already existing ROI "${roi.uid}"`) } } else { console.debug( `skip ROI "${roi.uid}" ` + `of SR document "${report.SOPInstanceUID}"` + 'because it is defined in another frame of reference' ) } }) }).catch((error) => { // eslint-disable-next-line @typescript-eslint/no-floating-promises message.error('Annotations could not be loaded') console.error( 'failed to load ROIs ' + `of SOP instance "${instance.SOPInstanceUID}" ` + `of series "${instance.SeriesInstanceUID}" ` + `of study "${this.props.studyInstanceUID}": `, error ) }) /* * React is not aware of the fact that ROIs have been added via the * viewer (the viewport is a ref object) and won't show the * annotations in the user interface unless an update is forced. */ this.forceUpdate() } }) }).catch((error) => { // eslint-disable-next-line @typescript-eslint/no-floating-promises message.error('Annotations could not be loaded') console.error(error) }) } /** * Retrieve Microscopy Bulk Simple Annotations instances that contain * annotation groups defined in the same frame of reference as the currently * selected series and add them to the VOLUME image viewer. */ addAnnotationGroups = (): void => { console.info('search for Microscopy Bulk Simple Annotations instances') this.props.client.searchForSeries({ studyInstanceUID: this.props.studyInstanceUID, queryParams: { Modality: 'ANN' } }).then((matchedSeries): void => { if (matchedSeries == null) { matchedSeries = [] } matchedSeries.forEach(s => { const { dataset } = dmv.metadata.formatMetadata(s) const series = dataset as dmv.metadata.Series this.props.client.retrieveSeriesMetadata({ studyInstanceUID: this.props.studyInstanceUID, seriesInstanceUID: series.SeriesInstanceUID }).then((retrievedMetadata): void => { let annotations: dmv.metadata.MicroscopyBulkSimpleAnnotations[] annotations = retrievedMetadata.map(metadata => { return new dmv.metadata.MicroscopyBulkSimpleAnnotations({ metadata }) }) annotations = annotations.filter(ann => { const refImage = this.props.slide.volumeImages[0] return ( ann.FrameOfReferenceUID === refImage.FrameOfReferenceUID && ann.ContainerIdentifier === refImage.ContainerIdentifier ) }) annotations.forEach(ann => { try { this.volumeViewer.addAnnotationGroups(ann) } catch (error: any) { // eslint-disable-next-line @typescript-eslint/no-floating-promises message.error( 'Microscopy Bulk Simple Annotations cannot be displayed.' ) // eslint-disable-next-line @typescript-eslint/no-floating-promises console.error('failed to add annotation groups: ', error) } }) /* * React is not aware of the fact that annotation groups have been * added via the viewer (the underlying HTML viewport element is a * ref object) and won't show the annotation groups in the user * interface unless an update is forced. */ this.forceUpdate() }).catch((error: any) => { // eslint-disable-next-line @typescript-eslint/no-floating-promises message.error( 'Retrieval of metadata of Microscopy Bulk Simple Annotations ' + 'instances failed.' ) console.error( 'failed to retrieve metadata of ' + 'Microscopy Bulk Simple Annotations instances: ', error ) }) }) }).catch((error: any) => { // eslint-disable-next-line @typescript-eslint/no-floating-promises message.error( 'Search for Microscopy Bulk Simple Annotations instances failed.' ) console.error( 'failed to search for Microscopy Bulk Simple Annotations instances: ', error ) }) } /** * Retrieve Segmentation instances that contain segments defined in the same * frame of reference as the currently selected series and add them to the * VOLUME image viewer. */ addSegmentations = (): void => { console.info('search for Segmentation instances') this.props.client.searchForSeries({ studyInstanceUID: this.props.studyInstanceUID, queryParams: { Modality: 'SEG' } }).then((matchedSeries): void => { if (matchedSeries == null) { matchedSeries = [] } matchedSeries.forEach((s, i) => { const { dataset } = dmv.metadata.formatMetadata(s) const series = dataset as dmv.metadata.Series this.props.client.retrieveSeriesMetadata({ studyInstanceUID: this.props.studyInstanceUID, seriesInstanceUID: series.SeriesInstanceUID }).then((retrievedMetadata): void => { const segmentations: dmv.metadata.Segmentation[] = [] retrievedMetadata.forEach(metadata => { const seg = new dmv.metadata.Segmentation({ metadata }) const refImage = this.props.slide.volumeImages[0] if ( seg.FrameOfReferenceUID === refImage.FrameOfReferenceUID && seg.ContainerIdentifier === refImage.ContainerIdentifier ) { segmentations.push(seg) } }) if (segmentations.length > 0) { try { this.volumeViewer.addSegments(segmentations) } catch (error: any) { // eslint-disable-next-line @typescript-eslint/no-floating-promises message.error('Segmentations cannot be displayed') console.error('failed to add segments: ', error) } /* * React is not aware of the fact that segments have been added via * the viewer (the underlying HTML viewport element is a ref object) * and won't show the segments in the user interface unless an update * is forced. */ this.forceUpdate() } }).catch((error: any) => { // eslint-disable-next-line @typescript-eslint/no-floating-promises message.error( 'Retrieval of metadata of Segmentation instances failed.' ) console.error( 'failed to retrieve metadata of Segmentation instances: ', error ) }) }) }).catch((error: any) => { // eslint-disable-next-line @typescript-eslint/no-floating-promises message.error('Search for Segmentation instances failed.') console.error('failed to search for Segmentation instances: ', error) }) } /** * Retrieve Parametric Map instances that contain mappings defined in the same * frame of reference as the currently selected series and add them to the * VOLUME image viewer. */ addParametricMaps = (): void => { console.info('search for Parametric Map instances') this.props.client.searchForSeries({ studyInstanceUID: this.props.studyInstanceUID, queryParams: { Modality: 'OT' } }).then((matchedSeries): void => { if (matchedSeries == null) { matchedSeries = [] } matchedSeries.forEach(s => { const { dataset } = dmv.metadata.formatMetadata(s) const series = dataset as dmv.metadata.Series this.props.client.retrieveSeriesMetadata({ studyInstanceUID: this.props.studyInstanceUID, seriesInstanceUID: series.SeriesInstanceUID }).then((retrievedMetadata): void => { const parametricMaps: dmv.metadata.ParametricMap[] = [] retrievedMetadata.forEach(metadata => { const pm = new dmv.metadata.ParametricMap({ metadata }) const refImage = this.props.slide.volumeImages[0] if ( pm.FrameOfReferenceUID === refImage.FrameOfReferenceUID && pm.ContainerIdentifier === refImage.ContainerIdentifier ) { parametricMaps.push(pm) } }) if (parametricMaps.length > 0) { try { this.volumeViewer.addParameterMappings(parametricMaps) } catch (error: any) { // eslint-disable-next-line @typescript-eslint/no-floating-promises message.error('Parametric Map cannot be displayed') console.error('failed to add mappings: ', error) } /* * React is not aware of the fact that mappings have been added via * the viewer (the underlying HTML viewport element is a ref object) * and won't show the mappings in the user interface unless an update * is forced. */ this.forceUpdate() } }).catch((error: any) => { // eslint-disable-next-line @typescript-eslint/no-floating-promises message.error( 'Retrieval of metadata of Parametric Map instances failed.' ) console.error( 'failed to retrieve metadata of Parametric Map instances: ', error ) }) }) }).catch((error: any) => { // eslint-disable-next-line @typescript-eslint/no-floating-promises message.error('Search for Parametric Map instances failed.') console.error('failed to search for Parametric Map instances: ', error) }) } /** * Populate viewports of the VOLUME and LABEL image viewers. */ populateViewports = (): void => { console.info('populate viewports...') this.setState({ isLoading: true }) if (this.volumeViewportRef.current != null) { this.volumeViewportRef.current.innerHTML = '' this.volumeViewer.render({ container: this.volumeViewportRef.current }) } if ( this.labelViewportRef.current != null && this.labelViewer != null ) { this.labelViewportRef.current.innerHTML = '' 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() this.addAnnotations() this.addAnnotationGroups() this.addSegmentations() this.addParametricMaps() } onRoiModified = (event: CustomEventInit): void => { // Update state to trigger rendering this.setState(state => ({ visibleRoiUIDs: [...state.visibleRoiUIDs] })) } onRoiDrawn = (event: CustomEventInit): void => { const roi = event.detail.payload as dmv.roi.ROI const selectedFinding = this.state.selectedFinding const selectedEvaluations = this.state.selectedEvaluations if (roi !== undefined && selectedFinding !== undefined) { console.debug(`add ROI "${roi.uid}"`) const findingItem = new dcmjs.sr.valueTypes.CodeContentItem({ name: new dcmjs.sr.coding.CodedConcept({ value: '121071', meaning: 'Finding', schemeDesignator: 'DCM' }), value: selectedFinding, relationshipType: 'CONTAINS' }) roi.addEvaluation(findingItem) selectedEvaluations.forEach((evaluation: Evaluation) => { const item = new dcmjs.sr.valueTypes.CodeContentItem({ name: evaluation.name, value: evaluation.value, relationshipType: 'CONTAINS' }) roi.addEvaluation(item) }) const key = _buildKey(selectedFinding) const style = this.getRoiStyle(key) this.volumeViewer.addROI(roi, style) this.setState(state => ({ visibleRoiUIDs: [...state.visibleRoiUIDs, roi.uid] })) } else { console.debug(`could not add ROI "${roi.uid}"`) } } onRoiSelected = (event: CustomEventInit): void => { const selectedRoi = event.detail.payload as dmv.roi.ROI if (selectedRoi !== null) { console.debug(`selected ROI "${selectedRoi.uid}"`) this.volumeViewer.setROIStyle(selectedRoi.uid, this.selectedRoiStyle) const key = _getRoiKey(selectedRoi) this.volumeViewer.getAllROIs().forEach((roi) => { if (roi.uid !== selectedRoi.uid) { this.volumeViewer.setROIStyle(roi.uid, this.getRoiStyle(key)) } }) this.setState({ selectedRoiUIDs: [selectedRoi.uid] }) } else { this.setState({ selectedRoiUIDs: [] }) } } onLoadingStarted = (event: CustomEventInit): void => { this.setState({ isLoading: true }) } onLoadingEnded = (event: CustomEventInit): void => { this.setState({ isLoading: false }) } onFrameLoadingEnded = (event: CustomEventInit): void => { const frameInfo = event.detail.payload if ( frameInfo.sopClassUID === SOPClassUIDs.VL_WHOLE_SLIDE_MICROSCOPY_IMAGE && this.props.slide.areVolumeImagesMonochrome ) { const opticalPathIdentifier = frameInfo.channelIdentifier if (!(opticalPathIdentifier in this.state.pixelDataStatistics)) { /* * There are limits on the number of arguments Math.min and Math.max * functions can accept. Therefore, we compute values in smaller chunks. */ const size = 2 ** 16 const chunks = Math.ceil(frameInfo.pixelArray.length / size) let offset = 0 let min = 0 let max = 0 for (let i = 0; i < chunks; i++) { offset = i * size const pixels = frameInfo.pixelArray.slice(offset, size) min = Math.min(min, ...pixels) max = Math.max(max, ...pixels) } this.setState(state => { const stats = state.pixelDataStatistics if (stats[opticalPathIdentifier] != null) { stats[opticalPathIdentifier] = { min: Math.min(stats[opticalPathIdentifier].min, min), max: Math.max(stats[opticalPathIdentifier].max, max), numFramesSampled: stats[opticalPathIdentifier].numFramesSampled + 1 } } else { stats[opticalPathIdentifier] = { min: min, max: max, numFramesSampled: 1 } } return state }) } } } onRoiRemoved = (event: CustomEventInit): void => { const roi = event.detail.payload as dmv.roi.ROI console.debug(`removed ROI "${roi.uid}"`) } componentCleanup (): void { document.body.removeEventListener( 'dicommicroscopyviewer_roi_drawn', this.onRoiDrawn ) document.body.removeEventListener( 'dicommicroscopyviewer_roi_selected', this.onRoiSelected ) document.body.removeEventListener( 'dicommicroscopyviewer_roi_removed', this.onRoiRemoved ) document.body.removeEventListener( 'dicommicroscopyviewer_roi_modified', this.onRoiModified ) document.body.removeEventListener( 'dicommicroscopyviewer_loading_started', this.onLoadingStarted ) document.body.removeEventListener( 'dicommicroscopyviewer_loading_ended', this.onLoadingEnded ) document.body.removeEventListener( 'dicommicroscopyviewer_frame_loading_ended', this.onFrameLoadingEnded ) this.volumeViewer.cleanup() if (this.labelViewer != null) { this.labelViewer.cleanup() } /* * FIXME: React appears to not clean the content of referenced * HTMLDivElement objects when the page is reloaded. As a consequence, * optical paths and other display items cannot be toggled or updated after * a manual page reload. I have tried using ref callbacks and passing the * ref objects from the parent component via the props. Both didn't work * either. */ } componentWillUnmount (): void { window.removeEventListener('beforeunload', this.componentCleanup) this.componentCleanup() } componentSetup (): void { document.body.addEventListener( 'dicommicroscopyviewer_roi_drawn', this.onRoiDrawn ) document.body.addEventListener( 'dicommicroscopyviewer_roi_selected', this.onRoiSelected ) document.body.addEventListener( 'dicommicroscopyviewer_roi_removed', this.onRoiRemoved ) document.body.addEventListener( 'dicommicroscopyviewer_roi_modified', this.onRoiModified ) document.body.addEventListener( 'dicommicroscopyviewer_loading_started', this.onLoadingStarted ) document.body.addEventListener( 'dicommicroscopyviewer_loading_ended', this.onLoadingEnded ) document.body.addEventListener( 'dicommicroscopyviewer_frame_loading_ended', this.onFrameLoadingEnded ) const onKeyUp = ( event: KeyboardEvent ): void => { if (event.key === 'Escape') { if (this.state.isRoiDrawingActive) { console.info('deactivate drawing of ROIs') this.volumeViewer.deactivateDrawInteraction() this.volumeViewer.activateSelectInteraction({}) this.setState({ isRoiDrawingActive: false }) } else if (this.state.isRoiModificationActive) { console.info('deactivate modification of ROIs') this.volumeViewer.deactivateModifyInteraction() this.volumeViewer.activateSelectInteraction({}) this.setState({ isRoiModificationActive: false }) } else if (this.state.isRoiTranslationActive) { console.info('deactivate modification of ROIs') this.volumeViewer.deactivateTranslateInteraction() this.volumeViewer.activateSelectInteraction({}) this.setState({ isRoiTranslationActive: false }) } } else if (event.key === 'd') { this.handleRoiDrawing() console.info('activate drawing of ROIs') this.setState({ isAnnotationModalVisible: true, isRoiDrawingActive: true, isRoiModificationActive: false, isRoiTranslationActive: false }) this.volumeViewer.deactivateSelectInteraction() this.volumeViewer.deactivateSnapInteraction() this.volumeViewer.deactivateTranslateInteraction() this.volumeViewer.deactivateModifyInteraction() } else if (event.key === 'm') { this.handleRoiModification() } else if (event.key === 't') { this.handleRoiTranslation() } else if (event.key === 'r') { this.handleRoiRemoval() } else if (event.key === 'v') { this.handleRoiVisibilityChange() } else if (event.key === 's') { this.handleReportGeneration() } } document.body.addEventListener( 'keyup', onKeyUp ) } componentDidMount (): void { window.addEventListener('beforeunload', this.componentCleanup) this.componentSetup() this.populateViewports() if (!this.props.slide.areVolumeImagesMonochrome) { let hasICCProfile = false const image = this.props.slide.volumeImages[0] const metadataItem = image.OpticalPathSequence[0] if (metadataItem.ICCProfile == null) { if ('OpticalPathSequence' in image.bulkdataReferences) { // @ts-expect-error const bulkdataItem = image.bulkdataReferences.OpticalPathSequence[0] if ('ICCProfile' in bulkdataItem) { hasICCProfile = true } } } else { hasICCProfile = true } if (!hasICCProfile) { // eslint-disable-next-line @typescript-eslint/no-floating-promises message.warning('No ICC Profile was found for color images') } } } /** * Handler that gets called when a finding has been selected for annotation. * * @param value - Code value of the coded finding that got selected * @param option - Option that got selected */ handleAnnotationFindingSelection ( value: string, option: any ): void { this.findingOptions.forEach(finding => { if (finding.CodeValue === value) { console.info(`selected finding "${finding.CodeMeaning}"`) this.setState({ selectedFinding: finding, selectedEvaluations: [] }) } }) } /** * Handler that gets called when a geometry type has been selected for * annotation. * * @param value - Code value of the coded finding that got selected * @param option - Option that got selected */ handleAnnotationGeometryTypeSelection (value: string, option: any): void { this.setState({ selectedGeometryType: value }) } /** * Handler that gets called when measurements have been selected for * annotation. */ handleAnnotationMeasurementActivation (event: any): void { const active: boolean = event.target.checked if (active) { this.setState({ selectedMarkup: 'measurement' }) } else { this.setState({ selectedMarkup: undefined }) } } /** * Handler that gets called when an evaluation has been selected for an * annotation. * * @param value - Code value of the coded evaluation that got selected * @param option - Option that got selected */ handleAnnotationEvaluationSelection ( value: string, option: any ): void { const selectedFinding = this.state.selectedFinding if (selectedFinding !== undefined) { const key = _buildKey(selectedFinding) const name = option.label this.evaluationOptions[key].forEach(evaluation => { if ( evaluation.name.CodeValue === name.CodeValue && evaluation.name.CodingSchemeDesignator === name.CodingSchemeDesignator ) { evaluation.values.forEach(code => { if (code.CodeValue === value) { const filteredEvaluations = this.state.selectedEvaluations.filter( (item: Evaluation) => item.name !== evaluation.name ) this.setState({ selectedEvaluations: [ ...filteredEvaluations, { name: name, value: code } ] }) } }) } }) } } /** * Handler that gets called when an evaluation has been cleared for an * annotation. */ handleAnnotationEvaluationClearance (): void { this.setState({ selectedEvaluations: [] }) } /** * Handler that gets called when annotation configuration has been completed. */ handleAnnotationConfigurationCompletion (): void { console.debug('complete annotation configuration') const finding = this.state.selectedFinding const geometryType = this.state.selectedGeometryType const markup = this.state.selectedMarkup if (geometryType !== undefined && finding !== undefined) { this.volumeViewer.activateDrawInteraction({ geometryType, markup }) this.setState({ isAnnotationModalVisible: false, isRoiDrawingActive: true }) } else { console.error('could not complete annotation configuration') } } /** * Handler that gets called when annotation configuration has been cancelled. */ handleAnnotationConfigurationCancellation (): void { console.debug('cancel annotation configuration') this.setState({ isAnnotationModalVisible: false, isRoiDrawingActive: false }) } /** * Handler that gets called when a report should be generated for the current * set of annotations. */ handleReportGeneration (): void { console.info('save ROIs') const rois = this.volumeViewer.getAllROIs() const opticalPaths = this.volumeViewer.getAllOpticalPaths() const metadata = this.volumeViewer.getOpticalPathMetadata( opticalPaths[0].identifier ) // Metadata should be sorted such that the image with the highest // resolution is the last item in the array. const refImage = metadata[metadata.length - 1] // We assume that there is only one specimen (tissue section) per // ontainer (slide). Only the tissue section is tracked with a unique // identifier, even if the section may be composed of different biological // samples. if (refImage.SpecimenDescriptionSequence.length > 1) { console.error('more than one specimen has been described for the slide') } const refSpecimen = refImage.SpecimenDescriptionSequence[0] console.debug('create Observation Context') var observer if (this.props.user !== undefined) { observer = new dcmjs.sr.templates.PersonObserverIdentifyingAttributes({ name: this.props.user.name, loginName: this.props.user.email }) } else { console.warn('no user information available') observer = new dcmjs.sr.templates.PersonObserverIdentifyingAttributes({ name: 'ANONYMOUS' }) } const observationContext = new dcmjs.sr.templates.ObservationContext({ observerPersonContext: new dcmjs.sr.templates.ObserverContext({ observerType: new dcmjs.sr.coding.CodedConcept({ value: '121006', schemeDesignator: 'DCM', meaning: 'Person' }), observerIdentifyingAttributes: observer }), observerDeviceContext: new dcmjs.sr.templates.ObserverContext({ observerType: new dcmjs.sr.coding.CodedConcept({ value: '121007', schemeDesignator: 'DCM', meaning: 'Device' }), observerIdentifyingAttributes: new dcmjs.sr.templates.DeviceObserverIdentifyingAttributes({ uid: this.props.app.uid, manufacturerName: 'MGH Computational Pathology', modelName: this.props.app.name }) }), subjectContext: new dcmjs.sr.templates.SubjectContext({ subjectClass: new dcmjs.sr.coding.CodedConcept({ value: '121027', schemeDesignator: 'DCM', meaning: 'Specimen' }), subjectClassSpecificContext: new dcmjs.sr.templates.SubjectContextSpecimen({ uid: refSpecimen.SpecimenUID, identifier: refSpecimen.SpecimenIdentifier, containerIdentifier: refImage.ContainerIdentifier }) }) }) console.debug('encode Imaging Measurements') const imagingMeasurements: dcmjs.sr.valueTypes.ContainerContentItem[] = [] for (let i = 0; i < rois.length; i++) { const roi = rois[i] if (!this.state.visibleRoiUIDs.includes(roi.uid as never)) { continue } let findingType = roi.evaluations.find( (item: dcmjs.sr.valueTypes.ContentItem) => { return item.ConceptNameCodeSequence[0].CodeValue === '121071' } ) if (findingType === undefined) { throw new Error(`No finding type was specified for ROI "${roi.uid}"`) } findingType = findingType as dcmjs.sr.valueTypes.CodeContentItem const group = new dcmjs.sr.templates.PlanarROIMeasurementsAndQualitativeEvaluations({ trackingIdentifier: new dcmjs.sr.templates.TrackingIdentifier({ uid: roi.properties.trackingUID ?? roi.uid, identifier: `ROI #${i + 1}` }), referencedRegion: new dcmjs.sr.contentItems.ImageRegion3D({ graphicType: roi.scoord3d.graphicType, graphicData: roi.scoord3d.graphicData, frameOfReferenceUID: roi.scoord3d.frameOfReferenceUID }), findingType: new dcmjs.sr.coding.CodedConcept({ value: findingType.ConceptCodeSequence[0].CodeValue, schemeDesignator: findingType.ConceptCodeSequence[0].CodingSchemeDesignator, meaning: findingType.ConceptCodeSequence[0].CodeMeaning }), qualitativeEvaluations: roi.evaluations.filter( (item: dcmjs.sr.valueTypes.ContentItem) => { return item.ConceptNameCodeSequence[0].CodeValue !== '121071' } ), measurements: roi.measurements }) const measurements = group as dcmjs.sr.valueTypes.ContainerContentItem[] measurements[0].ContentTemplateSequence = [{ MappingResource: 'DCMR', TemplateIdentifier: '1410' }] imagingMeasurements.push(...measurements) } console.debug('create Measurement Report document content') const measurementReport = new dcmjs.sr.templates.MeasurementReport({ languageOfContentItemAndDescendants: new dcmjs.sr.templates.LanguageOfContentItemAndDescendants({}), observationContext: observationContext, procedureReported: new dcmjs.sr.coding.CodedConcept({ value: '112703', schemeDesignator: 'DCM', meaning: 'Whole Slide Imaging' }), imagingMeasurements: imagingMeasurements }) console.info('create Comprehensive 3D SR document') const dataset = new dcmjs.sr.documents.Comprehensive3DSR({ content: measurementReport[0], evidence: [refImage], seriesInstanceUID: dcmjs.data.DicomMetaDictionary.uid(), seriesNumber: 1, seriesDescription: 'Annotation', sopInstanceUID: dcmjs.data.DicomMetaDictionary.uid(), instanceNumber: 1, manufacturer: 'MGH Computational Pathology', previousVersions: undefined // TODO }) this.setState({ isReportModalVisible: true, generatedReport: dataset as dmv.metadata.Comprehensive3DSR }) } /** * Handler that gets called when a report should be verified. The current * 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) { var 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() this.props.client.storeInstances({ datasets: [buffer] }).then( (response: any) => message.info('Annotations were saved.') ).catch((error: any) => { // eslint-disable-next-line @typescript-eslint/no-floating-promises message.error('Annotations could not be saved') console.error(error) }) } this.setState({ isReportModalVisible: false, generatedReport: undefined }) } /** * Handler that gets called when report generation has been cancelled. */ handleReportCancellation (): void { this.setState({ isReportModalVisible: false, generatedReport: undefined }) } /** * Handler that gets called when an annotation has been selected from the * current list of annotations. */ handleAnnotationSelection ({ roiUID }: { roiUID: string }): void { console.log(`selected ROI ${roiUID}`) this.setState({ selectedRoiUIDs: [roiUID] }) this.volumeViewer.getAllROIs().forEach((roi) => { var style = {} if (roi.uid === roiUID) { style = this.selectedRoiStyle this.setState(state => ({ visibleRoiUIDs: [...state.visibleRoiUIDs, roiUID] })) } else { if (this.state.visibleRoiUIDs.includes(roi.uid as never)) { const key = _getRoiKey(roi) style = this.getRoiStyle(key) } } this.volumeViewer.setROIStyle(roi.uid, style) }) } /** * Handle toggling of annotation visibility, i.e., whether a given * annotation should be either displayed or hidden by the viewer. */ handleAnnotationVisibilityChange ({ roiUID, isVisible }: { roiUID: string isVisible: boolean }): void { if (isVisible) { console.info(`show ROI ${roiUID}`) const roi = this.volumeViewer.getROI(roiUID) const key = _getRoiKey(roi) this.volumeViewer.setROIStyle(roi.uid, this.getRoiStyle(key)) this.setState(state => { if (!state.visibleRoiUIDs.includes(roiUID)) { return { visibleRoiUIDs: [...state.visibleRoiUIDs, roiUID] } } else { return { visibleRoiUIDs: state.visibleRoiUIDs } } }) } else { console.info(`hide ROI ${roiUID}`) this.setState(state => ({ visibleRoiUIDs: state.visibleRoiUIDs.filter(uid => uid !== roiUID), selectedRoiUIDs: state.selectedRoiUIDs.filter(uid => uid !== roiUID) })) this.volumeViewer.setROIStyle(roiUID, {}) } } /** * 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 }: { annotationGroupUID: string isVisible: boolean }): void { console.log(`change visibility of annotation group ${annotationGroupUID}`) if (isVisible) { console.info(`show annotation group ${annotationGroupUID}`) this.volumeViewer.showAnnotationGroup(annotationGroupUID) this.setState(state => ({ visibleAnnotationGroupUIDs: state.visibleAnnotationGroupUIDs.concat( annotationGroupUID ) })) } else { console.info(`hide annotation group ${annotationGroupUID}`) this.volumeViewer.hideAnnotationGroup(annotationGroupUID) this.setState(state => ({ visibleAnnotationGroupUIDs: state.visibleAnnotationGroupUIDs.filter( uid => uid !== annotationGroupUID ) })) } } /** * Handle change of annotation group style. */ handleAnnotationGroupStyleChange ({ annotationGroupUID, styleOptions }: { annotationGroupUID: string styleOptions: { opacity?: number } }): void { console.log(`change style of annotation group ${annotationGroupUID}`) this.volumeViewer.setAnnotationGroupStyle(annotationGroupUID, styleOptions) } /** * Handle toggling of segment visibility, i.e., whether a given * segment should be either displayed or hidden by the viewer. */ handleSegmentVisibilityChange ({ segmentUID, isVisible }: { segmentUID: string isVisible: boolean }): void { console.log(`change visibility of segment ${segmentUID}`) if (isVisible) { console.info(`show segment ${segmentUID}`) this.volumeViewer.showSegment(segmentUID) this.setState(state => ({ visibleSegmentUIDs: state.visibleSegmentUIDs.concat(segmentUID) })) } else { console.info(`hide segment ${segmentUID}`) this.volumeViewer.hideSegment(segmentUID) this.setState(state => ({ visibleSegmentUIDs: state.visibleSegmentUIDs.filter(uid => { return uid !== segmentUID }) })) } } /** * Handle change of segment style. */ handleSegmentStyleChange ({ segmentUID, styleOptions }: { segmentUID: string styleOptions: { opacity?: number } }): void { console.log(`change style of segment ${segmentUID}`) this.volumeViewer.setSegmentStyle(segmentUID, styleOptions) } /** * Handle toggling of mapping visibility, i.e., whether a given * mapping should be either displayed or hidden by the viewer. */ handleMappingVisibilityChange ({ mappingUID, isVisible }: { mappingUID: string isVisible: boolean }): void { console.log(`change visibility of mapping ${mappingUID}`) if (isVisible) { console.info(`show mapping ${mappingUID}`) this.volumeViewer.showParameterMapping(mappingUID) this.setState(state => ({ visibleMappingUIDs: state.visibleMappingUIDs.concat(mappingUID) })) } else { console.info(`hide mapping ${mappingUID}`) this.volumeViewer.hideParameterMapping(mappingUID) this.setState(state => ({ visibleMappingUIDs: state.visibleMappingUIDs.filter(uid => { return uid !== mappingUID }) })) } } /** * Handle change of mapping style. */ handleMappingStyleChange ({ mappingUID, styleOptions }: { mappingUID: string styleOptions: { opacity?: number } }): void { console.log(`change style of mapping ${mappingUID}`) this.volumeViewer.setParameterMappingStyle(mappingUID, styleOptions) } /** * 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 }: { opticalPathIdentifier: string isVisible: boolean }): void { console.log(`change visibility of optical path ${opticalPathIdentifier}`) if (isVisible) { console.info(`show optical path ${opticalPathIdentifier}`) this.volumeViewer.showOpticalPath(opticalPathIdentifier) this.setState(state => ({ visibleOpticalPathIdentifiers: state.visibleOpticalPathIdentifiers.concat(opticalPathIdentifier) })) } else { console.info(`hide optical path ${opticalPathIdentifier}`) this.volumeViewer.hideOpticalPath(opticalPathIdentifier) this.setState(state => ({ visibleOpticalPathIdentifiers: state.visibleOpticalPathIdentifiers.filter( identifier => identifier !== opticalPathIdentifier ) })) } } /** * Handle change of optical path style. */ handleOpticalPathStyleChange ({ opticalPathIdentifier, styleOptions }: { opticalPathIdentifier: string styleOptions: { opacity?: number color?: number[] limitValues?: number[] } }): void { console.log(`change style of optical path ${opticalPathIdentifier}`) this.volumeViewer.setOpticalPathStyle(opticalPathIdentifier, styleOptions) } /** * 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 }: { opticalPathIdentifier: string isActive: boolean }): void { console.log(`change activity of optical path ${opticalPathIdentifier}`) if (isActive) { console.info(`activate optical path ${opticalPathIdentifier}`) this.volumeViewer.activateOpticalPath(opticalPathIdentifier) this.setState(state => ({ activeOpticalPathIdentifiers: state.activeOpticalPathIdentifiers.concat(opticalPathIdentifier) })) } else { console.info(`deactivate optical path ${opticalPathIdentifier}`) this.volumeViewer.deactivateOpticalPath(opticalPathIdentifier) this.setState(state => ({ activeOpticalPathIdentifiers: state.activeOpticalPathIdentifiers.filter( identifier => identifier !== opticalPathIdentifier ) })) } } setDefaultPresentationState (): void { const opticalPaths = this.volumeViewer.getAllOpticalPaths() opticalPaths.sort((a, b) => a.identifier - b.identifier) const visibleOpticalPathIdentifiers: string[] = [] const defaultOpticalPathStyles: { [opticalPathIdentifier: string]: { color?: number[] paletteColorLookupTable?: dmv.color.PaletteColorLookupTable opacity?: number limitValues?: number[] } } = this.state.defaultOpticalPathStyles opticalPaths.forEach((item: dmv.opticalPath.OpticalPath) => { const identifier = item.identifier this.volumeViewer.hideOpticalPath(identifier) this.volumeViewer.deactivateOpticalPath(identifier) /* * Reset the style of the optical path to its default if it has * previously been changed. */ const stats = this.state.pixelDataStatistics[item.identifier] let limitValues if (stats != null) { limitValues = [stats.min, stats.max] } if (identifier in defaultOpticalPathStyles) { this.volumeViewer.setOpticalPathStyle( identifier, { color: [255, 255, 255], limitValues, opacity: 1 } ) } if (item.isMonochromatic) { /* * 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) { visibleOpticalPathIdentifiers.push(identifier) } } else { /* Color images will always be displayed by default. */ visibleOpticalPathIdentifiers.push(identifier) } }) /* * If no optical paths have been selected for visualization so far, select * first 3 optical paths and set a default value of interest (VOI) window * (using pre-computed pixel data statistics) and a default color. */ if (visibleOpticalPathIdentifiers.length === 0) { const defaultColors = [ [0, 0, 255], [0, 255, 0], [255, 0, 0] ] opticalPaths.forEach((item: dmv.opticalPath.OpticalPath) => { const identifier = item.identifier if (item.isMonochromatic) { const numVisible = visibleOpticalPathIdentifiers.length if (numVisible < 3) { const style = { ...this.volumeViewer.getOpticalPathStyle(identifier) // copy! } if (!(identifier in defaultOpticalPathStyles)) { defaultOpticalPathStyles[identifier] = style } const index = numVisible style.color = defaultColors[index] const stats = this.state.pixelDataStatistics[item.identifier] if (stats != null) { style.limitValues = [stats.min, stats.max] } this.volumeViewer.setOpticalPathStyle(item.identifier, style) visibleOpticalPathIdentifiers.push(item.identifier) } } }) } console.info( `selected n=${visibleOpticalPathIdentifiers.length} optical paths ` + 'for visualization' ) visibleOpticalPathIdentifiers.forEach(identifier => { this.volumeViewer.showOpticalPath(identifier) }) this.setState(state => ({ activeOpticalPathIdentifiers: visibleOpticalPathIdentifiers, visibleOpticalPathIdentifiers: visibleOpticalPathIdentifiers, defaultOpticalPathStyles: defaultOpticalPathStyles })) } /** * Handler that gets called when a presentation state has been selected from * the current list of available presentation states. */ handlePresentationStateReset (): void { this.setDefaultPresentationState() this.setState({ selectedPresentationStateUID: undefined }) const urlPath = this.props.location.pathname this.props.history.push(urlPath) } /** * Handler that gets called when a presentation state has been selected from * the current list of available presentation states. */ handlePresentationStateSelection ( value?: string, option?: any ): void { if (value != null) { console.info(`select Presentation State instance "${value}"`) const presentationState = this.state.presentationStates.find(item => { return item.SOPInstanceUID === value }) if (presentationState != null) { this.setPresentationState(presentationState) let urlPath = this.props.location.pathname urlPath += `?state=${presentationState.SOPInstanceUID}` this.props.history.push(urlPath) } else { // eslint-disable-next-line @typescript-eslint/no-floating-promises message.error('Presentation State could not be found') console.log( 'failed to handle section of presentation state: ' + `could not find instance "${value}"` ) } } else { this.setDefaultPresentationState() } this.setState({ selectedPresentationStateUID: value }) } /** * Handler that will toggle the ROI drawing tool, i.e., either activate or * de-activate it, depending on its current state. */ handleRoiDrawing (): void { if (this.volumeViewer.isDrawInteractionActive) { console.info('deactivate drawing of ROIs') this.volumeViewer.deactivateDrawInteraction() this.volumeViewer.activateSelectInteraction({}) this.setState({ isAnnotationModalVisible: false, isRoiTranslationActive: false, isRoiDrawingActive: false, isRoiModificationActive: false }) } else { console.info('activate drawing of ROIs') this.setState({ isAnnotationModalVisible: true, isRoiDrawingActive: true, isRoiModificationActive: false, isRoiTranslationActive: false }) this.volumeViewer.deactivateSelectInteraction() this.volumeViewer.deactivateSnapInteraction() this.volumeViewer.deactivateTranslateInteraction() this.volumeViewer.deactivateModifyInteraction() } } /** * Handler that will toggle the ROI modification tool, i.e., either activate * or de-activate it, depending on its current state. */ handleRoiModification (): void { console.info('toggle modification of ROIs') if (this.volumeViewer.isModifyInteractionActive) { this.volumeViewer.deactivateModifyInteraction() this.volumeViewer.deactivateSnapInteraction() this.volumeViewer.activateSelectInteraction({}) this.setState({ isRoiTranslationActive: false, isRoiDrawingActive: false, isRoiModificationActive: false }) } else { this.setState({ isRoiModificationActive: true, isRoiDrawingActive: false, isRoiTranslationActive: false }) this.volumeViewer.deactivateDrawInteraction() this.volumeViewer.deactivateTranslateInteraction() this.volumeViewer.deactivateSelectInteraction() this.volumeViewer.activateSnapInteraction({}) this.volumeViewer.activateModifyInteraction({}) } } /** * Handler that will toggle the ROI translation tool, i.e., either activate * or de-activate it, depending on its current state. */ handleRoiTranslation (): void { console.info('toggle translation of ROIs') if (this.volumeViewer.isTranslateInteractionActive) { this.volumeViewer.deactivateTranslateInteraction() this.setState({ isRoiTranslationActive: false, isRoiDrawingActive: false, isRoiModificationActive: false }) } else { this.setState({ isRoiTranslationActive: true, isRoiDrawingActive: false, isRoiModificationActive: false }) this.volumeViewer.deactivateModifyInteraction() this.volumeViewer.deactivateSnapInteraction() this.volumeViewer.deactivateDrawInteraction() this.volumeViewer.deactivateSelectInteraction() this.volumeViewer.activateTranslateInteraction({}) } } /** * Handler that will toggle the ROI removal tool, i.e., either activate * or de-activate it, depending on its current state. */ handleRoiRemoval (): void { this.volumeViewer.deactivateDrawInteraction() this.volumeViewer.deactivateSnapInteraction() this.volumeViewer.deactivateTranslateInteraction() this.volumeViewer.deactivateModifyInteraction() if (this.state.selectedRoiUIDs.length > 0) { this.state.selectedRoiUIDs.forEach(uid => { if (uid === undefined) { // eslint-disable-next-line @typescript-eslint/no-floating-promises message.warning('No annotation was selected for removal') return } console.info(`remove ROI "${uid}"`) this.volumeViewer.removeROI(uid) // eslint-disable-next-line @typescript-eslint/no-floating-promises message.info('Annotation was removed') }) this.setState({ selectedRoiUIDs: [], isRoiTranslationActive: false, isRoiDrawingActive: false, isRoiModificationActive: false }) } else { this.state.visibleRoiUIDs.forEach(uid => { console.info(`remove ROI "${uid}"`) this.volumeViewer.removeROI(uid) }) this.setState({ visibleRoiUIDs: [], isRoiTranslationActive: false, isRoiDrawingActive: false, isRoiModificationActive: false }) } this.volumeViewer.activateSelectInteraction({}) } /** * Handler that will toggle the ROI visibility tool, i.e., either activate * or de-activate it, depending on its current state. */ handleRoiVisibilityChange (): void { console.info('toggle visibility of ROIs') if (this.volumeViewer.areROIsVisible) { this.volumeViewer.deactivateDrawInteraction() this.volumeViewer.deactivateSnapInteraction() this.volumeViewer.deactivateTranslateInteraction() this.volumeViewer.deactivateSelectInteraction() this.volumeViewer.deactivateModifyInteraction() this.volumeViewer.hideROIs() this.setState({ areRoisHidden: true, isRoiDrawingActive: false, isRoiModificationActive: false, isRoiTranslationActive: false }) } else { this.volumeViewer.showROIs() this.volumeViewer.activateSelectInteraction({}) this.state.selectedRoiUIDs.forEach(uid => { if (uid !== undefined) { this.volumeViewer.setROIStyle(uid, this.selectedRoiStyle) } }) this.setState({ areRoisHidden: false }) } } render (): React.ReactNode { const rois: dmv.roi.ROI[] = [] const segments: dmv.segment.Segment[] = [] const mappings: dmv.mapping.ParameterMapping[] = [] const annotationGroups: dmv.annotation.AnnotationGroup[] = [] rois.push(...this.volumeViewer.getAllROIs()) segments.push(...this.volumeViewer.getAllSegments()) mappings.push(...this.volumeViewer.getAllParameterMappings()) annotationGroups.push(...this.volumeViewer.getAllAnnotationGroups()) const openSubMenuItems = ['specimens', 'opticalpaths', 'annotations'] let report: React.ReactNode const dataset = this.state.generatedReport if (dataset !== undefined) { report = <Report dataset={dataset} /> } let annotationMenuItems: React.ReactNode if (rois.length > 0) { annotationMenuItems = ( <AnnotationList rois={rois} selectedRoiUIDs={this.state.selectedRoiUIDs} visibleRoiUIDs={this.state.visibleRoiUIDs} onSelection={this.handleAnnotationSelection} onVisibilityChange={this.handleAnnotationVisibilityChange} /> ) } const findingOptions = this.findingOptions.map(finding => { return ( <Select.Option key={finding.CodeValue} value={finding.CodeValue} > {finding.CodeMeaning} </Select.Option> ) }) const geometryTypeOptionsMapping: { [key: string]: React.ReactNode } = { point: <Select.Option key='point' value='point'>Point</Select.Option>, circle: <Select.Option key='circle' value='circle'>Circle</Select.Option>, box: <Select.Option key='box' value='box'>Box</Select.Option>, polygon: <Select.Option key='polygon' value='polygon'>Polygon</Select.Option>, line: <Select.Option key='line' value='line'>Line</Select.Option>, freehandpolygon: ( <Select.Option key='freehandpolygon' value='freehandpolygon'> Polygon (freehand) </Select.Option> ), freehandline: ( <Select.Option key='freehandline' value='freehandline'> Line (freehand) </Select.Option> ) } const selections: React.ReactNode[] = [ ( <Select style={{ minWidth: 130 }} onSelect={this.handleAnnotationFindingSelection} key='annotation-finding' defaultActiveFirstOption > {findingOptions} </Select> ) ] const selectedFinding = this.state.selectedFinding if (selectedFinding !== undefined) { const key = _buildKey(selectedFinding) this.evaluationOptions[key].forEach(evaluation => { const evaluationOptions = evaluation.values.map(code => { return ( <Select.Option key={code.CodeValue} value={code.CodeValue} label={evaluation.name} > {code.CodeMeaning} </Select.Option> ) }) selections.push( <> {evaluation.name.CodeMeaning} <Select style={{ minWidth: 130 }} onSelect={this.handleAnnotationEvaluationSelection} allowClear onClear={this.handleAnnotationEvaluationClearance} defaultActiveFirstOption={false} > {evaluationOptions} </Select> </> ) }) const geometryTypeOptions = this.geometryTypeOptions[key].map(name => { return geometryTypeOptionsMapping[name] }) selections.push( <Select style={{ minWidth: 130 }} onSelect={this.handleAnnotationGeometryTypeSelection} key='annotation-geometry-type' > {geometryTypeOptions} </Select> ) selections.push( <Checkbox onChange={this.handleAnnotationMeasurementActivation} key='annotation-measurement' > measure </Checkbox> ) } const specimenMenu = ( <Menu.SubMenu key='specimens' title='Specimens'> <SpecimenList metadata={this.props.slide.volumeImages[0]} showstain={false} /> </Menu.SubMenu> ) const equipmentMenu = ( <Menu.SubMenu key='equipment' title='Equipment'> <Equipment metadata={this.props.slide.volumeImages[0]} /> </Menu.SubMenu> ) const defaultOpticalPathStyles: { [identifier: string]: { opacity: number color?: number[] limitValues?: number[] } } = {} const opticalPathMetadata: { [identifier: string]: dmv.metadata.VLWholeSlideMicroscopyImage[] } = {} const opticalPaths = this.volumeViewer.getAllOpticalPaths() opticalPaths.sort((a, b) => { if (a.identifier < b.identifier) { return -1 } else if (a.identifier > b.identifier) { return 1 } return 0 }) opticalPaths.forEach(opticalPath => { const identifier = opticalPath.identifier const metadata = this.volumeViewer.getOpticalPathMetadata(identifier) opticalPathMetadata[identifier] = metadata const style = this.volumeViewer.getOpticalPathStyle(identifier) defaultOpticalPathStyles[identifier] = style }) const opticalPathMenu = ( <Menu.SubMenu key='opticalpaths' title='Optical Paths'> <OpticalPathList metadata={opticalPathMetadata} opticalPaths={opticalPaths} defaultOpticalPathStyles={defaultOpticalPathStyles} visibleOpticalPathIdentifiers={this.state.visibleOpticalPathIdentifiers} activeOpticalPathIdentifiers={this.state.activeOpticalPathIdentifiers} onOpticalPathVisibilityChange={this.handleOpticalPathVisibilityChange} onOpticalPathStyleChange={this.handleOpticalPathStyleChange} onOpticalPathActivityChange={this.handleOpticalPathActivityChange} selectedPresentationStateUID={this.state.selectedPresentationStateUID} /> </Menu.SubMenu> ) let presentationStateMenu console.log('DEBUG: ', this.state.presentationStates) if (this.state.presentationStates.length > 0) { const presentationStateOptions = this.state.presentationStates.map( presentationState => { return ( <Select.Option key={presentationState.SOPInstanceUID} value={presentationState.SOPInstanceUID} dropdownMatchSelectWidth={false} size='small' > {presentationState.ContentDescription} </Select.Option> ) } ) presentationStateMenu = ( <Menu.SubMenu key='presentationStates' title='Presentation States'> <Space align='center' size={20} style={{ padding: '14px' }}> <Select style={{ minWidth: 200, maxWidth: 200 }} onSelect={this.handlePresentationStateSelection} key='presentation-states' defaultValue={this.props.selectedPresentationStateUID} value={this.state.selectedPresentationStateUID} > {presentationStateOptions} </Select> <Tooltip title='Reset'> <Btn icon={<UndoOutlined />} type='primary' onClick={this.handlePresentationStateReset} /> </Tooltip> </Space> </Menu.SubMenu> ) } let segmentationMenu if (segments.length > 0) { const defaultSegmentStyles: { [segmentUID: string]: { opacity: number } } = {} const segmentMetadata: { [segmentUID: string]: dmv.metadata.Segmentation[] } = {} const segments = this.volumeViewer.getAllSegments() segments.forEach(segment => { defaultSegmentStyles[segment.uid] = this.volumeViewer.getSegmentStyle( segment.uid ) segmentMetadata[segment.uid] = this.volumeViewer.getSegmentMetadata( segment.uid ) }) segmentationMenu = ( <Menu.SubMenu key='segmentations' title='Segmentations'> <SegmentList segments={segments} metadata={segmentMetadata} defaultSegmentStyles={defaultSegmentStyles} visibleSegmentUIDs={this.state.visibleSegmentUIDs} onSegmentVisibilityChange={this.handleSegmentVisibilityChange} onSegmentStyleChange={this.handleSegmentStyleChange} /> </Menu.SubMenu> ) openSubMenuItems.push('segmentations') } let parametricMapMenu if (mappings.length > 0) { const defaultMappingStyles: { [mappingUID: string]: { opacity: number } } = {} const mappingMetadata: { [mappingUID: string]: dmv.metadata.ParametricMap[] } = {} mappings.forEach(mapping => { defaultMappingStyles[mapping.uid] = this.volumeViewer.getParameterMappingStyle( mapping.uid ) mappingMetadata[mapping.uid] = this.volumeViewer.getParameterMappingMetadata( mapping.uid ) }) parametricMapMenu = ( <Menu.SubMenu key='parmetricmaps' title='Parametric Maps'> <MappingList mappings={mappings} metadata={mappingMetadata} defaultMappingStyles={defaultMappingStyles} visibleMappingUIDs={this.state.visibleMappingUIDs} onMappingVisibilityChange={this.handleMappingVisibilityChange} onMappingStyleChange={this.handleMappingStyleChange} /> </Menu.SubMenu> ) openSubMenuItems.push('parametricmaps') } let annotationGroupMenu if (annotationGroups.length > 0) { const defaultAnnotationGroupStyles: { [annotationGroupUID: string]: { opacity: number } } = {} const annotationGroupMetadata: { [annotationGroupUID: string]: dmv.metadata.MicroscopyBulkSimpleAnnotations } = {} const annotationGroups = this.volumeViewer.getAllAnnotationGroups() annotationGroups.forEach(annotationGroup => { defaultAnnotationGroupStyles[annotationGroup.uid] = this.volumeViewer.getAnnotationGroupStyle( annotationGroup.uid ) annotationGroupMetadata[annotationGroup.uid] = this.volumeViewer.getAnnotationGroupMetadata( annotationGroup.uid ) }) annotationGroupMenu = ( <Menu.SubMenu key='annotationGroups' title='Annotation Groups'> <AnnotationGroupList annotationGroups={annotationGroups} metadata={annotationGroupMetadata} defaultAnnotationGroupStyles={defaultAnnotationGroupStyles} visibleAnnotationGroupUIDs={this.state.visibleAnnotationGroupUIDs} onAnnotationGroupVisibilityChange={this.handleAnnotationGroupVisibilityChange} onAnnotationGroupStyleChange={this.handleAnnotationGroupStyleChange} /> </Menu.SubMenu> ) openSubMenuItems.push('annotationGroups') } let toolbar let toolbarHeight = '0px' if (this.props.enableAnnotationTools) { toolbar = ( <Row> <Button tooltip='Draw ROI [d]' icon={FaDrawPolygon} onClick={this.handleRoiDrawing} isSelected={this.state.isRoiDrawingActive} /> <Button tooltip='Modify ROIs [m]' icon={FaHandPointer} onClick={this.handleRoiModification} isSelected={this.state.isRoiModificationActive} /> <Button tooltip='Translate ROIs [t]' icon={FaHandPaper} onClick={this.handleRoiTranslation} isSelected={this.state.isRoiTranslationActive} /> <Button tooltip='Remove selected ROI [r]' onClick={this.handleRoiRemoval} icon={FaTrash} /> <Button tooltip='Show/Hide ROIs [v]' icon={this.state.areRoisHidden ? FaEye : FaEyeSlash} onClick={this.handleRoiVisibilityChange} isSelected={this.state.areRoisHidden} /> <Button tooltip='Save ROIs [s]' icon={FaSave} onClick={this.handleReportGeneration} /> </Row> ) toolbarHeight = '50px' } /* It would be nicer to use the ant Spin component, but that causes issues * with the positioning of the viewport. */ let loadingDisplay = 'none' if (this.state.isLoading) { loadingDisplay = 'block' } return ( <Layout style={{ height: '100%' }} hasSider> <Layout.Content style={{ height: '100%' }}> {toolbar} <div className='dimmer' style={{ display: loadingDisplay }} /> <div className='spinner' style={{ display: loadingDisplay }} /> <div style={{ height: `calc(100% - ${toolbarHeight})`, overflow: 'hidden' }} ref={this.volumeViewportRef} /> <Modal visible={this.state.isAnnotationModalVisible} title='Configure annotations' onOk={this.handleAnnotationConfigurationCompletion} onCancel={this.handleAnnotationConfigurationCancellation} okText='Select' > <Space align='start' direction='vertical'> {selections} </Space> </Modal> <Modal visible={this.state.isReportModalVisible} title='Verify and save report' onOk={this.handleReportVerification} onCancel={this.handleReportCancellation} okText='Save' > {report} </Modal> </Layout.Content> <Layout.Sider width={300} reverseArrow style={{ borderLeft: 'solid', borderLeftWidth: 0.25, overflow: 'hidden', background: 'none' }} > <Menu mode='inline' defaultOpenKeys={openSubMenuItems} style={{ height: '100%' }} inlineIndent={14} forceSubMenuRender > <Menu.SubMenu key='label' title='Slide label'> <Menu.Item style={{ height: '100%' }}> <div style={{ height: '220px' }} ref={this.labelViewportRef} /> </Menu.Item> </Menu.SubMenu> {specimenMenu} {equipmentMenu} {opticalPathMenu} {presentationStateMenu} <Menu.SubMenu key='annotations' title='Annotations'> {annotationMenuItems} </Menu.SubMenu> {annotationGroupMenu} {segmentationMenu} {parametricMapMenu} </Menu> </Layout.Sider> </Layout> ) } } export default withRouter(SlideViewer)