From e1f55e65f2d2a34136ad5d0b1ada77d337a0ea23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20H=2E=20K=C3=B6hler?= Date: Wed, 13 Dec 2023 16:21:46 -0300 Subject: [PATCH] feat(customizationService): Enable saving and loading of private tags in SRs (#3842) --- .../src/commandsModule.js | 14 ++++++- .../src/getSopClassHandlerModule.ts | 37 ++++++++++++++-- .../src/tools/DICOMSRDisplayTool.ts | 4 +- .../src/utils/addMeasurement.ts | 2 +- .../src/utils/hydrateStructuredReport.js | 12 +++++- .../utils/measurementServiceMappings/Angle.ts | 3 +- .../ArrowAnnotate.ts | 3 +- .../Bidirectional.ts | 3 +- .../measurementServiceMappings/CircleROI.ts | 3 +- .../measurementServiceMappings/CobbAngle.ts | 3 +- .../EllipticalROI.ts | 3 +- .../measurementServiceMappings/Length.ts | 3 +- .../PlanarFreehandROI.ts | 3 +- .../RectangleROI.ts | 3 +- .../default/src/DicomWebDataSource/index.js | 42 ++++++++++++------- .../src/DicomWebProxyDataSource/index.js | 4 +- .../default/src/MergeDataSource/index.ts | 2 +- .../core/src/extensions/ExtensionManager.ts | 3 +- .../MeasurementService/MeasurementService.ts | 1 + 19 files changed, 110 insertions(+), 38 deletions(-) diff --git a/extensions/cornerstone-dicom-sr/src/commandsModule.js b/extensions/cornerstone-dicom-sr/src/commandsModule.js index ec68cabb9..33e8a869f 100644 --- a/extensions/cornerstone-dicom-sr/src/commandsModule.js +++ b/extensions/cornerstone-dicom-sr/src/commandsModule.js @@ -40,7 +40,9 @@ const _generateReport = (measurementData, additionalFindingTypes, options = {}) return dataset; }; -const commandsModule = ({}) => { +const commandsModule = props => { + const { servicesManager } = props; + const { customizationService } = servicesManager.services; const actions = { /** * @@ -95,7 +97,15 @@ const commandsModule = ({}) => { throw new Error('Invalid report, no content'); } - await dataSource.store.dicom(naturalizedReport); + const onBeforeDicomStore = + customizationService.getModeCustomization('onBeforeDicomStore')?.value; + + let dicomDict; + if (typeof onBeforeDicomStore === 'function') { + dicomDict = onBeforeDicomStore({ measurementData, naturalizedReport }); + } + + await dataSource.store.dicom(naturalizedReport, null, dicomDict); if (StudyInstanceUID) { dataSource.deleteStudyMetadataPromise(StudyInstanceUID); diff --git a/extensions/cornerstone-dicom-sr/src/getSopClassHandlerModule.ts b/extensions/cornerstone-dicom-sr/src/getSopClassHandlerModule.ts index d89849fdf..fbeaa5316 100644 --- a/extensions/cornerstone-dicom-sr/src/getSopClassHandlerModule.ts +++ b/extensions/cornerstone-dicom-sr/src/getSopClassHandlerModule.ts @@ -171,7 +171,12 @@ function _load(displaySet, servicesManager, extensionManager) { // Check currently added displaySets and add measurements if the sources exist. displaySetService.activeDisplaySets.forEach(activeDisplaySet => { - _checkIfCanAddMeasurementsToDisplaySet(displaySet, activeDisplaySet, dataSource); + _checkIfCanAddMeasurementsToDisplaySet( + displaySet, + activeDisplaySet, + dataSource, + servicesManager + ); }); // Subscribe to new displaySets as the source may come in after. @@ -180,12 +185,23 @@ function _load(displaySet, servicesManager, extensionManager) { // If there are still some measurements that have not yet been loaded into cornerstone, // See if we can load them onto any of the new displaySets. displaySetsAdded.forEach(newDisplaySet => { - _checkIfCanAddMeasurementsToDisplaySet(displaySet, newDisplaySet, dataSource); + _checkIfCanAddMeasurementsToDisplaySet( + displaySet, + newDisplaySet, + dataSource, + servicesManager + ); }); }); } -function _checkIfCanAddMeasurementsToDisplaySet(srDisplaySet, newDisplaySet, dataSource) { +function _checkIfCanAddMeasurementsToDisplaySet( + srDisplaySet, + newDisplaySet, + dataSource, + servicesManager +) { + const { customizationService } = servicesManager.services; let unloadedMeasurements = srDisplaySet.measurements.filter( measurement => measurement.loaded === false ); @@ -244,7 +260,20 @@ function _checkIfCanAddMeasurementsToDisplaySet(srDisplaySet, newDisplaySet, dat if (SOPInstanceUIDs.includes(SOPInstanceUID)) { for (let j = unloadedMeasurements.length - 1; j >= 0; j--) { - const measurement = unloadedMeasurements[j]; + let measurement = unloadedMeasurements[j]; + + const onBeforeSRAddMeasurement = customizationService.getModeCustomization( + 'onBeforeSRAddMeasurement' + )?.value; + + if (typeof onBeforeSRAddMeasurement === 'function') { + measurement = onBeforeSRAddMeasurement({ + measurement, + StudyInstanceUID: srDisplaySet.StudyInstanceUID, + SeriesInstanceUID: srDisplaySet.SeriesInstanceUID, + }); + } + if (_measurementReferencesSOPInstanceUID(measurement, SOPInstanceUID, frameNumber)) { addMeasurement(measurement, imageId, newDisplaySet.displaySetInstanceUID); diff --git a/extensions/cornerstone-dicom-sr/src/tools/DICOMSRDisplayTool.ts b/extensions/cornerstone-dicom-sr/src/tools/DICOMSRDisplayTool.ts index 8160ea650..7b14b107f 100644 --- a/extensions/cornerstone-dicom-sr/src/tools/DICOMSRDisplayTool.ts +++ b/extensions/cornerstone-dicom-sr/src/tools/DICOMSRDisplayTool.ts @@ -339,7 +339,9 @@ export default class DICOMSRDisplayTool extends AnnotationTool { const textLines = this._getTextBoxLinesFromLabels(label); const canvasTextBoxCoords = utilities.drawing.getTextBoxCoordsCanvas(adaptedCanvasCoordinates); - annotation.data.handles.textBox.worldPosition = viewport.canvasToWorld(canvasTextBoxCoords); + if (!annotation.data?.handles?.textBox?.worldPosition) { + annotation.data.handles.textBox.worldPosition = viewport.canvasToWorld(canvasTextBoxCoords); + } const textBoxPosition = viewport.worldToCanvas(annotation.data.handles.textBox.worldPosition); diff --git a/extensions/cornerstone-dicom-sr/src/utils/addMeasurement.ts b/extensions/cornerstone-dicom-sr/src/utils/addMeasurement.ts index e78b3c7f1..348a7a79d 100644 --- a/extensions/cornerstone-dicom-sr/src/utils/addMeasurement.ts +++ b/extensions/cornerstone-dicom-sr/src/utils/addMeasurement.ts @@ -52,7 +52,7 @@ export default function addMeasurement(measurement, imageId, displaySetInstanceU data: { label: measurement.labels, handles: { - textBox: {}, + textBox: measurement.textBox ?? {}, }, cachedStats: { TrackingUniqueIdentifier: measurementData.TrackingUniqueIdentifier, diff --git a/extensions/cornerstone-dicom-sr/src/utils/hydrateStructuredReport.js b/extensions/cornerstone-dicom-sr/src/utils/hydrateStructuredReport.js index 17216a844..7dc9e657a 100644 --- a/extensions/cornerstone-dicom-sr/src/utils/hydrateStructuredReport.js +++ b/extensions/cornerstone-dicom-sr/src/utils/hydrateStructuredReport.js @@ -90,7 +90,7 @@ export default function hydrateStructuredReport( const datasetToUse = _mapLegacyDataSet(instance); // Use dcmjs to generate toolState. - const storedMeasurementByAnnotationType = MeasurementReport.generateToolState( + let storedMeasurementByAnnotationType = MeasurementReport.generateToolState( datasetToUse, // NOTE: we need to pass in the imageIds to dcmjs since the we use them // for the imageToWorld transformation. The following assumes that the order @@ -101,6 +101,16 @@ export default function hydrateStructuredReport( metaData ); + const onBeforeSRHydration = + customizationService.getModeCustomization('onBeforeSRHydration')?.value; + + if (typeof onBeforeSRHydration === 'function') { + storedMeasurementByAnnotationType = onBeforeSRHydration({ + storedMeasurementByAnnotationType, + displaySet, + }); + } + // Filter what is found by DICOM SR to measurements we support. const mappingDefinitions = mappings.map(m => m.annotationType); const hydratableMeasurementsInSR = {}; diff --git a/extensions/cornerstone/src/utils/measurementServiceMappings/Angle.ts b/extensions/cornerstone/src/utils/measurementServiceMappings/Angle.ts index 5708582a4..7173e1344 100644 --- a/extensions/cornerstone/src/utils/measurementServiceMappings/Angle.ts +++ b/extensions/cornerstone/src/utils/measurementServiceMappings/Angle.ts @@ -50,7 +50,7 @@ const Angle = { displaySet = displaySetService.getDisplaySetsForSeries(SeriesInstanceUID); } - const { points } = data.handles; + const { points, textBox } = data.handles; const mappedAnnotations = getMappedAnnotations(annotation, displaySetService); @@ -62,6 +62,7 @@ const Angle = { SOPInstanceUID, FrameOfReferenceUID, points, + textBox, metadata, referenceSeriesUID: SeriesInstanceUID, referenceStudyUID: StudyInstanceUID, diff --git a/extensions/cornerstone/src/utils/measurementServiceMappings/ArrowAnnotate.ts b/extensions/cornerstone/src/utils/measurementServiceMappings/ArrowAnnotate.ts index 9ebb12079..3d5904171 100644 --- a/extensions/cornerstone/src/utils/measurementServiceMappings/ArrowAnnotate.ts +++ b/extensions/cornerstone/src/utils/measurementServiceMappings/ArrowAnnotate.ts @@ -48,7 +48,7 @@ const Length = { displaySet = displaySetService.getDisplaySetsForSeries(SeriesInstanceUID); } - const { points } = data.handles; + const { points, textBox } = data.handles; const mappedAnnotations = getMappedAnnotations(annotation, displaySetService); @@ -59,6 +59,7 @@ const Length = { SOPInstanceUID, FrameOfReferenceUID, points, + textBox, metadata, referenceSeriesUID: SeriesInstanceUID, referenceStudyUID: StudyInstanceUID, diff --git a/extensions/cornerstone/src/utils/measurementServiceMappings/Bidirectional.ts b/extensions/cornerstone/src/utils/measurementServiceMappings/Bidirectional.ts index 11e1c7c02..958e22ed6 100644 --- a/extensions/cornerstone/src/utils/measurementServiceMappings/Bidirectional.ts +++ b/extensions/cornerstone/src/utils/measurementServiceMappings/Bidirectional.ts @@ -45,7 +45,7 @@ const Bidirectional = { displaySet = displaySetService.getDisplaySetsForSeries(SeriesInstanceUID); } - const { points } = data.handles; + const { points, textBox } = data.handles; const mappedAnnotations = getMappedAnnotations(annotation, displaySetService); @@ -57,6 +57,7 @@ const Bidirectional = { SOPInstanceUID, FrameOfReferenceUID, points, + textBox, metadata, referenceSeriesUID: SeriesInstanceUID, referenceStudyUID: StudyInstanceUID, diff --git a/extensions/cornerstone/src/utils/measurementServiceMappings/CircleROI.ts b/extensions/cornerstone/src/utils/measurementServiceMappings/CircleROI.ts index 6ec79919b..e65564c81 100644 --- a/extensions/cornerstone/src/utils/measurementServiceMappings/CircleROI.ts +++ b/extensions/cornerstone/src/utils/measurementServiceMappings/CircleROI.ts @@ -43,7 +43,7 @@ const CircleROI = { displaySet = DisplaySetService.getDisplaySetsForSeries(SeriesInstanceUID); } - const { points } = data.handles; + const { points, textBox } = data.handles; const mappedAnnotations = getMappedAnnotations(annotation, DisplaySetService); @@ -55,6 +55,7 @@ const CircleROI = { SOPInstanceUID, FrameOfReferenceUID, points, + textBox, metadata, referenceSeriesUID: SeriesInstanceUID, referenceStudyUID: StudyInstanceUID, diff --git a/extensions/cornerstone/src/utils/measurementServiceMappings/CobbAngle.ts b/extensions/cornerstone/src/utils/measurementServiceMappings/CobbAngle.ts index f61356c29..dbde82860 100644 --- a/extensions/cornerstone/src/utils/measurementServiceMappings/CobbAngle.ts +++ b/extensions/cornerstone/src/utils/measurementServiceMappings/CobbAngle.ts @@ -50,7 +50,7 @@ const CobbAngle = { displaySet = displaySetService.getDisplaySetsForSeries(SeriesInstanceUID); } - const { points } = data.handles; + const { points, textBox } = data.handles; const mappedAnnotations = getMappedAnnotations(annotation, displaySetService); @@ -62,6 +62,7 @@ const CobbAngle = { SOPInstanceUID, FrameOfReferenceUID, points, + textBox, metadata, referenceSeriesUID: SeriesInstanceUID, referenceStudyUID: StudyInstanceUID, diff --git a/extensions/cornerstone/src/utils/measurementServiceMappings/EllipticalROI.ts b/extensions/cornerstone/src/utils/measurementServiceMappings/EllipticalROI.ts index a38b6f835..1882d9ba0 100644 --- a/extensions/cornerstone/src/utils/measurementServiceMappings/EllipticalROI.ts +++ b/extensions/cornerstone/src/utils/measurementServiceMappings/EllipticalROI.ts @@ -43,7 +43,7 @@ const EllipticalROI = { displaySet = displaySetService.getDisplaySetsForSeries(SeriesInstanceUID); } - const { points } = data.handles; + const { points, textBox } = data.handles; const mappedAnnotations = getMappedAnnotations(annotation, displaySetService); @@ -55,6 +55,7 @@ const EllipticalROI = { SOPInstanceUID, FrameOfReferenceUID, points, + textBox, metadata, referenceSeriesUID: SeriesInstanceUID, referenceStudyUID: StudyInstanceUID, diff --git a/extensions/cornerstone/src/utils/measurementServiceMappings/Length.ts b/extensions/cornerstone/src/utils/measurementServiceMappings/Length.ts index 5c5072d2c..e5e9886b3 100644 --- a/extensions/cornerstone/src/utils/measurementServiceMappings/Length.ts +++ b/extensions/cornerstone/src/utils/measurementServiceMappings/Length.ts @@ -53,7 +53,7 @@ const Length = { displaySet = displaySetService.getDisplaySetsForSeries(SeriesInstanceUID); } - const { points } = data.handles; + const { points, textBox } = data.handles; const mappedAnnotations = getMappedAnnotations(annotation, displaySetService); @@ -65,6 +65,7 @@ const Length = { SOPInstanceUID, FrameOfReferenceUID, points, + textBox, metadata, referenceSeriesUID: SeriesInstanceUID, referenceStudyUID: StudyInstanceUID, diff --git a/extensions/cornerstone/src/utils/measurementServiceMappings/PlanarFreehandROI.ts b/extensions/cornerstone/src/utils/measurementServiceMappings/PlanarFreehandROI.ts index b6db513b6..d1322e484 100644 --- a/extensions/cornerstone/src/utils/measurementServiceMappings/PlanarFreehandROI.ts +++ b/extensions/cornerstone/src/utils/measurementServiceMappings/PlanarFreehandROI.ts @@ -48,7 +48,7 @@ const PlanarFreehandROI = { displaySet = DisplaySetService.getDisplaySetsForSeries(SeriesInstanceUID); } - const { points } = data.handles; + const { points, textBox } = data.handles; const mappedAnnotations = getMappedAnnotations(annotation, DisplaySetService); @@ -60,6 +60,7 @@ const PlanarFreehandROI = { SOPInstanceUID, FrameOfReferenceUID, points, + textBox, metadata, referenceSeriesUID: SeriesInstanceUID, referenceStudyUID: StudyInstanceUID, diff --git a/extensions/cornerstone/src/utils/measurementServiceMappings/RectangleROI.ts b/extensions/cornerstone/src/utils/measurementServiceMappings/RectangleROI.ts index c7cdb3e41..6c7e4b4ca 100644 --- a/extensions/cornerstone/src/utils/measurementServiceMappings/RectangleROI.ts +++ b/extensions/cornerstone/src/utils/measurementServiceMappings/RectangleROI.ts @@ -43,7 +43,7 @@ const RectangleROI = { displaySet = DisplaySetService.getDisplaySetsForSeries(SeriesInstanceUID); } - const { points } = data.handles; + const { points, textBox } = data.handles; const mappedAnnotations = getMappedAnnotations(annotation, DisplaySetService); @@ -55,6 +55,7 @@ const RectangleROI = { SOPInstanceUID, FrameOfReferenceUID, points, + textBox, metadata, referenceSeriesUID: SeriesInstanceUID, referenceStudyUID: StudyInstanceUID, diff --git a/extensions/default/src/DicomWebDataSource/index.js b/extensions/default/src/DicomWebDataSource/index.js index 2d26a90b8..2ae44dc7e 100644 --- a/extensions/default/src/DicomWebDataSource/index.js +++ b/extensions/default/src/DicomWebDataSource/index.js @@ -40,7 +40,8 @@ const metadataProvider = classes.MetadataProvider; * @param {bool} lazyLoadStudy - "enableStudyLazyLoad"; Request series meta async instead of blocking * @param {string|bool} singlepart - indicates of the retrieves can fetch singlepart. Options are bulkdata, video, image or boolean true */ -function createDicomWebApi(dicomWebConfig, userAuthenticationService) { +function createDicomWebApi(dicomWebConfig, servicesManager) { + const { userAuthenticationService, customizationService } = servicesManager.services; let dicomWebConfigCopy, qidoConfig, wadoConfig, @@ -140,7 +141,13 @@ function createDicomWebApi(dicomWebConfig, userAuthenticationService) { instances: { search: (studyInstanceUid, queryParameters) => { qidoDicomWebClient.headers = getAuthrorizationHeader(); - return qidoSearch.call(undefined, qidoDicomWebClient, studyInstanceUid, null, queryParameters); + return qidoSearch.call( + undefined, + qidoDicomWebClient, + studyInstanceUid, + null, + queryParameters + ); }, }, }, @@ -211,7 +218,7 @@ function createDicomWebApi(dicomWebConfig, userAuthenticationService) { }, store: { - dicom: async (dataset, request) => { + dicom: async (dataset, request, dicomDict) => { wadoDicomWebClient.headers = getAuthrorizationHeader(); if (dataset instanceof ArrayBuffer) { const options = { @@ -220,21 +227,26 @@ function createDicomWebApi(dicomWebConfig, userAuthenticationService) { }; await wadoDicomWebClient.storeInstances(options); } else { - const meta = { - FileMetaInformationVersion: dataset._meta?.FileMetaInformationVersion?.Value, - MediaStorageSOPClassUID: dataset.SOPClassUID, - MediaStorageSOPInstanceUID: dataset.SOPInstanceUID, - TransferSyntaxUID: EXPLICIT_VR_LITTLE_ENDIAN, - ImplementationClassUID, - ImplementationVersionName, - }; + let effectiveDicomDict = dicomDict; - const denaturalized = denaturalizeDataset(meta); - const dicomDict = new DicomDict(denaturalized); + if (!dicomDict) { + const meta = { + FileMetaInformationVersion: dataset._meta?.FileMetaInformationVersion?.Value, + MediaStorageSOPClassUID: dataset.SOPClassUID, + MediaStorageSOPInstanceUID: dataset.SOPInstanceUID, + TransferSyntaxUID: EXPLICIT_VR_LITTLE_ENDIAN, + ImplementationClassUID, + ImplementationVersionName, + }; - dicomDict.dict = denaturalizeDataset(dataset); + const denaturalized = denaturalizeDataset(meta); + const defaultDicomDict = new DicomDict(denaturalized); + defaultDicomDict.dict = denaturalizeDataset(dataset); - const part10Buffer = dicomDict.write(); + effectiveDicomDict = defaultDicomDict; + } + + const part10Buffer = effectiveDicomDict.write(); const options = { datasets: [part10Buffer], diff --git a/extensions/default/src/DicomWebProxyDataSource/index.js b/extensions/default/src/DicomWebProxyDataSource/index.js index ab1d8e49e..99007de3e 100644 --- a/extensions/default/src/DicomWebProxyDataSource/index.js +++ b/extensions/default/src/DicomWebProxyDataSource/index.js @@ -9,7 +9,7 @@ import { createDicomWebApi } from '../DicomWebDataSource/index'; * dicomWeb configuration array * */ -function createDicomWebProxyApi(dicomWebProxyConfig, UserAuthenticationService) { +function createDicomWebProxyApi(dicomWebProxyConfig, servicesManager) { const { name } = dicomWebProxyConfig; let dicomWebDelegate = undefined; @@ -28,7 +28,7 @@ function createDicomWebProxyApi(dicomWebProxyConfig, UserAuthenticationService) dicomWebDelegate = createDicomWebApi( data.servers.dicomWeb[0].configuration, - UserAuthenticationService + servicesManager ); dicomWebDelegate.initialize({ params, query }); } diff --git a/extensions/default/src/MergeDataSource/index.ts b/extensions/default/src/MergeDataSource/index.ts index 7143ee894..1c14069b6 100644 --- a/extensions/default/src/MergeDataSource/index.ts +++ b/extensions/default/src/MergeDataSource/index.ts @@ -139,7 +139,7 @@ export const callByRetrieveAETitle = ({ function createMergeDataSourceApi( mergeConfig: MergeConfig, - UserAuthenticationService: unknown, + servicesManager: unknown, extensionManager ) { const { seriesMerge } = mergeConfig; diff --git a/platform/core/src/extensions/ExtensionManager.ts b/platform/core/src/extensions/ExtensionManager.ts index 5b2e4b560..083fde7f2 100644 --- a/platform/core/src/extensions/ExtensionManager.ts +++ b/platform/core/src/extensions/ExtensionManager.ts @@ -461,10 +461,9 @@ export default class ExtensionManager extends PubSubService { this.dataSourceDefs[dataSourceDef.sourceName] = dataSourceDef; - const { userAuthenticationService } = this._servicesManager.services; const dataSourceInstance = module.createDataSource( dataSourceDef.configuration, - userAuthenticationService, + this._servicesManager, this ); diff --git a/platform/core/src/services/MeasurementService/MeasurementService.ts b/platform/core/src/services/MeasurementService/MeasurementService.ts index 30228a251..7d253ecff 100644 --- a/platform/core/src/services/MeasurementService/MeasurementService.ts +++ b/platform/core/src/services/MeasurementService/MeasurementService.ts @@ -58,6 +58,7 @@ const MEASUREMENT_SCHEMA_KEYS = [ 'longestDiameter', 'cachedStats', 'selected', + 'textBox', ]; const EVENTS = {