diff --git a/extensions/cornerstone-dicom-sr/src/utils/hydrateStructuredReport.ts b/extensions/cornerstone-dicom-sr/src/utils/hydrateStructuredReport.ts index c6c674777..b934f6562 100644 --- a/extensions/cornerstone-dicom-sr/src/utils/hydrateStructuredReport.ts +++ b/extensions/cornerstone-dicom-sr/src/utils/hydrateStructuredReport.ts @@ -80,7 +80,7 @@ export default function hydrateStructuredReport( const { ReferencedSOPInstanceUID, imageId, frameNumber = 1 } = measurement; const key = `${ReferencedSOPInstanceUID}:${frameNumber}`; - if (!sopInstanceUIDToImageId[key]) { + if (imageId && !sopInstanceUIDToImageId[key]) { sopInstanceUIDToImageId[key] = imageId; } }); @@ -118,41 +118,17 @@ export default function hydrateStructuredReport( } }); - // Set the series touched as tracked. - const imageIds = []; - - // TODO: notification if no hydratable? - Object.keys(hydratableMeasurementsInSR).forEach(annotationType => { - const toolDataForAnnotationType = hydratableMeasurementsInSR[annotationType]; - - toolDataForAnnotationType.forEach(toolData => { - // Add the measurement to toolState - // dcmjs and Cornerstone3D has structural defect in supporting multi-frame - // files, and looking up the imageId from sopInstanceUIDToImageId results - // in the wrong value. - const frameNumber = toolData.annotation.data?.frameNumber || 1; - const imageId = sopInstanceUIDToImageId[`${toolData.sopInstanceUid}:${frameNumber}`]; - - if (!imageIds.includes(imageId)) { - imageIds.push(imageId); - } - }); - }); - let targetStudyInstanceUID; const SeriesInstanceUIDs = []; - for (let i = 0; i < imageIds.length; i++) { - const imageId = imageIds[i]; - if (!imageId) { - continue; - } - const { SeriesInstanceUID, StudyInstanceUID } = metaData.get('instance', imageId); + // Set the series touched as tracked. + const imageIds = getImageIds(hydratableMeasurementsInSR, sopInstanceUIDToImageId); + for (const imageId of imageIds) { + const { SeriesInstanceUID, StudyInstanceUID } = metaData.get('instance', imageId); if (!SeriesInstanceUIDs.includes(SeriesInstanceUID)) { SeriesInstanceUIDs.push(SeriesInstanceUID); } - if (!targetStudyInstanceUID) { targetStudyInstanceUID = StudyInstanceUID; } else if (targetStudyInstanceUID !== StudyInstanceUID) { @@ -160,6 +136,31 @@ export default function hydrateStructuredReport( } } + // For 3d annotations there are no image IDs, + // so we need to find the display sets by frame of reference to get the SeriesInstanceUIDs + const frameOfReferenceUIDs = getFrameOfReferenceUIDs( + hydratableMeasurementsInSR, + sopInstanceUIDToImageId + ); + + for (const FrameOfReferenceUID of frameOfReferenceUIDs) { + const displaySetsFOR = displaySetService.getDisplaySetsBy( + ds => ds.FrameOfReferenceUID === FrameOfReferenceUID && !ds.isDerivedDisplaySet + ); + const ds = chooseDisplaySet(displaySetsFOR, FrameOfReferenceUID); + if (!ds) { + continue; + } + if (!SeriesInstanceUIDs.includes(ds.SeriesInstanceUID)) { + SeriesInstanceUIDs.push(ds.SeriesInstanceUID); + } + if (!targetStudyInstanceUID) { + targetStudyInstanceUID = ds.StudyInstanceUID; + } else if (targetStudyInstanceUID !== ds.StudyInstanceUID) { + console.warn('NO SUPPORT FOR SRs THAT HAVE MEASUREMENTS FROM MULTIPLE STUDIES.'); + } + } + /** * Gets reference data for what frame of reference and the referenced * image id, or for 3d measurements, the volumeId to apply this annotation to. @@ -196,7 +197,7 @@ export default function hydrateStructuredReport( toolDataForAnnotationType.forEach(toolData => { toolData.uid = guid(); const referenceData = getReferenceData(toolData); - const { imageId } = referenceData; + const { referencedImageId } = referenceData; const annotation = { annotationUID: toolData.annotation.annotationUID, @@ -241,8 +242,8 @@ export default function hydrateStructuredReport( locking.setAnnotationLocked(newAnnotationUID, true); } - if (imageId && !imageIds.includes(imageId)) { - imageIds.push(imageId); + if (referencedImageId && !imageIds.includes(referencedImageId)) { + imageIds.push(referencedImageId); } }); }); @@ -255,15 +256,64 @@ export default function hydrateStructuredReport( }; } +/** + * Gets the unique imageIds from hydratable measurements that have an imageId reference + * (i.e., 2D/SCOORD annotations). + */ +function getImageIds(hydratableMeasurementsInSR, sopInstanceUIDToImageId): string[] { + const imageIds: string[] = []; + Object.keys(hydratableMeasurementsInSR).forEach(annotationType => { + const toolDataForAnnotationType = hydratableMeasurementsInSR[annotationType]; + + toolDataForAnnotationType.forEach(toolData => { + // Add the measurement to toolState + // dcmjs and Cornerstone3D has structural defect in supporting multi-frame + // files, and looking up the imageId from sopInstanceUIDToImageId results + // in the wrong value. + const frameNumber = toolData.annotation.data?.frameNumber || 1; + const imageId = sopInstanceUIDToImageId[`${toolData.sopInstanceUid}:${frameNumber}`]; + + if (imageId && !imageIds.includes(imageId)) { + imageIds.push(imageId); + } + }); + }); + return imageIds; +} + +/** + * Gets the unique FrameOfReferenceUIDs from hydratable measurements that have no imageId reference + * (i.e., 3D/SCOORD3D annotations). This excludes annotations handled by the getImageIds function. + */ +function getFrameOfReferenceUIDs(hydratableMeasurementsInSR, sopInstanceUIDToImageId): string[] { + const frameOfReferenceUIDs: string[] = []; + + Object.keys(hydratableMeasurementsInSR).forEach(annotationType => { + const toolDataForAnnotationType = hydratableMeasurementsInSR[annotationType]; + toolDataForAnnotationType.forEach(toolData => { + const frameNumber = toolData.annotation.data?.frameNumber || 1; + const imageId = sopInstanceUIDToImageId[`${toolData.sopInstanceUid}:${frameNumber}`]; + + if (!imageId) { + const { FrameOfReferenceUID } = toolData.annotation.metadata; + if (FrameOfReferenceUID && !frameOfReferenceUIDs.includes(FrameOfReferenceUID)) { + frameOfReferenceUIDs.push(FrameOfReferenceUID); + } + } + }); + }); + return frameOfReferenceUIDs; +} + /** * For 3d annotations, there are often several display sets which could * be used to display the annotation. Choose the first annotation with the * same frame of reference that is reconstructable, or the first display set * otherwise. */ -function chooseDisplaySet(displaySets, annotation) { +function chooseDisplaySet(displaySets, reference) { if (!displaySets?.length) { - console.warn('No display set found for', annotation); + console.warn('No display set found for', reference); return; } if (displaySets.length === 1) { diff --git a/tests/SRHydrationArrowNavigation.spec.ts b/tests/SRHydrationArrowNavigation.spec.ts new file mode 100644 index 000000000..9be6fbe94 --- /dev/null +++ b/tests/SRHydrationArrowNavigation.spec.ts @@ -0,0 +1,94 @@ +import { expect, navigateWithViewportArrow, test, visitStudy } from './utils'; +import { expectRowSelected } from './utils/assertions'; +import { press } from './utils/keyboardUtils'; + +test.beforeEach(async ({ page }) => { + const studyInstanceUID = '1.3.6.1.4.1.14519.5.2.1.7310.5101.860473186348887719777907797922'; + const mode = 'viewer'; + await visitStudy(page, studyInstanceUID, mode, 2000); +}); + +test('should navigate SR measurements with next/prev arrows after hydration for 3D SR', async ({ + page, + DOMOverlayPageObject, + leftPanelPageObject, + rightPanelPageObject, + viewportPageObject, +}) => { + await rightPanelPageObject.toggle(); + await rightPanelPageObject.measurementsPanel.select(); + await leftPanelPageObject.loadSeriesByModality('SR'); + await page.waitForTimeout(2000); + + await DOMOverlayPageObject.viewport.segmentationHydration.yes.click(); + await page.waitForTimeout(2000); + + const measurementCount = await rightPanelPageObject.measurementsPanel.panel.getMeasurementCount(); + expect(measurementCount).toBeGreaterThan(1); + + // Navigate to first image + await viewportPageObject.active.pane.click(); + await press({ page, key: 'Home' }); + await page.waitForTimeout(2000); + + // first arrow click should navigate to second measurement + await navigateWithViewportArrow(viewportPageObject, 'next'); + + await expectRowSelected(rightPanelPageObject.measurementsPanel.panel.nthMeasurement(1)); + + await expect(viewportPageObject.active.svg('circle')).toBeVisible(); + + const secondAnnotation = viewportPageObject.active.nthAnnotation(0); + await expect(secondAnnotation.locator).toBeVisible(); + await expect(secondAnnotation.text.locator).toBeVisible(); + + // navigate back to first measurement + await navigateWithViewportArrow(viewportPageObject, 'prev'); + + await expectRowSelected(rightPanelPageObject.measurementsPanel.panel.nthMeasurement(0)); + + await expect(viewportPageObject.active.svg('circle')).toBeVisible(); + + const firstAnnotation = viewportPageObject.active.nthAnnotation(0); + await expect(firstAnnotation.locator).toBeVisible(); + await expect(firstAnnotation.text.locator).toBeVisible(); +}); + +test('should keep arrows visible and functional after clicking measurement in right panel', async ({ + page, + DOMOverlayPageObject, + leftPanelPageObject, + rightPanelPageObject, + viewportPageObject, +}) => { + await rightPanelPageObject.toggle(); + await rightPanelPageObject.measurementsPanel.select(); + await leftPanelPageObject.loadSeriesByModality('SR'); + await page.waitForTimeout(2000); + + await DOMOverlayPageObject.viewport.segmentationHydration.yes.click(); + await page.waitForTimeout(2000); + + const measurementCount = await rightPanelPageObject.measurementsPanel.panel.getMeasurementCount(); + expect(measurementCount).toBeGreaterThan(1); + + // click on first measurement + await rightPanelPageObject.measurementsPanel.panel.nthMeasurement(0).click(); + + await expect(viewportPageObject.active.nthAnnotation(0).locator).toBeVisible(); + + await expect(viewportPageObject.active.navigationArrows.next.button).toBeVisible(); + await expect(viewportPageObject.active.navigationArrows.prev.button).toBeVisible(); + + // navigate to second measurement + await navigateWithViewportArrow(viewportPageObject, 'next'); + + await expectRowSelected(rightPanelPageObject.measurementsPanel.panel.nthMeasurement(1)); + await expect(viewportPageObject.active.nthAnnotation(0).locator).toBeVisible(); + + // navigate back to first measurement + await navigateWithViewportArrow(viewportPageObject, 'prev'); + + await expectRowSelected(rightPanelPageObject.measurementsPanel.panel.nthMeasurement(0)); + await expect(viewportPageObject.active.nthAnnotation(0).locator).toBeVisible(); +});