diff --git a/extensions/cornerstone-dicom-sr/src/getSopClassHandlerModule.ts b/extensions/cornerstone-dicom-sr/src/getSopClassHandlerModule.ts index 0fc3d3fb951..e2440fd1990 100644 --- a/extensions/cornerstone-dicom-sr/src/getSopClassHandlerModule.ts +++ b/extensions/cornerstone-dicom-sr/src/getSopClassHandlerModule.ts @@ -22,7 +22,7 @@ const { CodeScheme: Cornerstone3DCodeScheme, } = adaptersSR.Cornerstone3D; -type InstanceMetadata = Types.InstanceMetadata; +type InstanceMetadata = OhifTypes.InstanceMetadata; /** * TODO @@ -69,9 +69,10 @@ function addInstances(instances: InstanceMetadata[], _displaySetService: Display * DICOM SR SOP Class Handler * For all referenced images in the TID 1500/300 sections, add an image to the * display. - * @param instances is a set of instances all from the same series - * @param servicesManager is the services that can be used for creating - * @returns The list of display sets created for the given instances object + * @param {InstanceMetadata[]} instances - A set of instances all from the same series + * @param {AppTypes.ServicesManager} servicesManager - The services that can be used for creating + * @param {AppTypes.ExtensionManager} extensionManager - The extension manager + * @returns {Types.DisplaySet[]} The list of display sets created for the given instances object */ function _getDisplaySetsFromSeries( instances, @@ -143,7 +144,7 @@ function _getDisplaySetsFromSeries( * @param extensionManager - The extension manager containing data sources. */ async function _load( - srDisplaySet: Types.DisplaySet, + srDisplaySet: OhifTypes.DisplaySet, servicesManager: AppTypes.ServicesManager, extensionManager: AppTypes.ExtensionManager ) { @@ -227,17 +228,9 @@ function _measurementBelongsToDisplaySet({ measurement, displaySet }) { ); } -/** - * Checks if measurements can be added to a display set. - * - * @param srDisplaySet - The source display set containing measurements. - * @param newDisplaySet - The new display set to check if measurements can be added. - * @param dataSource - The data source used to retrieve image IDs. - * @param servicesManager - The services manager. - */ function _checkIfCanAddMeasurementsToDisplaySet( - srDisplaySet, - newDisplaySet, + srDisplaySet: OhifTypes.DisplaySet, + newDisplaySet: OhifTypes.DisplaySet, dataSource, servicesManager: AppTypes.ServicesManager ) { @@ -289,9 +282,7 @@ function _checkIfCanAddMeasurementsToDisplaySet( is3DMeasurement && _measurementBelongsToDisplaySet({ measurement, displaySet: newDisplaySet }) ) { - _measurementBelongsToDisplaySet({ measurement, displaySet: newDisplaySet }) - - addSRAnnotation(measurement, null, null); + addSRAnnotation({ measurement, displaySet: newDisplaySet }); measurement.loaded = true; measurement.displaySetInstanceUID = newDisplaySet.displaySetInstanceUID; unloadedMeasurements.splice(j, 1); @@ -312,15 +303,12 @@ function _checkIfCanAddMeasurementsToDisplaySet( imageId && _measurementReferencesSOPInstanceUID(measurement, ReferencedSOPInstanceUID, frame) ) { - addSRAnnotation(measurement, imageId, frame); - - // Update measurement properties + addSRAnnotation({ measurement, imageId, frameNumber: frame, displaySet: newDisplaySet }); measurement.loaded = true; measurement.imageId = imageId; measurement.displaySetInstanceUID = newDisplaySet.displaySetInstanceUID; measurement.ReferencedSOPInstanceUID = ReferencedSOPInstanceUID; measurement.frameNumber = frame; - unloadedMeasurements.splice(j, 1); } } @@ -328,10 +316,10 @@ function _checkIfCanAddMeasurementsToDisplaySet( /** * Checks if a measurement references a specific SOP Instance UID. - * @param measurement - The measurement object. - * @param SOPInstanceUID - The SOP Instance UID to check against. - * @param frameNumber - The frame number to check against (optional). - * @returns True if the measurement references the specified SOP Instance UID, false otherwise. + * @param {any} measurement - The measurement object. + * @param {string} sopInstanceUID - The SOP Instance UID to check against. + * @param {number} frameNumber - The frame number to check against (optional). + * @returns {boolean} True if the measurement references the specified SOP Instance UID, false otherwise. */ function _measurementReferencesSOPInstanceUID(measurement, SOPInstanceUID, frameNumber) { const { coords } = measurement; @@ -363,10 +351,8 @@ function _measurementReferencesSOPInstanceUID(measurement, SOPInstanceUID, frame /** * Retrieves the SOP class handler module. * - * @param {Object} options - The options for retrieving the SOP class handler module. - * @param {Object} options.servicesManager - The services manager. - * @param {Object} options.extensionManager - The extension manager. - * @returns {Array} An array containing the SOP class handler module. + * @param {OhifTypes.Extensions.ExtensionParams} params - The extension parameters. + * @returns {Array} An array containing the SOP class handler modules. */ function getSopClassHandlerModule(params: OhifTypes.Extensions.ExtensionParams) { const { servicesManager, extensionManager } = params; @@ -390,8 +376,8 @@ function getSopClassHandlerModule(params: OhifTypes.Extensions.ExtensionParams) /** * Retrieves the measurements from the ImagingMeasurementReportContentSequence. * - * @param {Array} ImagingMeasurementReportContentSequence - The ImagingMeasurementReportContentSequence array. - * @returns {Array} - The array of measurements. + * @param {any[]} imagingMeasurementReportContentSequence - The ImagingMeasurementReportContentSequence array. + * @returns {any[]} The array of measurements. */ function _getMeasurements(ImagingMeasurementReportContentSequence) { const ImagingMeasurements = ImagingMeasurementReportContentSequence.find( @@ -429,8 +415,8 @@ function _getMeasurements(ImagingMeasurementReportContentSequence) { /** * Retrieves merged content sequences by tracking unique identifiers. * - * @param {Array} MeasurementGroups - The measurement groups. - * @returns {Object} - The merged content sequences by tracking unique identifiers. + * @param {any[]} measurementGroups - The measurement groups. + * @returns {Object} The merged content sequences by tracking unique identifiers. */ function _getMergedContentSequencesByTrackingUniqueIdentifiers(MeasurementGroups) { const mergedContentSequencesByTrackingUniqueIdentifiers = {}; @@ -477,8 +463,8 @@ function _getMergedContentSequencesByTrackingUniqueIdentifiers(MeasurementGroups * it calls the _processTID1410Measurement function. * Otherwise, it calls the _processNonGeometricallyDefinedMeasurement function. * - * @param {Array} mergedContentSequence - The merged content sequence to process. - * @returns {any} - The processed measurement result. + * @param {any[]} mergedContentSequence - The merged content sequence to process. + * @returns {any} The processed measurement result. */ function _processMeasurement(mergedContentSequence) { if (mergedContentSequence.some(group => isScoordOr3d(group) && !isTextPosition(group))) { @@ -493,8 +479,8 @@ function _processMeasurement(mergedContentSequence) { * TID 1410 style measurements have a SCOORD or SCOORD3D at the top level, * and non-geometric representations where each NUM has "INFERRED FROM" SCOORD/SCOORD3D. * - * @param mergedContentSequence - The merged content sequence containing the measurements. - * @returns The measurement object containing the loaded status, labels, coordinates, tracking unique identifier, and tracking identifier. + * @param {any[]} mergedContentSequence - The merged content sequence containing the measurements. + * @returns {any} The measurement object containing the loaded status, labels, coordinates, tracking unique identifier, and tracking identifier. */ function _processTID1410Measurement(mergedContentSequence) { // Need to deal with TID 1410 style measurements, which will have a SCOORD or SCOORD3D at the top level, @@ -567,8 +553,8 @@ function _processTID1410Measurement(mergedContentSequence) { /** * Processes the non-geometrically defined measurement from the merged content sequence. * - * @param mergedContentSequence The merged content sequence containing the measurement data. - * @returns The processed measurement object. + * @param {any[]} mergedContentSequence The merged content sequence containing the measurement data. + * @returns {any} The processed measurement object. */ function _processNonGeometricallyDefinedMeasurement(mergedContentSequence) { const NUMContentItems = mergedContentSequence.filter(group => group.ValueType === 'NUM'); @@ -668,8 +654,8 @@ function _processNonGeometricallyDefinedMeasurement(mergedContentSequence) { /** * Extracts coordinates from a graphic item of type SCOORD or SCOORD3D. - * @param {object} graphicItem - The graphic item containing the coordinates. - * @returns {object} - The extracted coordinates. + * @param {any} graphicItem - The graphic item containing the coordinates. + * @returns {any} The extracted coordinates. */ const _getCoordsFromSCOORDOrSCOORD3D = graphicItem => { const { ValueType, GraphicType, GraphicData } = graphicItem; @@ -683,9 +669,9 @@ const _getCoordsFromSCOORDOrSCOORD3D = graphicItem => { /** * Retrieves the label and value from the provided ConceptNameCodeSequence and MeasuredValueSequence. - * @param {Object} ConceptNameCodeSequence - The ConceptNameCodeSequence object. - * @param {Object} MeasuredValueSequence - The MeasuredValueSequence object. - * @returns {Object} - An object containing the label and value. + * @param {any} conceptNameCodeSequence - The ConceptNameCodeSequence object. + * @param {any} measuredValueSequence - The MeasuredValueSequence object. + * @returns {Object} An object containing the label and value. * The label represents the CodeMeaning from the ConceptNameCodeSequence. * The value represents the formatted NumericValue and CodeValue from the MeasuredValueSequence. * Example: { label: 'Long Axis', value: '31.00 mm' } @@ -704,8 +690,8 @@ function _getLabelFromMeasuredValueSequence(ConceptNameCodeSequence, MeasuredVal /** * Retrieves a list of referenced images from the Imaging Measurement Report Content Sequence. * - * @param {Array} ImagingMeasurementReportContentSequence - The Imaging Measurement Report Content Sequence. - * @returns {Array} - The list of referenced images. + * @param {any[]} imagingMeasurementReportContentSequence - The Imaging Measurement Report Content Sequence. + * @returns {any[]} The list of referenced images. */ function _getReferencedImagesList(ImagingMeasurementReportContentSequence) { const ImageLibrary = ImagingMeasurementReportContentSequence.find( @@ -752,7 +738,7 @@ function _getReferencedImagesList(ImagingMeasurementReportContentSequence) { * Otherwise, the sequence is wrapped in an array and returned. * * @param {any} sequence - The DICOM sequence to convert. - * @returns {any[]} - The converted array. + * @returns {any[]} The converted array. */ function _getSequenceAsArray(sequence) { if (!sequence) { diff --git a/extensions/cornerstone-dicom-sr/src/init.ts b/extensions/cornerstone-dicom-sr/src/init.ts index 7bf80bd2bab..9687a07a2e2 100644 --- a/extensions/cornerstone-dicom-sr/src/init.ts +++ b/extensions/cornerstone-dicom-sr/src/init.ts @@ -9,10 +9,10 @@ import { LengthTool, PlanarFreehandROITool, RectangleROITool, - utilities as csToolsUtils, } from '@cornerstonejs/tools'; -import { Types, MeasurementService } from '@ohif/core'; +import { Types } from '@ohif/core'; import { Enums as CSExtensionEnums } from '@ohif/extension-cornerstone'; + import DICOMSRDisplayTool from './tools/DICOMSRDisplayTool'; import SCOORD3DPointTool from './tools/SCOORD3DPointTool'; import SRSCOOR3DProbeMapper from './utils/SRSCOOR3DProbeMapper'; @@ -28,7 +28,7 @@ export default function init({ configuration = {}, servicesManager, }: Types.Extensions.ExtensionParams): void { - const { measurementService, cornerstoneViewportService } = servicesManager.services; + const { measurementService } = servicesManager.services; addToolInstance(toolNames.DICOMSRDisplay, DICOMSRDisplayTool); addToolInstance(toolNames.SRLength, LengthTool); diff --git a/extensions/cornerstone-dicom-sr/src/utils/addSRAnnotation.ts b/extensions/cornerstone-dicom-sr/src/utils/addSRAnnotation.ts index 52b1425d106..084b2026228 100644 --- a/extensions/cornerstone-dicom-sr/src/utils/addSRAnnotation.ts +++ b/extensions/cornerstone-dicom-sr/src/utils/addSRAnnotation.ts @@ -7,8 +7,53 @@ import toolNames from '../tools/toolNames'; const { MeasurementReport } = adaptersSR.Cornerstone3D; -export default function addSRAnnotation(measurement, imageId, frameNumber) { +/** + * Adds a DICOM SR (Structured Report) annotation to the annotation manager. + * This function processes measurement data from DICOM SR and converts it into + * a format suitable for display in the Cornerstone3D viewer. + * + * @param {Object} params - The parameters object + * @param {Object} params.measurement - The DICOM SR measurement data containing coordinates, labels, and metadata + * @param {Array} params.measurement.coords - Array of coordinate objects with GraphicType, ValueType, and other properties + * @param {string} params.measurement.TrackingUniqueIdentifier - Unique identifier for the measurement + * @param {string} params.measurement.TrackingIdentifier - Tracking identifier for adapter lookup + * @param {Array} [params.measurement.labels] - Optional array of label objects + * @param {string} [params.measurement.displayText] - Optional display text for the annotation + * @param {Object} [params.measurement.textBox] - Optional text box configuration + * @param {string|null} [params.imageId] - Optional image ID for the referenced image (defaults to null) + * @param {number|null} [params.frameNumber] - Optional frame number for multi-frame images (defaults to null) + * @param {Object} params.displaySet - The display set containing the image + * @param {string} params.displaySet.displaySetInstanceUID - Unique identifier for the display set + * @returns {void} + * + * @example + * ```typescript + * addSRAnnotation({ + * measurement: { + * TrackingUniqueIdentifier: '1.2.3.4.5', + * TrackingIdentifier: 'POINT', + * coords: [{ + * GraphicType: 'POINT', + * ValueType: 'SCOORD', + * // ... other coordinate properties + * }], + * labels: [{ value: 'Measurement Point' }], + * displayText: 'Point measurement' + * }, + * imageId: 'wadouri:file://path/to/image.dcm', // Optional + * frameNumber: 0, // Optional + * displaySet: { displaySetInstanceUID: '1.2.3.4' } + * }); + * ``` + */ +export default function addSRAnnotation({ measurement, imageId = null, frameNumber = null, displaySet }) { + /** @type {string} The tool name to use for the annotation, defaults to DICOMSRDisplay */ let toolName = toolNames.DICOMSRDisplay; + + /** + * @type {Object} Renderable data organized by graphic type + * Groups coordinate data by GraphicType for efficient rendering + */ const renderableData = measurement.coords.reduce((acc, coordProps) => { acc[coordProps.GraphicType] = acc[coordProps.GraphicType] || []; acc[coordProps.GraphicType].push(getRenderableData({ ...coordProps, imageId })); @@ -52,10 +97,15 @@ export default function addSRAnnotation(measurement, imageId, frameNumber) { referencedImageId: imageId, }; + /** + * @type {Types.Annotation} The annotation object to be added to the annotation manager + * Contains all necessary metadata and data for rendering the DICOM SR measurement + */ const SRAnnotation: Types.Annotation = { annotationUID: TrackingUniqueIdentifier, highlighted: false, isLocked: false, + isPreview: toolName === toolNames.DICOMSRDisplay, invalidated: false, metadata: { toolName, @@ -64,6 +114,7 @@ export default function addSRAnnotation(measurement, imageId, frameNumber) { graphicType, FrameOfReferenceUID: frameOfReferenceUID, referencedImageId: imageId, + displaySetInstanceUID: displaySet.displaySetInstanceUID, }, data: { label: measurement.labels?.[0]?.value || undefined, @@ -81,8 +132,11 @@ export default function addSRAnnotation(measurement, imageId, frameNumber) { }; /** - * const annotationManager = annotation.annotationState.getAnnotationManager(); - * was not triggering annotation_added events. + * Add the annotation to the annotation state manager. + * Note: Using annotation.state.addAnnotation() instead of annotationManager.addAnnotation() + * because the latter was not triggering annotation_added events properly. + * + * @param {Types.Annotation} SRAnnotation - The annotation to add */ annotation.state.addAnnotation(SRAnnotation); } diff --git a/extensions/cornerstone/src/components/AccordionGroup/AccordionGroup.tsx b/extensions/cornerstone/src/components/AccordionGroup/AccordionGroup.tsx index 337614a29e4..d91ba8834ee 100644 --- a/extensions/cornerstone/src/components/AccordionGroup/AccordionGroup.tsx +++ b/extensions/cornerstone/src/components/AccordionGroup/AccordionGroup.tsx @@ -55,7 +55,7 @@ const DEFAULT_TYPES = [GroupAccordion, Content, Trigger]; * measurements panel for a practical, working example. */ export function AccordionGroup(props) { - const { grouping, items, children, sourceChildren, type } = props; + const { grouping, items, children, sourceChildren } = props; const childProps = useSystem(); let defaultValue = props.defaultValue; const groups = grouping.groupingFunction(items, grouping, childProps); diff --git a/extensions/cornerstone/src/components/StudyMeasurements.tsx b/extensions/cornerstone/src/components/StudyMeasurements.tsx index dd1f864a4d4..c13e4f72205 100644 --- a/extensions/cornerstone/src/components/StudyMeasurements.tsx +++ b/extensions/cornerstone/src/components/StudyMeasurements.tsx @@ -1,6 +1,5 @@ import React from 'react'; -import { useActiveViewportDisplaySets, useSystem, utils } from '@ohif/core'; -// import { AccordionContent, AccordionItem, AccordionTrigger } from '@ohif/ui-next'; +import { useActiveViewportDisplaySets, utils } from '@ohif/core'; import { AccordionGroup } from './AccordionGroup'; import MeasurementsOrAdditionalFindings from './MeasurementsOrAdditionalFindings'; @@ -63,8 +62,7 @@ export const groupByStudy = (items, grouping, childProps) => { export function StudyMeasurements(props): React.ReactNode { const { items, grouping = {}, children } = props; - const system = useSystem(); - const activeDisplaySets = useActiveViewportDisplaySets(system); + const activeDisplaySets = useActiveViewportDisplaySets(); const activeStudyUID = activeDisplaySets?.[0]?.StudyInstanceUID; return ( diff --git a/extensions/cornerstone/src/hooks/useMeasurements.ts b/extensions/cornerstone/src/hooks/useMeasurements.ts index a8bd3504a80..0f9fc87a891 100644 --- a/extensions/cornerstone/src/hooks/useMeasurements.ts +++ b/extensions/cornerstone/src/hooks/useMeasurements.ts @@ -1,6 +1,7 @@ import { useState, useEffect } from 'react'; import debounce from 'lodash.debounce'; import { useSystem } from '@ohif/core'; + function mapMeasurementToDisplay(measurement, displaySetService) { const { referenceSeriesUID } = measurement; diff --git a/extensions/cornerstone/src/initMeasurementService.ts b/extensions/cornerstone/src/initMeasurementService.ts index ab150a4e0a0..d66ef291bba 100644 --- a/extensions/cornerstone/src/initMeasurementService.ts +++ b/extensions/cornerstone/src/initMeasurementService.ts @@ -450,7 +450,7 @@ const connectMeasurementServiceToTools = ({ if (measurement?.metadata?.referencedImageId) { imageId = measurement.metadata.referencedImageId; frameNumber = getSOPInstanceAttributes(measurement.metadata.referencedImageId).frameNumber; - } else { + } else if (instance) { imageId = dataSource.getImageIdsForInstance({ instance }); } diff --git a/extensions/cornerstone/src/panels/PanelMeasurement.tsx b/extensions/cornerstone/src/panels/PanelMeasurement.tsx index 0cd861bd8bb..224a299f2a9 100644 --- a/extensions/cornerstone/src/panels/PanelMeasurement.tsx +++ b/extensions/cornerstone/src/panels/PanelMeasurement.tsx @@ -78,6 +78,7 @@ export default function PanelMeasurement(props): React.ReactNode { ); return cloned; } + // Need to merge defaults on the content props to ensure they get passed to children return ; } diff --git a/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking.tsx b/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking.tsx index 6735066da69..c8523dd2e45 100644 --- a/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking.tsx +++ b/extensions/measurement-tracking/src/panels/PanelMeasurementTableTracking.tsx @@ -13,7 +13,8 @@ import { import { useTrackedMeasurements } from '../getContextModule'; import { UntrackSeriesModal } from './PanelStudyBrowserTracking/untrackSeriesModal'; -const { filterMeasurementsBySeriesUID, filterAny } = utils.MeasurementFilters; +const { filterMeasurementsBySeriesUID, filterAny } = + utils.MeasurementFilters; function PanelMeasurementTableTracking(props) { const [viewportGrid] = useViewportGrid(); @@ -22,7 +23,9 @@ function PanelMeasurementTableTracking(props) { const [trackedMeasurements, sendTrackedMeasurementsEvent] = useTrackedMeasurements(); const { trackedStudy, trackedSeries } = trackedMeasurements.context; - const measurementFilter = trackedStudy ? filterMeasurementsBySeriesUID(trackedSeries) : filterAny; + const measurementFilter = trackedStudy + ? filterMeasurementsBySeriesUID(trackedSeries) + : filterAny; const onUntrackConfirm = () => { sendTrackedMeasurementsEvent('UNTRACK_ALL', {}); diff --git a/platform/core/src/services/MeasurementService/MeasurementService.ts b/platform/core/src/services/MeasurementService/MeasurementService.ts index f69dbe7e318..15fba19203d 100644 --- a/platform/core/src/services/MeasurementService/MeasurementService.ts +++ b/platform/core/src/services/MeasurementService/MeasurementService.ts @@ -1,6 +1,7 @@ import log from '../../log'; import guid from '../../utils/guid'; import { PubSubService } from '../_shared/pubSubServiceInterface'; +import { DicomMetadataStore } from '../DicomMetadataStore/DicomMetadataStore'; /** * Measurement source schema @@ -127,7 +128,6 @@ class MeasurementService extends PubSubService { public readonly VALUE_TYPES = VALUE_TYPES; private measurements = new Map(); - private unmappedMeasurements = new Map(); private isMeasurementDeletedIndividually: boolean; private sources = {}; @@ -495,6 +495,7 @@ class MeasurementService extends PubSubService { ); if (!sourceMapping) { console.log('No source mapping', source.uid, annotationType, source); + this.addUnmappedMeasurement(sourceAnnotationDetail, source); return; } const { toMeasurementSchema } = sourceMapping; @@ -507,16 +508,7 @@ class MeasurementService extends PubSubService { measurement.source = source; } catch (error) { - // Todo: handle other - this.unmappedMeasurements.set(sourceAnnotationDetail.uid, { - ...sourceAnnotationDetail, - source: { - name: source.name, - version: source.version, - uid: source.uid, - }, - }); - + this.addUnmappedMeasurement(sourceAnnotationDetail, source); console.log('Failed to map', error); throw new Error( `Failed to map '${sourceInfo}' measurement for annotationType ${annotationType}: ${error.message}` @@ -577,14 +569,98 @@ class MeasurementService extends PubSubService { return newMeasurement.uid; } + /** + * Recursively searches for any attribute at any level in the object + * @param {any} obj The object to search + * @param {string} attributeName The name of the attribute to find + * @returns {any} The attribute value if found, undefined otherwise + */ + private findAttributeRecursively(obj: any, attributeName: string): any { + if (!obj || typeof obj !== 'object') { + return undefined; + } + if (obj[attributeName]) { + return obj[attributeName]; + } + for (const key in obj) { + if (obj.hasOwnProperty(key)) { + const result = this.findAttributeRecursively(obj[key], attributeName); + if (result) { + return result; + } + } + } + return undefined; + } + + /** + * Adds an unmapped measurement to the measurement service. + * + * @param {any} sourceAnnotationDetail The source annotation detail + * @param {any} source The source + */ + private addUnmappedMeasurement(sourceAnnotationDetail: any, source: any) { + if (sourceAnnotationDetail.annotation?.invalidated === true) { + console.log('Measurement is invalidated, skipping...', sourceAnnotationDetail); + return; + } + + if (sourceAnnotationDetail.annotation?.isPreview === true) { + console.log('Measurement is preview, skipping...', sourceAnnotationDetail); + return; + } + + const metadata = this.findAttributeRecursively(sourceAnnotationDetail, 'metadata'); + const label = this.findAttributeRecursively(sourceAnnotationDetail, 'label'); + const referencedImageId = this.findAttributeRecursively( + sourceAnnotationDetail, + 'referencedImageId' + ); + const displaySetInstanceUID = this.findAttributeRecursively( + sourceAnnotationDetail, + 'displaySetInstanceUID' + ); + + const measurement = { + ...sourceAnnotationDetail, + isUnmapped: true, + statusTooltip: 'This measurement is not compatible with this application', + source: { + name: source.name, + version: source.version, + uid: source.uid, + }, + }; + + if (metadata) { + measurement.metadata = metadata; + } + + if (label) { + measurement.label = label; + } + + if (referencedImageId) { + measurement.referencedImageId = referencedImageId; + const instance = DicomMetadataStore.getInstanceByImageId(referencedImageId); + measurement.referenceStudyUID = instance.StudyInstanceUID; + measurement.referenceSeriesUID = instance.SeriesInstanceUID; + } + + if (displaySetInstanceUID) { + measurement.displaySetInstanceUID = displaySetInstanceUID; + } + + this.measurements.set(sourceAnnotationDetail.uid, measurement); + } + /** * Removes a measurement and broadcasts the removed event. * * @param {string} measurementUID The measurement uid */ remove(measurementUID: string): void { - const measurement = - this.measurements.get(measurementUID) || this.unmappedMeasurements.get(measurementUID); + const measurement = this.measurements.get(measurementUID); if (!measurementUID || !measurement) { console.debug(`No uid provided, or unable to find measurement by uid.`); @@ -593,7 +669,6 @@ class MeasurementService extends PubSubService { const source = measurement.source; - this.unmappedMeasurements.delete(measurementUID); this.measurements.delete(measurementUID); this.isMeasurementDeletedIndividually = true; this._broadcastEvent(this.EVENTS.MEASUREMENT_REMOVED, { @@ -609,14 +684,13 @@ class MeasurementService extends PubSubService { const measurements = []; for (const measurementUID of measurementUIDs) { const measurement = - this.measurements.get(measurementUID) || this.unmappedMeasurements.get(measurementUID); + this.measurements.get(measurementUID) if (!measurementUID || !measurement) { console.debug(`No uid provided, or unable to find measurement by uid.`); continue; } - this.unmappedMeasurements.delete(measurementUID); this.measurements.delete(measurementUID); measurements.push(measurement); } @@ -634,11 +708,7 @@ class MeasurementService extends PubSubService { public clearMeasurements(filter?: MeasurementFilter) { // Make a copy of the measurements const toClear = this.getMeasurements(filter); - const unmappedClear = filter - ? [...this.unmappedMeasurements.values()].filter(filter) - : this.unmappedMeasurements; - const measurements = [...toClear, ...unmappedClear]; - unmappedClear.forEach(measurement => this.unmappedMeasurements.delete(measurement.uid)); + const measurements = [...toClear]; toClear.forEach(measurement => this.measurements.delete(measurement.uid)); this._broadcastEvent(this.EVENTS.MEASUREMENTS_CLEARED, { measurements }); } @@ -657,7 +727,8 @@ class MeasurementService extends PubSubService { */ public jumpToMeasurement(viewportId: string, measurementUID: string): void { - const measurement = this.measurements.get(measurementUID); + const measurement = + this.measurements.get(measurementUID) if (!measurement) { log.warn(`No measurement uid, or unable to find by uid.`); @@ -779,7 +850,8 @@ class MeasurementService extends PubSubService { }; public toggleLockMeasurement(measurementUID: string): void { - const measurement = this.measurements.get(measurementUID); + const measurement = + this.measurements.get(measurementUID) if (!measurement) { console.debug(`No measurement found for uid: ${measurementUID}`); @@ -796,7 +868,8 @@ class MeasurementService extends PubSubService { } public toggleVisibilityMeasurement(measurementUID: string, visibility?: boolean): void { - const measurement = this.measurements.get(measurementUID); + const measurement = + this.measurements.get(measurementUID) if (!measurement) { console.debug(`No measurement found for uid: ${measurementUID}`); @@ -820,7 +893,8 @@ class MeasurementService extends PubSubService { } public updateColorMeasurement(measurementUID: string, color: number[]): void { - const measurement = this.measurements.get(measurementUID); + const measurement = + this.measurements.get(measurementUID) if (!measurement) { console.debug(`No measurement found for uid: ${measurementUID}`); diff --git a/platform/core/src/utils/measurementFilters.ts b/platform/core/src/utils/measurementFilters.ts index ef1a887f6ef..72e847b0b8f 100644 --- a/platform/core/src/utils/measurementFilters.ts +++ b/platform/core/src/utils/measurementFilters.ts @@ -10,6 +10,7 @@ export function filterMeasurementsBySeriesUID(selectedSeries: string[]) { return measurement => selectedSeries.includes(measurement.referenceSeriesUID); } +/** A filter that filters for measurements belonging to the study */ export function filterMeasurementsByStudyUID(studyUID) { return measurement => measurement.referenceStudyUID == studyUID; } diff --git a/platform/ui-next/src/components/DataRow/DataRow.tsx b/platform/ui-next/src/components/DataRow/DataRow.tsx index fad5b2988c6..e229b43e7e9 100644 --- a/platform/ui-next/src/components/DataRow/DataRow.tsx +++ b/platform/ui-next/src/components/DataRow/DataRow.tsx @@ -18,6 +18,7 @@ import { cn } from '../../lib/utils'; * @component * @example * ```tsx + * // Basic usage without status * {}} * onColor={() => {}} * /> + * + * // With warning status using composite pattern + * + * + * + * + * // With success status using composite pattern + * + * + * + * + * // Multiple status indicators + * + * + * + * + * * ``` */ @@ -55,6 +79,7 @@ import { cn } from '../../lib/utils'; * @property {() => void} onRename - Callback when rename is requested * @property {() => void} onDelete - Callback when delete is requested * @property {() => void} onColor - Callback when color change is requested + * @property {React.ReactNode} children - Optional children, including Status components */ interface DataRowProps { number: number | null; @@ -79,9 +104,10 @@ interface DataRowProps { colorHex?: string; onColor: (e) => void; className?: string; + children?: React.ReactNode; } -export const DataRow: React.FC = ({ +const DataRowComponent: React.FC = ({ number, title, colorHex, @@ -97,11 +123,20 @@ export const DataRow: React.FC = ({ isVisible = true, disableEditing = false, className, + children, }) => { const [isDropdownOpen, setIsDropdownOpen] = useState(false); const isTitleLong = title?.length > 25; const rowRef = useRef(null); + // Extract Status components from children + const statusComponents = React.Children.toArray(children).filter( + child => + React.isValidElement(child) && + child.type && + (child.type as any).displayName?.startsWith('DataRow.Status') + ); + // useEffect(() => { // if (isSelected && rowRef.current) { // setTimeout(() => { @@ -281,6 +316,9 @@ export const DataRow: React.FC = ({ {/* Lock Icon (if needed) */} {isLocked && !disableEditing && } + {/* Status Components */} + {statusComponents} + {/* Actions Dropdown Menu */} {disableEditing &&
} {!disableEditing && ( @@ -369,4 +407,94 @@ export const DataRow: React.FC = ({ ); }; +interface StatusProps { + children: React.ReactNode; +} + +interface StatusIndicatorProps { + tooltip?: string; + icon: React.ReactNode; + defaultTooltip: string; +} + +const StatusIndicator: React.FC = ({ tooltip, icon, defaultTooltip }) => ( + + +
{icon}
+
+ +
{tooltip || defaultTooltip}
+
+
+); + +const Status: React.FC & { + Warning: React.FC<{ tooltip?: string }>; + Success: React.FC<{ tooltip?: string }>; + Error: React.FC<{ tooltip?: string }>; + Info: React.FC<{ tooltip?: string }>; +} = ({ children }) => { + return <>{children}; +}; + +const StatusWarning: React.FC<{ tooltip?: string }> = ({ tooltip }) => ( + + } + defaultTooltip="Warning" + /> +); + +const StatusSuccess: React.FC<{ tooltip?: string }> = ({ tooltip }) => ( + } + defaultTooltip="Success" + /> +); + +const StatusError: React.FC<{ tooltip?: string }> = ({ tooltip }) => ( + + } + defaultTooltip="Error" + /> +); + +const StatusInfo: React.FC<{ tooltip?: string }> = ({ tooltip }) => ( + } + defaultTooltip="Info" + /> +); + +Status.displayName = 'DataRow.Status'; +StatusWarning.displayName = 'DataRow.Status.Warning'; +StatusSuccess.displayName = 'DataRow.Status.Success'; +StatusError.displayName = 'DataRow.Status.Error'; +StatusInfo.displayName = 'DataRow.Status.Info'; + +Status.Warning = StatusWarning; +Status.Success = StatusSuccess; +Status.Error = StatusError; +Status.Info = StatusInfo; + +const DataRow = DataRowComponent as React.FC & { + Status: typeof Status; +}; + +DataRow.Status = Status; + export default DataRow; +export { DataRow }; diff --git a/platform/ui-next/src/components/MeasurementTable/MeasurementTable.tsx b/platform/ui-next/src/components/MeasurementTable/MeasurementTable.tsx index 7a8b13f6db2..fccde4c757a 100644 --- a/platform/ui-next/src/components/MeasurementTable/MeasurementTable.tsx +++ b/platform/ui-next/src/components/MeasurementTable/MeasurementTable.tsx @@ -1,6 +1,7 @@ import React from 'react'; import { useTranslation } from 'react-i18next'; -import { DataRow, PanelSection } from '../../index'; +import { Icons, PanelSection, Tooltip, TooltipContent, TooltipTrigger } from '../../index'; +import DataRow from '../DataRow/DataRow'; import { createContext } from '../../lib/createContext'; interface MeasurementTableContext { @@ -11,7 +12,7 @@ interface MeasurementTableContext { } const [MeasurementTableProvider, useMeasurementTableContext] = - createContext('MeasurementTable', { data: [] }); + createContext('MeasurementTable', { data: [], isExpanded: true }); interface MeasurementDataProps extends MeasurementTableContext { title: string; @@ -91,6 +92,8 @@ interface MeasurementItem { isLocked: boolean; toolName: string; isExpanded: boolean; + isUnmapped?: boolean; + statusTooltip?: string; } interface RowProps { @@ -117,11 +120,15 @@ const Row = ({ item, index }: RowProps) => { onRename={e => onAction(e, 'renameMeasurement', uid)} onToggleVisibility={e => onAction(e, 'toggleVisibilityMeasurement', uid)} onToggleLocked={e => onAction(e, 'toggleLockMeasurement', uid)} + onColor={e => onAction(e, 'changeMeasurementColor', uid)} disableEditing={disableEditing} - isExpanded={isExpanded} isVisible={item.isVisible} isLocked={item.isLocked} - /> + > + {item.isUnmapped && ( + + )} +
); };