diff --git a/extensions/dicom-microscopy/src/DicomMicroscopyViewport.tsx b/extensions/dicom-microscopy/src/DicomMicroscopyViewport.tsx index 46bf0ff4d..11da8bca2 100644 --- a/extensions/dicom-microscopy/src/DicomMicroscopyViewport.tsx +++ b/extensions/dicom-microscopy/src/DicomMicroscopyViewport.tsx @@ -7,252 +7,324 @@ import getDicomWebClient from './utils/dicomWebClient'; import dcmjs from 'dcmjs'; import { useSystem } from '@ohif/core'; -function DicomMicroscopyViewport({ - activeViewportId, - setViewportActive, - displaySets, - viewportId, - dataSource, - resizeRef, -}: { - activeViewportId: string; - setViewportActive: Function; - displaySets: any[]; - viewportId: string; - dataSource: any; - resizeRef: any; -}) { - const { servicesManager, extensionManager } = useSystem(); - const [isLoaded, setIsLoaded] = useState(false); - const [viewer, setViewer] = useState(null); - const [managedViewer, setManagedViewer] = useState(null); - const overlayElement = useRef(); - const container = useRef(); - const { microscopyService, customizationService } = servicesManager.services; +// This component is wrapped with React.memo and uses a custom areEqual comparison +// function to prevent unnecessary re-renders when props are semantically identical +// (e.g. `displaySets` reference changes, but the content is the same). +// Note: If this component starts depending on additional props, +// update `areEqual` accordingly. +const DicomMicroscopyViewport = React.memo( + ({ + activeViewportId, + setViewportActive, + displaySets, + viewportId, + dataSource, + resizeRef, + }: { + activeViewportId: string; + setViewportActive: Function; + displaySets: any[]; + viewportId: string; + dataSource: any; + resizeRef: any; + }) => { + const { servicesManager, extensionManager } = useSystem(); + const [isLoaded, setIsLoaded] = useState(false); + const [viewer, setViewer] = useState(null); + const [managedViewer, setManagedViewer] = useState(null); + const overlayElement = useRef(); + const container = useRef(); + const { microscopyService, customizationService } = servicesManager.services; - const overlayData = customizationService.getCustomization('microscopyViewport.overlay'); + const overlayData = customizationService.getCustomization('microscopyViewport.overlay'); - // install the microscopy renderer into the web page. - // you should only do this once. - const installOpenLayersRenderer = useCallback( - async (container, displaySet) => { - const loadViewer = async metadata => { - const dicomMicroscopyModule = await microscopyService.importDicomMicroscopyViewer(); - const { viewer: DicomMicroscopyViewer, metadata: metadataUtils } = dicomMicroscopyModule; + // install the microscopy renderer into the web page. + // you should only do this once. + const installOpenLayersRenderer = useCallback( + async (container, displaySet) => { + const loadViewer = async metadata => { + const dicomMicroscopyModule = await microscopyService.importDicomMicroscopyViewer(); + const { viewer: DicomMicroscopyViewer, metadata: metadataUtils } = dicomMicroscopyModule; - const microscopyViewer = DicomMicroscopyViewer.VolumeImageViewer; + const microscopyViewer = DicomMicroscopyViewer.VolumeImageViewer; - const client = getDicomWebClient({ - extensionManager, - servicesManager, - }); - - // Parse, format, and filter metadata - const volumeImages: any[] = []; - - /** - * This block of code is the original way of loading DICOM into dicom-microscopy-viewer - * as in their documentation. - * But we have the metadata already loaded by our loaders. - * As the metadata for microscopy DIOM files tends to be big and we don't - * want to double load it, below we have the mechanism to reconstruct the - * DICOM JSON structure (denaturalized) from naturalized metadata. - * (NOTE: Our loaders cache only naturalized metadata, not the denaturalized.) - */ - // { - // const retrieveOptions = { - // studyInstanceUID: metadata[0].StudyInstanceUID, - // seriesInstanceUID: metadata[0].SeriesInstanceUID, - // }; - // metadata = await client.retrieveSeriesMetadata(retrieveOptions); - // // Parse, format, and filter metadata - // metadata.forEach(m => { - // if ( - // volumeImages.length > 0 && - // m['00200052'].Value[0] != volumeImages[0].FrameOfReferenceUID - // ) { - // console.warn( - // 'Expected FrameOfReferenceUID of difference instances within a series to be the same, found multiple different values', - // m['00200052'].Value[0] - // ); - // m['00200052'].Value[0] = volumeImages[0].FrameOfReferenceUID; - // } - // NOTE: depending on different data source, image.ImageType sometimes - // is a string, not a string array. - // m['00080008'] = transformImageTypeUnnaturalized(m['00080008']); - - // const image = new metadataUtils.VLWholeSlideMicroscopyImage({ - // metadata: m, - // }); - // const imageFlavor = image.ImageType[2]; - // if (imageFlavor === 'VOLUME' || imageFlavor === 'THUMBNAIL') { - // volumeImages.push(image); - // } - // }); - // } - - metadata.forEach(m => { - // NOTE: depending on different data source, image.ImageType sometimes - // is a string, not a string array. - m.ImageType = typeof m.ImageType === 'string' ? m.ImageType.split('\\') : m.ImageType; - - const inst = cleanDenaturalizedDataset( - dcmjs.data.DicomMetaDictionary.denaturalizeDataset(m), - { - StudyInstanceUID: m.StudyInstanceUID, - SeriesInstanceUID: m.SeriesInstanceUID, - dataSourceConfig: dataSource.getConfig(), - } - ); - if (!inst['00480105']) { - // Optical Path Sequence, no OpticalPathIdentifier? - // NOTE: this is actually a not-well formatted DICOM VL Whole Slide Microscopy Image. - inst['00480105'] = { - vr: 'SQ', - Value: [ - { - '00480106': { - vr: 'SH', - Value: ['1'], - }, - }, - ], - }; - } - const image = new metadataUtils.VLWholeSlideMicroscopyImage({ - metadata: inst, + const client = getDicomWebClient({ + extensionManager, + servicesManager, }); - const imageFlavor = image.ImageType[2]; - if (imageFlavor === 'VOLUME' || imageFlavor === 'THUMBNAIL') { - volumeImages.push(image); - } - }); + // Parse, format, and filter metadata + const volumeImages: any[] = []; - // format metadata for microscopy-viewer - const options = { - client, - metadata: volumeImages, - retrieveRendered: false, - controls: ['overview', 'position'], + /** + * This block of code is the original way of loading DICOM into dicom-microscopy-viewer + * as in their documentation. + * But we have the metadata already loaded by our loaders. + * As the metadata for microscopy DIOM files tends to be big and we don't + * want to double load it, below we have the mechanism to reconstruct the + * DICOM JSON structure (denaturalized) from naturalized metadata. + * (NOTE: Our loaders cache only naturalized metadata, not the denaturalized.) + */ + // { + // const retrieveOptions = { + // studyInstanceUID: metadata[0].StudyInstanceUID, + // seriesInstanceUID: metadata[0].SeriesInstanceUID, + // }; + // metadata = await client.retrieveSeriesMetadata(retrieveOptions); + // // Parse, format, and filter metadata + // metadata.forEach(m => { + // if ( + // volumeImages.length > 0 && + // m['00200052'].Value[0] != volumeImages[0].FrameOfReferenceUID + // ) { + // console.warn( + // 'Expected FrameOfReferenceUID of difference instances within a series to be the same, found multiple different values', + // m['00200052'].Value[0] + // ); + // m['00200052'].Value[0] = volumeImages[0].FrameOfReferenceUID; + // } + // NOTE: depending on different data source, image.ImageType sometimes + // is a string, not a string array. + // m['00080008'] = transformImageTypeUnnaturalized(m['00080008']); + + // const image = new metadataUtils.VLWholeSlideMicroscopyImage({ + // metadata: m, + // }); + // const imageFlavor = image.ImageType[2]; + // if (imageFlavor === 'VOLUME' || imageFlavor === 'THUMBNAIL') { + // volumeImages.push(image); + // } + // }); + // } + + metadata.forEach(m => { + // NOTE: depending on different data source, image.ImageType sometimes + // is a string, not a string array. + m.ImageType = typeof m.ImageType === 'string' ? m.ImageType.split('\\') : m.ImageType; + + const inst = cleanDenaturalizedDataset( + dcmjs.data.DicomMetaDictionary.denaturalizeDataset(m), + { + StudyInstanceUID: m.StudyInstanceUID, + SeriesInstanceUID: m.SeriesInstanceUID, + dataSourceConfig: dataSource.getConfig(), + } + ); + if (!inst['00480105']) { + // Optical Path Sequence, no OpticalPathIdentifier? + // NOTE: this is actually a not-well formatted DICOM VL Whole Slide Microscopy Image. + inst['00480105'] = { + vr: 'SQ', + Value: [ + { + '00480106': { + vr: 'SH', + Value: ['1'], + }, + }, + ], + }; + } + const image = new metadataUtils.VLWholeSlideMicroscopyImage({ + metadata: inst, + }); + + const imageFlavor = image.ImageType[2]; + if (imageFlavor === 'VOLUME' || imageFlavor === 'THUMBNAIL') { + volumeImages.push(image); + } + }); + + // format metadata for microscopy-viewer + const options = { + client, + metadata: volumeImages, + retrieveRendered: false, + controls: ['overview', 'position'], + }; + + const viewer = new microscopyViewer(options); + + if (overlayElement && overlayElement.current && viewer.addViewportOverlay) { + viewer.addViewportOverlay({ + element: overlayElement.current, + coordinates: [0, 0], // TODO: dicom-microscopy-viewer documentation says this can be false to be automatically, but it is not. + navigate: true, + className: 'OpenLayersOverlay', + }); + } + + viewer.render({ container }); + + const { StudyInstanceUID, SeriesInstanceUID } = displaySet; + + const managedViewer = microscopyService.addViewer( + viewer, + viewportId, + container, + StudyInstanceUID, + SeriesInstanceUID + ); + + managedViewer.addContextMenuCallback((event: Event) => { + // TODO: refactor this after Bill's changes on ContextMenu feature get merged + // const roiAnnotationNearBy = this.getNearbyROI(event); + }); + + setViewer(viewer); + setManagedViewer(managedViewer); }; - const viewer = new microscopyViewer(options); + microscopyService.clearAnnotations(); - if (overlayElement && overlayElement.current && viewer.addViewportOverlay) { - viewer.addViewportOverlay({ - element: overlayElement.current, - coordinates: [0, 0], // TODO: dicom-microscopy-viewer documentation says this can be false to be automatically, but it is not. - navigate: true, - className: 'OpenLayersOverlay', - }); + let smDisplaySet = displaySet; + if (displaySet.isOverlayDisplaySet) { + // for SR displaySet, let's load the actual image displaySet + smDisplaySet = displaySet.getSourceDisplaySet(); } + console.log('Loading viewer metadata', smDisplaySet); - viewer.render({ container }); + await loadViewer(smDisplaySet.others); - const { StudyInstanceUID, SeriesInstanceUID } = displaySet; + if (displaySet.isOverlayDisplaySet && !displaySet.isLoaded && !displaySet.isLoading) { + const referencedDisplaySet = displaySet.getSourceDisplaySet(); + displaySet.load(referencedDisplaySet); + } + }, + [dataSource, extensionManager, microscopyService, servicesManager, viewportId] + ); - const managedViewer = microscopyService.addViewer( - viewer, - viewportId, - container, - StudyInstanceUID, - SeriesInstanceUID - ); - - managedViewer.addContextMenuCallback((event: Event) => { - // TODO: refactor this after Bill's changes on ContextMenu feature get merged - // const roiAnnotationNearBy = this.getNearbyROI(event); + useEffect(() => { + if (!viewer) { + const displaySet = displaySets[0]; + installOpenLayersRenderer(container.current, displaySet).then(() => { + setIsLoaded(true); }); + } - setViewer(viewer); - setManagedViewer(managedViewer); + return () => { + if (viewer) { + microscopyService.removeViewer(viewer); + } }; + }, [viewer]); + + useEffect(() => { + const displaySet = displaySets[0]; microscopyService.clearAnnotations(); - let smDisplaySet = displaySet; - if (displaySet.isOverlayDisplaySet) { - // for SR displaySet, let's load the actual image displaySet - smDisplaySet = displaySet.getSourceDisplaySet(); - } - console.log('Loading viewer metadata', smDisplaySet); - - await loadViewer(smDisplaySet.others); - + // loading SR - only if not already loaded and not currently loading if (displaySet.isOverlayDisplaySet && !displaySet.isLoaded && !displaySet.isLoading) { - displaySet.load(smDisplaySet); + const referencedDisplaySet = displaySet.getSourceDisplaySet(); + displaySet.load(referencedDisplaySet); } - }, - [dataSource, extensionManager, microscopyService, servicesManager, viewportId] - ); + }, [managedViewer, displaySets, microscopyService]); - useEffect(() => { + const style = { width: '100%', height: '100%' }; const displaySet = displaySets[0]; - installOpenLayersRenderer(container.current, displaySet).then(() => { - setIsLoaded(true); - }); + const firstInstance = displaySet.firstInstance || displaySet.instance; + const LoadingIndicatorProgress = customizationService.getCustomization( + 'ui.loadingIndicatorProgress' + ); - return () => { - if (viewer) { - microscopyService.removeViewer(viewer); - } - }; - }, []); - - useEffect(() => { - const displaySet = displaySets[0]; - - microscopyService.clearAnnotations(); - - // loading SR - only if not already loaded and not currently loading - if (displaySet.isOverlayDisplaySet && !displaySet.isLoaded && !displaySet.isLoading) { - const referencedDisplaySet = displaySet.getSourceDisplaySet(); - displaySet.load(referencedDisplaySet); - } - }, [managedViewer, displaySets, microscopyService]); - - const style = { width: '100%', height: '100%' }; - const displaySet = displaySets[0]; - const firstInstance = displaySet.firstInstance || displaySet.instance; - const LoadingIndicatorProgress = customizationService.getCustomization( - 'ui.loadingIndicatorProgress' - ); - - return ( -