diff --git a/addOns/externals/devDependencies/package.json b/addOns/externals/devDependencies/package.json index 5533fb4f873..f64d2d77184 100644 --- a/addOns/externals/devDependencies/package.json +++ b/addOns/externals/devDependencies/package.json @@ -68,7 +68,7 @@ "prettier-plugin-tailwindcss": "^0.5.4", "react-refresh": "^0.14.2", "semver": "^7.5.1", - "serve": "^14.2.0", + "serve": "^14.2.4", "shader-loader": "^1.3.1", "shx": "^0.3.3", "source-map-loader": "^4.0.1", diff --git a/extensions/cornerstone-dicom-pmap/src/viewports/OHIFCornerstonePMAPViewport.tsx b/extensions/cornerstone-dicom-pmap/src/viewports/OHIFCornerstonePMAPViewport.tsx index 2133e96ddfd..959d8dbfe37 100644 --- a/extensions/cornerstone-dicom-pmap/src/viewports/OHIFCornerstonePMAPViewport.tsx +++ b/extensions/cornerstone-dicom-pmap/src/viewports/OHIFCornerstonePMAPViewport.tsx @@ -97,6 +97,7 @@ function OHIFCornerstonePMAPViewport(props: withAppTypes) { viewportType: 'volume', orientation: viewportOptions.orientation, viewportId: viewportOptions.viewportId, + presentationIds: viewportOptions.presentationIds, }} displaySetOptions={[{}, pmapDisplaySetOptions]} > diff --git a/extensions/cornerstone-dicom-rt/src/loadRTStruct.js b/extensions/cornerstone-dicom-rt/src/loadRTStruct.js index 10d9cc99f5b..0e9ab38417e 100644 --- a/extensions/cornerstone-dicom-rt/src/loadRTStruct.js +++ b/extensions/cornerstone-dicom-rt/src/loadRTStruct.js @@ -126,6 +126,7 @@ export default async function loadRTStruct(extensionManager, rtStructDisplaySet, SeriesInstanceUID: instance.SeriesInstanceUID, ROIContours: [], visible: true, + ReferencedSOPInstanceUIDsSet: new Set(), }; for (let i = 0; i < ROIContourSequence.length; i++) { @@ -142,7 +143,8 @@ export default async function loadRTStruct(extensionManager, rtStructDisplaySet, const contourPoints = []; for (let c = 0; c < ContourSequenceArray.length; c++) { - const { ContourData, NumberOfContourPoints, ContourGeometricType } = ContourSequenceArray[c]; + const { ContourData, NumberOfContourPoints, ContourGeometricType, ContourImageSequence } = + ContourSequenceArray[c]; let isSupported = false; @@ -172,6 +174,12 @@ export default async function loadRTStruct(extensionManager, rtStructDisplaySet, type: ContourGeometricType, isSupported, }); + + if (ContourImageSequence?.ReferencedSOPInstanceUID) { + structureSet.ReferencedSOPInstanceUIDsSet.add( + ContourImageSequence?.ReferencedSOPInstanceUID + ); + } } _setROIContourMetadata( diff --git a/extensions/cornerstone-dicom-rt/src/viewports/OHIFCornerstoneRTViewport.tsx b/extensions/cornerstone-dicom-rt/src/viewports/OHIFCornerstoneRTViewport.tsx index 1c4a9e0e9b3..662541ff1d9 100644 --- a/extensions/cornerstone-dicom-rt/src/viewports/OHIFCornerstoneRTViewport.tsx +++ b/extensions/cornerstone-dicom-rt/src/viewports/OHIFCornerstoneRTViewport.tsx @@ -6,7 +6,7 @@ import promptHydrateRT from '../utils/promptHydrateRT'; import _getStatusComponent from './_getStatusComponent'; import createRTToolGroupAndAddTools from '../utils/initRTToolGroup'; -import { SegmentationRepresentations } from '@cornerstonejs/tools/enums'; +import { usePositionPresentationStore } from '@ohif/extension-cornerstone'; const RT_TOOLGROUP_BASE_NAME = 'RTToolGroup'; @@ -43,8 +43,8 @@ function OHIFCornerstoneRTViewport(props: withAppTypes) { const [viewportGrid, viewportGridService] = useViewportGrid(); // States - const [isToolGroupCreated, setToolGroupCreated] = useState(false); const [selectedSegment, setSelectedSegment] = useState(1); + const { setPositionPresentation } = usePositionPresentationStore(); // Hydration means that the RT is opened and segments are loaded into the // segmentation panel, and RT is also rendered on any viewport that is in the @@ -123,6 +123,7 @@ function OHIFCornerstoneRTViewport(props: withAppTypes) { toolGroupId: toolGroupId, orientation: viewportOptions.orientation, viewportId: viewportOptions.viewportId, + presentationIds: viewportOptions.presentationIds, }} onElementEnabled={evt => { props.onElementEnabled?.(evt); @@ -185,6 +186,19 @@ function OHIFCornerstoneRTViewport(props: withAppTypes) { setRtIsLoading(false); } + if (rtDisplaySet?.firstSegmentedSliceImageId && viewportOptions?.presentationIds) { + const { firstSegmentedSliceImageId } = rtDisplaySet; + const { presentationIds } = viewportOptions; + + setPositionPresentation(presentationIds.positionPresentationId, { + viewportType: 'stack', + viewReference: { + referencedImageId: firstSegmentedSliceImageId, + }, + viewPresentation: {}, + }); + } + if (evt.overlappingSegments) { uiNotificationService.show({ title: 'Overlapping Segments', @@ -247,8 +261,6 @@ function OHIFCornerstoneRTViewport(props: withAppTypes) { toolGroup = createRTToolGroupAndAddTools(toolGroupService, customizationService, toolGroupId); - setToolGroupCreated(true); - return () => { // remove the segmentation representations if seg displayset changed segmentationService.removeSegmentationRepresentations(viewportId); diff --git a/extensions/cornerstone-dicom-seg/src/viewports/OHIFCornerstoneSEGViewport.tsx b/extensions/cornerstone-dicom-seg/src/viewports/OHIFCornerstoneSEGViewport.tsx index 21dff244fb9..340afad1662 100644 --- a/extensions/cornerstone-dicom-seg/src/viewports/OHIFCornerstoneSEGViewport.tsx +++ b/extensions/cornerstone-dicom-seg/src/viewports/OHIFCornerstoneSEGViewport.tsx @@ -4,6 +4,7 @@ import { LoadingIndicatorTotalPercent, useViewportGrid, ViewportActionArrows } f import createSEGToolGroupAndAddTools from '../utils/initSEGToolGroup'; import promptHydrateSEG from '../utils/promptHydrateSEG'; import _getStatusComponent from './_getStatusComponent'; +import { usePositionPresentationStore } from '@ohif/extension-cornerstone'; import { SegmentationRepresentations } from '@cornerstonejs/tools/enums'; const SEG_TOOLGROUP_BASE_NAME = 'SEGToolGroup'; @@ -41,6 +42,7 @@ function OHIFCornerstoneSEGViewport(props: withAppTypes) { // States const [selectedSegment, setSelectedSegment] = useState(1); + const { setPositionPresentation } = usePositionPresentationStore(); // Hydration means that the SEG is opened and segments are loaded into the // segmentation panel, and SEG is also rendered on any viewport that is in the @@ -198,6 +200,17 @@ function OHIFCornerstoneSEGViewport(props: withAppTypes) { if (evt.segDisplaySet.displaySetInstanceUID === segDisplaySet.displaySetInstanceUID) { setSegIsLoading(false); } + + if (segDisplaySet?.firstSegmentedSliceImageId && viewportOptions?.presentationIds) { + const { firstSegmentedSliceImageId } = segDisplaySet; + const { presentationIds } = viewportOptions; + + setPositionPresentation(presentationIds.positionPresentationId, { + viewReference: { + referencedImageId: firstSegmentedSliceImageId, + }, + }); + } } ); diff --git a/extensions/cornerstone-dicom-sr/src/components/OHIFCornerstoneSRMeasurementViewport.tsx b/extensions/cornerstone-dicom-sr/src/components/OHIFCornerstoneSRMeasurementViewport.tsx index 7c63f346c7c..592ee486324 100644 --- a/extensions/cornerstone-dicom-sr/src/components/OHIFCornerstoneSRMeasurementViewport.tsx +++ b/extensions/cornerstone-dicom-sr/src/components/OHIFCornerstoneSRMeasurementViewport.tsx @@ -9,24 +9,20 @@ import { Icon, Tooltip, useViewportGrid, ViewportActionArrows } from '@ohif/ui'; import hydrateStructuredReport from '../utils/hydrateStructuredReport'; import { useAppConfig } from '@state'; import createReferencedImageDisplaySet from '../utils/createReferencedImageDisplaySet'; +import { usePositionPresentationStore } from '@ohif/extension-cornerstone'; const MEASUREMENT_TRACKING_EXTENSION_ID = '@ohif/extension-measurement-tracking'; const SR_TOOLGROUP_BASE_NAME = 'SRToolGroup'; function OHIFCornerstoneSRMeasurementViewport(props: withAppTypes) { - const { - commandsManager, - children, - dataSource, - displaySets, - viewportOptions, - servicesManager, - extensionManager, - } = props; + const { children, dataSource, displaySets, viewportOptions, servicesManager, extensionManager } = + props; const [appConfig] = useAppConfig(); + const { setPositionPresentation } = usePositionPresentationStore(); + const { displaySetService, cornerstoneViewportService, @@ -157,30 +153,13 @@ function OHIFCornerstoneSRMeasurementViewport(props: withAppTypes) { setActiveImageDisplaySetData(referencedDisplaySet); setReferencedDisplaySetMetadata(referencedDisplaySetMetadata); - if ( - referencedDisplaySet.displaySetInstanceUID === - activeImageDisplaySetData?.displaySetInstanceUID - ) { - const { measurements } = srDisplaySet; - - // it means that we have a new referenced display set, and the - // imageIdIndex will handle it by updating the viewport, but if they - // are the same we just need to use measurementService to jump to the - // new measurement - const csViewport = cornerstoneViewportService.getCornerstoneViewport(viewportId); - - if (!csViewport) { - return; - } - - const imageIds = csViewport.getImageIds(); - - const imageIdIndex = imageIds.indexOf(measurements[newMeasurementSelected].imageId); - - if (imageIdIndex !== -1) { - csViewport.setImageIdIndex(imageIdIndex); - } - } + const { presentationIds } = viewportOptions; + const measurement = srDisplaySet.measurements[newMeasurementSelected]; + setPositionPresentation(presentationIds.positionPresentationId, { + viewReference: { + referencedImageId: measurement.imageId, + }, + }); }); }, [dataSource, srDisplaySet, activeImageDisplaySetData, viewportId] @@ -202,10 +181,6 @@ function OHIFCornerstoneSRMeasurementViewport(props: withAppTypes) { return null; } - const initialImageIndex = activeImageDisplaySetData.images.findIndex( - image => image.imageId === measurement.imageId - ); - return ( ); diff --git a/extensions/cornerstone-dicom-sr/src/utils/formatContentItem.ts b/extensions/cornerstone-dicom-sr/src/utils/formatContentItem.ts index 1446077552a..064a8a2cc78 100644 --- a/extensions/cornerstone-dicom-sr/src/utils/formatContentItem.ts +++ b/extensions/cornerstone-dicom-sr/src/utils/formatContentItem.ts @@ -25,7 +25,7 @@ const contentItemFormatters = { return `${NumericValue} ${CodeValue}`; }, PNAME: contentItem => { - const personName = contentItem.PersonName?.[0]?.Alphabetic; + const personName = contentItem.PersonName?.[0]; return personName ? utils.formatPN(personName) : undefined; }, DATE: contentItem => { diff --git a/extensions/cornerstone-dynamic-volume/src/actions/updateSegmentationsChartDisplaySet.ts b/extensions/cornerstone-dynamic-volume/src/actions/updateSegmentationsChartDisplaySet.ts index 78df25a66b6..7e901804bc9 100644 --- a/extensions/cornerstone-dynamic-volume/src/actions/updateSegmentationsChartDisplaySet.ts +++ b/extensions/cornerstone-dynamic-volume/src/actions/updateSegmentationsChartDisplaySet.ts @@ -262,7 +262,6 @@ function _getInstanceFromSegmentations(segmentations, { servicesManager }) { } function updateSegmentationsChartDisplaySet({ servicesManager }: withAppTypes): void { - debugger; const { segmentationService } = servicesManager.services; const segmentations = segmentationService.getSegmentations(); const { seriesMetadata, instance } = diff --git a/extensions/cornerstone/src/Viewport/Overlays/CustomizableViewportOverlay.tsx b/extensions/cornerstone/src/Viewport/Overlays/CustomizableViewportOverlay.tsx index b459c69cddd..769494891c7 100644 --- a/extensions/cornerstone/src/Viewport/Overlays/CustomizableViewportOverlay.tsx +++ b/extensions/cornerstone/src/Viewport/Overlays/CustomizableViewportOverlay.tsx @@ -5,12 +5,14 @@ import { metaData, Enums, utilities } from '@cornerstonejs/core'; import type { ImageSliceData } from '@cornerstonejs/core/types'; import { ViewportOverlay } from '@ohif/ui'; import type { InstanceMetadata } from '@ohif/core/src/types'; -import { formatPN, formatDICOMDate, formatDICOMTime, formatNumberPrecision } from './utils'; +import { formatDICOMDate, formatDICOMTime, formatNumberPrecision } from './utils'; +import { utils } from '@ohif/core'; import { StackViewportData, VolumeViewportData } from '../../types/CornerstoneCacheService'; import './CustomizableViewportOverlay.css'; const EPSILON = 1e-4; +const { formatPN } = utils; type ViewportData = StackViewportData | VolumeViewportData; diff --git a/extensions/cornerstone/src/Viewport/Overlays/utils.ts b/extensions/cornerstone/src/Viewport/Overlays/utils.ts index d8795f3b3b5..cd90a095eb8 100644 --- a/extensions/cornerstone/src/Viewport/Overlays/utils.ts +++ b/extensions/cornerstone/src/Viewport/Overlays/utils.ts @@ -52,31 +52,6 @@ export function formatDICOMTime(time, strFormat = 'HH:mm:ss') { return moment(time, 'HH:mm:ss').format(strFormat); } -/** - * Formats a patient name for display purposes - * - * @param {string} name - * @returns {string} formatted name. - */ -export function formatPN(name) { - if (!name) { - return ''; - } - if (typeof name === 'object') { - name = name.Alphabetic; - if (!name) { - return ''; - } - } - - const cleaned = name - .split('^') - .filter(s => !!s) - .join(', ') - .trim(); - return cleaned === ',' || cleaned === '' ? '' : cleaned; -} - /** * Gets compression type * diff --git a/extensions/cornerstone/src/hps/frameView.ts b/extensions/cornerstone/src/hps/frameView.ts index cf80aa2a168..7990cd0599b 100644 --- a/extensions/cornerstone/src/hps/frameView.ts +++ b/extensions/cornerstone/src/hps/frameView.ts @@ -20,7 +20,7 @@ const frameView: Types.HangingProtocol.Protocol = { }, { attribute: 'isDisplaySetFromUrl', - weight: 10, + weight: 20, constraint: { equals: true, }, diff --git a/extensions/cornerstone/src/panels/PanelSegmentation.tsx b/extensions/cornerstone/src/panels/PanelSegmentation.tsx index 03109fc5c73..83bbc8bfb0a 100644 --- a/extensions/cornerstone/src/panels/PanelSegmentation.tsx +++ b/extensions/cornerstone/src/panels/PanelSegmentation.tsx @@ -162,6 +162,14 @@ export default function PanelSegmentation({ const firstImageId = referencedImageIds[0]; const instance = metaData.get('instance', firstImageId); + + if (!instance) { + return { + segmentationId, + isExportable: false, + }; + } + const { SOPInstanceUID, SeriesInstanceUID } = instance; const displaySet = displaySetService.getDisplaySetForSOPInstanceUID( diff --git a/extensions/cornerstone/src/services/SegmentationService/SegmentationService.ts b/extensions/cornerstone/src/services/SegmentationService/SegmentationService.ts index 45b1dbd1691..c298591d695 100644 --- a/extensions/cornerstone/src/services/SegmentationService/SegmentationService.ts +++ b/extensions/cornerstone/src/services/SegmentationService/SegmentationService.ts @@ -478,14 +478,24 @@ class SegmentationService extends PubSubService { // We should parse the segmentation as separate slices to support overlapping segments. // This parsing should occur in the CornerstoneJS library adapters. // For now, we use the volume returned from the library and chop it here. + let firstSegmentedSliceImageId = null; for (let i = 0; i < derivedSegmentationImages.length; i++) { const voxelManager = derivedSegmentationImages[i] .voxelManager as csTypes.IVoxelManager; const scalarData = voxelManager.getScalarData(); - scalarData.set(volumeScalarData.slice(i * scalarData.length, (i + 1) * scalarData.length)); + const sliceData = volumeScalarData.slice(i * scalarData.length, (i + 1) * scalarData.length); + scalarData.set(sliceData); voxelManager.setScalarData(scalarData); + + // Check if this slice has any non-zero voxels and we haven't found one yet + if (!firstSegmentedSliceImageId && sliceData.some(value => value !== 0)) { + firstSegmentedSliceImageId = derivedSegmentationImages[i].referencedImageId; + } } + // assign the first non zero voxel image id to the segDisplaySet + segDisplaySet.firstSegmentedSliceImageId = firstSegmentedSliceImageId; + this._broadcastEvent(EVENTS.SEGMENTATION_LOADING_COMPLETE, { segmentationId, segDisplaySet, @@ -542,7 +552,19 @@ class SegmentationService extends PubSubService { } const rtDisplaySetUID = rtDisplaySet.displaySetInstanceUID; + const referencedDisplaySet = this.servicesManager.services.displaySetService.getDisplaySetByUID( + rtDisplaySet.referencedDisplaySetInstanceUID + ); + + const referencedImageIdsWithGeometry = Array.from(structureSet.ReferencedSOPInstanceUIDsSet); + + const referencedImageIds = referencedDisplaySet.instances.map(image => image.imageId); + // find the first image id that contains a referenced SOP instance UID + const firstSegmentedSliceImageId = referencedImageIds.find(imageId => + referencedImageIdsWithGeometry.some(referencedId => imageId.includes(referencedId)) + ); + rtDisplaySet.firstSegmentedSliceImageId = firstSegmentedSliceImageId; // Map ROI contours to RT Struct Data const allRTStructData = mapROIContoursToRTStructData(structureSet, rtDisplaySetUID); diff --git a/extensions/cornerstone/src/services/ViewportService/CornerstoneViewportService.ts b/extensions/cornerstone/src/services/ViewportService/CornerstoneViewportService.ts index c07a4de01e2..56d338c48d8 100644 --- a/extensions/cornerstone/src/services/ViewportService/CornerstoneViewportService.ts +++ b/extensions/cornerstone/src/services/ViewportService/CornerstoneViewportService.ts @@ -605,10 +605,6 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi let initialImageIndexToUse = presentations?.positionPresentation?.initialImageIndex ?? initialImageIndex; - if (initialImageIndexToUse === undefined || initialImageIndexToUse === null) { - initialImageIndexToUse = this._getInitialImageIndexForViewport(viewportInfo, imageIds) || 0; - } - const { rotation, flipHorizontal, displayArea } = viewportInfo.getViewportOptions(); const properties = { ...presentations.lutPresentation?.properties }; @@ -637,12 +633,26 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi }); let imageIdsToSet = imageIds; - const res = this._processExtraDisplaySetsForViewport(viewport); - imageIdsToSet = res?.imageIds ?? imageIdsToSet; + const overlayProcessingResult = this._processExtraDisplaySetsForViewport(viewport); + imageIdsToSet = overlayProcessingResult?.imageIds ?? imageIdsToSet; + + const referencedImageId = presentations?.positionPresentation?.viewReference?.referencedImageId; + if (referencedImageId) { + initialImageIndexToUse = imageIdsToSet.indexOf(referencedImageId); + } + + if (initialImageIndexToUse === undefined || initialImageIndexToUse === null) { + initialImageIndexToUse = this._getInitialImageIndexForViewport(viewportInfo, imageIds) || 0; + } return viewport.setStack(imageIdsToSet, initialImageIndexToUse).then(() => { viewport.setProperties({ ...properties }); this.setPresentations(viewport.id, presentations, viewportInfo); + + if (overlayProcessingResult?.addOverlayFn) { + overlayProcessingResult.addOverlayFn(); + } + if (displayArea) { viewport.setDisplayArea(displayArea); } @@ -827,10 +837,14 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi }); // For SEG and RT viewports - this._processExtraDisplaySetsForViewport(viewport); + const { addOverlayFn } = this._processExtraDisplaySetsForViewport(viewport) || {}; await viewport.setVolumes(volumeInputArray); + if (addOverlayFn) { + addOverlayFn(); + } + volumesProperties.forEach(({ properties, volumeId }) => { viewport.setProperties(properties, volumeId); }); @@ -880,8 +894,12 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi segOrRTSOverlayDisplaySet.referencedDisplaySetInstanceUID ); const imageIds = referenceDisplaySet.images.map(image => image.imageId); - this.addOverlayRepresentationForDisplaySet(segOrRTSOverlayDisplaySet, viewport); - return { imageIds }; + + return { + imageIds, + addOverlayFn: () => + this.addOverlayRepresentationForDisplaySet(segOrRTSOverlayDisplaySet, viewport), + }; } private addOverlayRepresentationForDisplaySet( diff --git a/extensions/cornerstone/src/stores/usePositionPresentationStore.ts b/extensions/cornerstone/src/stores/usePositionPresentationStore.ts index 1b06bd8e52d..ac6d94c9f84 100644 --- a/extensions/cornerstone/src/stores/usePositionPresentationStore.ts +++ b/extensions/cornerstone/src/stores/usePositionPresentationStore.ts @@ -4,7 +4,7 @@ import { PositionPresentation } from '../types/Presentation'; import { addUniqueIndex, JOIN_STR } from './presentationUtils'; const PRESENTATION_TYPE_ID = 'positionPresentationId'; -const DEBUG_STORE = true; +const DEBUG_STORE = false; /** * Represents the state and actions for managing position presentations. diff --git a/extensions/default/src/DicomLocalDataSource/index.js b/extensions/default/src/DicomLocalDataSource/index.js index a0ea9fb4ffa..57badc06c68 100644 --- a/extensions/default/src/DicomLocalDataSource/index.js +++ b/extensions/default/src/DicomLocalDataSource/index.js @@ -142,7 +142,9 @@ function createDicomLocalApi(dicomLocalConfig) { study.series.forEach(aSeries => { const { SeriesInstanceUID } = aSeries; - aSeries.instances.forEach(instance => { + const isMultiframe = aSeries.instances[0].NumberOfFrames > 1; + + aSeries.instances.forEach((instance, index) => { const { url: imageId, StudyInstanceUID, @@ -151,22 +153,14 @@ function createDicomLocalApi(dicomLocalConfig) { } = instance; instance.imageId = imageId; - const numberOfFrames = instance.NumberOfFrames || 1; - // Process all frames consistently, whether single or multiframe - for (let i = 0; i < numberOfFrames; i++) { - const frameNumber = i + 1; - const frameImageId = implementation.getImageIdsForInstance({ - instance, - frame: frameNumber, - }); - // Add imageId specific mapping to this data as the URL isn't necessarily WADO-URI. - metadataProvider.addImageIdToUIDs(frameImageId, { - StudyInstanceUID, - SeriesInstanceUID, - SOPInstanceUID, - frameNumber: numberOfFrames > 1 ? frameNumber : undefined, - }); - } + + // Add imageId specific mapping to this data as the URL isn't necessarily WADO-URI. + metadataProvider.addImageIdToUIDs(imageId, { + StudyInstanceUID, + SeriesInstanceUID, + SOPInstanceUID, + frameIndex: isMultiframe ? index : 1, + }); }); DicomMetadataStore._broadcastEvent(EVENTS.INSTANCES_ADDED, { diff --git a/extensions/default/src/Panels/WrappedPanelStudyBrowser.tsx b/extensions/default/src/Panels/WrappedPanelStudyBrowser.tsx index 80480f18114..f3c2e247398 100644 --- a/extensions/default/src/Panels/WrappedPanelStudyBrowser.tsx +++ b/extensions/default/src/Panels/WrappedPanelStudyBrowser.tsx @@ -30,6 +30,7 @@ function WrappedPanelStudyBrowser({ commandsManager, extensionManager, servicesM return ( = new Map(); - private readonly imageUIDsByImageId: Map = new Map(); // Can be used to store custom metadata for a specific type. // For instance, the scaling metadata for PET can be stored here // as type "scalingModule" @@ -25,7 +25,6 @@ class MetadataProvider { // An example would be dicom hosted at some random site. const imageURI = imageIdToURI(imageId); this.imageURIToUIDs.set(imageURI, uids); - this.imageUIDsByImageId.set(imageId, uids); } addCustomMetadata(imageId, type, metadata) { @@ -347,7 +346,7 @@ class MetadataProvider { let patientName; if (PatientName) { - patientName = PatientName.Alphabetic; + patientName = formatPN(PatientName); } metadata = { @@ -462,11 +461,6 @@ class MetadataProvider { } getUIDsFromImageID(imageId) { - const cachedUIDs = this.imageUIDsByImageId.get(imageId); - if (cachedUIDs) { - return cachedUIDs; - } - if (imageId.startsWith('wadors:')) { const strippedImageId = imageId.split('/studies/')[1]; const splitImageId = strippedImageId.split('/'); diff --git a/platform/core/src/utils/combineFrameInstance.ts b/platform/core/src/utils/combineFrameInstance.ts index cf70232cf5d..4202c5b0fd7 100644 --- a/platform/core/src/utils/combineFrameInstance.ts +++ b/platform/core/src/utils/combineFrameInstance.ts @@ -1,4 +1,5 @@ import { vec3 } from 'gl-matrix'; +import { dicomSplit } from './dicomSplit'; /** * Combine the Per instance frame data, the shared frame data @@ -15,24 +16,13 @@ const combineFrameInstance = (frame, instance) => { PerFrameFunctionalGroupsSequence, SharedFunctionalGroupsSequence, NumberOfFrames, - SpacingBetweenSlices, + ImageType, } = instance; + instance.ImageType = dicomSplit(ImageType); + if (PerFrameFunctionalGroupsSequence || NumberOfFrames > 1) { const frameNumber = Number.parseInt(frame || 1); - const shared = SharedFunctionalGroupsSequence - ? Object.values(SharedFunctionalGroupsSequence[0]) - .filter(Boolean) - .map(it => it[0]) - .filter(it => typeof it === 'object') - : []; - - const perFrame = PerFrameFunctionalGroupsSequence - ? Object.values(PerFrameFunctionalGroupsSequence[frameNumber - 1]) - .filter(Boolean) - .map(it => it[0]) - .filter(it => typeof it === 'object') - : []; // this is to fix NM multiframe datasets with position and orientation // information inside DetectorInformationSequence @@ -44,8 +34,12 @@ const combineFrameInstance = (frame, instance) => { let ImagePositionPatientToUse = instance.ImagePositionPatient; if (!instance.ImagePositionPatient && instance.DetectorInformationSequence) { - const imagePositionPatient = instance.DetectorInformationSequence[0].ImagePositionPatient; - const imageOrientationPatient = instance.ImageOrientationPatient; + let imagePositionPatient = instance.DetectorInformationSequence[0].ImagePositionPatient; + let imageOrientationPatient = instance.ImageOrientationPatient; + + imagePositionPatient = imagePositionPatient.map(it => Number(it)); + imageOrientationPatient = imageOrientationPatient.map(it => Number(it)); + const SpacingBetweenSlices = Number(instance.SpacingBetweenSlices); // Calculate the position for the current frame if (imageOrientationPatient && SpacingBetweenSlices) { @@ -73,27 +67,77 @@ const combineFrameInstance = (frame, instance) => { ImagePositionPatientToUse = [position[0], position[1], position[2]]; } } - console.debug('🚀 ~ ImagePositionPatientToUse:', ImagePositionPatientToUse); - const newInstance = Object.assign(instance, { frameNumber: frameNumber }); - - // merge the shared first then the per frame to override - [...shared, ...perFrame].forEach(item => { - Object.entries(item).forEach(([key, value]) => { - newInstance[key] = value; + // Cache the _parentInstance at the top level as a full copy to prevent + // setting values hard. + if (!instance._parentInstance) { + Object.defineProperty(instance, '_parentInstance', { + value: { ...instance }, }); - }); + } + const sharedInstance = createCombinedValue( + instance._parentInstance, + SharedFunctionalGroupsSequence?.[0], + '_shared' + ); + const newInstance = createCombinedValue( + sharedInstance, + PerFrameFunctionalGroupsSequence?.[frameNumber - 1], + frameNumber + ); + + newInstance.ImagePositionPatient = ImagePositionPatientToUse ?? + newInstance.ImagePositionPatient ?? [0, 0, frameNumber]; - // Todo: we should cache this combined instance somewhere, maybe add it - // back to the dicomMetaStore so we don't have to do this again. - return { - ...newInstance, - ImagePositionPatient: ImagePositionPatientToUse ?? - newInstance.ImagePositionPatient ?? [0, 0, frameNumber], - }; + Object.defineProperty(newInstance, 'frameNumber', { + value: frameNumber, + writable: true, + enumerable: true, + configurable: true, + }); + return newInstance; } else { return instance; } }; +/** + * Creates a combined instance stored in the parent object which + * inherits from the parent instance the attributes in the functional groups. + * The storage key in the parent is in key + */ +function createCombinedValue(parent, functionalGroups, key) { + if (parent[key]) { + return parent[key]; + } + // Exclude any proxying values + const newInstance = Object.create(parent); + Object.defineProperty(parent, key, { + value: newInstance, + writable: false, + enumerable: false, + }); + if (!functionalGroups) { + return newInstance; + } + const shared = functionalGroups + ? Object.values(functionalGroups) + .filter(Boolean) + .map(it => it[0]) + .filter(it => typeof it === 'object') + : []; + + // merge the shared first then the per frame to override + [...shared].forEach(item => { + if (item.SOPInstanceUID) { + // This sub-item is a previous value information item, so don't merge it + return; + } + Object.entries(item).forEach(([key, value]) => { + newInstance[key] = value; + }); + }); + return newInstance; +} + export default combineFrameInstance; diff --git a/platform/core/src/utils/dicomSplit.ts b/platform/core/src/utils/dicomSplit.ts new file mode 100644 index 00000000000..54b20d38f0d --- /dev/null +++ b/platform/core/src/utils/dicomSplit.ts @@ -0,0 +1,5 @@ +export function dicomSplit(value) { + return ( + (Array.isArray(value) && value) || (typeof value === 'string' && value.split('\\')) || value + ); +} diff --git a/platform/core/src/utils/downloadCSVReport.js b/platform/core/src/utils/downloadCSVReport.js index 4185a791986..70adf9f3b21 100644 --- a/platform/core/src/utils/downloadCSVReport.js +++ b/platform/core/src/utils/downloadCSVReport.js @@ -1,4 +1,5 @@ import { DicomMetadataStore } from '../services/DicomMetadataStore/DicomMetadataStore'; +import formatPN from './formatPN'; export default function downloadCSVReport(measurementData) { if (measurementData.length === 0) { @@ -86,7 +87,7 @@ function _getCommonRowItems(measurement, seriesMetadata) { return { 'Patient ID': firstInstance.PatientID, // Patient ID - 'Patient Name': firstInstance.PatientName?.Alphabetic || '', // Patient Name + 'Patient Name': formatPN(firstInstance.PatientName) || '', // Patient Name StudyInstanceUID: measurement.referenceStudyUID, // StudyInstanceUID SeriesInstanceUID: measurement.referenceSeriesUID, // SeriesInstanceUID SOPInstanceUID: measurement.SOPInstanceUID, // SOPInstanceUID diff --git a/platform/ui/src/components/HeaderPatientInfo/HeaderPatientInfo.tsx b/platform/ui/src/components/HeaderPatientInfo/HeaderPatientInfo.tsx index 7e7de4d2383..e62f07793ab 100644 --- a/platform/ui/src/components/HeaderPatientInfo/HeaderPatientInfo.tsx +++ b/platform/ui/src/components/HeaderPatientInfo/HeaderPatientInfo.tsx @@ -48,7 +48,7 @@ function usePatientInfo(servicesManager: AppTypes.ServicesManager) { } setPatientInfo({ PatientID: instance.PatientID || null, - PatientName: instance.PatientName ? formatPN(instance.PatientName.Alphabetic) : null, + PatientName: instance.PatientName ? formatPN(instance.PatientName) : null, PatientSex: instance.PatientSex || null, PatientDOB: formatDate(instance.PatientBirthDate) || null, }); diff --git a/version.txt b/version.txt index 4764627f925..03d70eaebc4 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -3.9.2 \ No newline at end of file +3.9.3 \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 737d8ac837a..85a2a60ae2f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12594,13 +12594,6 @@ fast-uri@^3.0.1: resolved "https://registry.yarnpkg.com/fast-uri/-/fast-uri-3.0.2.tgz#d78b298cf70fd3b752fd951175a3da6a7b48f024" integrity sha512-GR6f0hD7XXyNJa25Tb9BuIdN0tdr+0BMi6/CJPH3wJO1JjNG3n/VsSw38AwRdKZABm8lGbPfakLRkYzx2V9row== -fast-url-parser@1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/fast-url-parser/-/fast-url-parser-1.1.3.tgz#f4af3ea9f34d8a271cf58ad2b3759f431f0b318d" - integrity sha512-5jOCVXADYNuRkKFzNJ0dCCewsZiYo0dz8QNYljkOpFC6r2U4OBmKtvm/Tsuh4w1YYdDqDb31a8TVhBJ2OJKdqQ== - dependencies: - punycode "^1.3.2" - fastest-levenshtein@^1.0.12: version "1.0.16" resolved "https://registry.yarnpkg.com/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz#210e61b6ff181de91ea9b3d1b84fdedd47e034e5" @@ -19130,7 +19123,7 @@ path-to-regexp@1.9.0, path-to-regexp@^1.7.0: dependencies: isarray "0.0.1" -path-to-regexp@2.2.1, path-to-regexp@3.3.0: +path-to-regexp@3.3.0: version "3.3.0" resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-3.3.0.tgz#f7f31d32e8518c2660862b644414b6d5c63a611b" integrity sha512-qyCH421YQPS2WFDxDjftfc1ZR5WKQzVzqsp4n9M2kQhVOo/ByahFoUNJfl58kOcEGfQ//7weFTDhm+ss8Ecxgw== @@ -20438,7 +20431,7 @@ pumpify@^1.3.3: inherits "^2.0.3" pump "^2.0.0" -punycode@^1.3.2, punycode@^1.4.1: +punycode@^1.4.1: version "1.4.1" resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" integrity sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ== @@ -22034,21 +22027,7 @@ serialize-javascript@^6.0.0, serialize-javascript@^6.0.1: dependencies: randombytes "^2.1.0" -serve-handler@6.1.5: - version "6.1.5" - resolved "https://registry.yarnpkg.com/serve-handler/-/serve-handler-6.1.5.tgz#a4a0964f5c55c7e37a02a633232b6f0d6f068375" - integrity sha512-ijPFle6Hwe8zfmBxJdE+5fta53fdIY0lHISJvuikXB3VYFafRjMRpOffSPvCYsbKyBA7pvy9oYr/BT1O3EArlg== - dependencies: - bytes "3.0.0" - content-disposition "0.5.2" - fast-url-parser "1.1.3" - mime-types "2.1.18" - minimatch "3.1.2" - path-is-inside "1.0.2" - path-to-regexp "2.2.1" - range-parser "1.2.0" - -serve-handler@^6.1.6: +serve-handler@6.1.6, serve-handler@^6.1.6: version "6.1.6" resolved "https://registry.yarnpkg.com/serve-handler/-/serve-handler-6.1.6.tgz#50803c1d3e947cd4a341d617f8209b22bd76cfa1" integrity sha512-x5RL9Y2p5+Sh3D38Fh9i/iQ5ZK+e4xuXRd/pGbM4D13tgo/MGwbttUk8emytcr1YYzBYs+apnUngBDFYfpjPuQ== @@ -22084,10 +22063,10 @@ serve-static@1.16.2: parseurl "~1.3.3" send "0.19.0" -serve@^14.2.0: - version "14.2.3" - resolved "https://registry.yarnpkg.com/serve/-/serve-14.2.3.tgz#047ba2b349354255bc09e0332cd41a92787836c9" - integrity sha512-VqUFMC7K3LDGeGnJM9h56D3XGKb6KGgOw0cVNtA26yYXHCcpxf3xwCTUaQoWlVS7i8Jdh3GjQkOB23qsXyjoyQ== +serve@^14.2.4: + version "14.2.4" + resolved "https://registry.yarnpkg.com/serve/-/serve-14.2.4.tgz#ba4c425c3c965f496703762e808f34b913f42fb0" + integrity sha512-qy1S34PJ/fcY8gjVGszDB3EXiPSk5FKhUa7tQe0UPRddxRidc2V6cNHPNewbE1D7MAkgLuWEt3Vw56vYy73tzQ== dependencies: "@zeit/schemas" "2.36.0" ajv "8.12.0" @@ -22098,7 +22077,7 @@ serve@^14.2.0: clipboardy "3.0.0" compression "1.7.4" is-port-reachable "4.0.0" - serve-handler "6.1.5" + serve-handler "6.1.6" update-check "1.5.4" set-blocking@^2.0.0: