feat(MeasurementService): add rendering of unmapped measurements (#5416)
This commit is contained in:
parent
639863a8ef
commit
851e74d7b8
@ -22,7 +22,7 @@ const {
|
|||||||
CodeScheme: Cornerstone3DCodeScheme,
|
CodeScheme: Cornerstone3DCodeScheme,
|
||||||
} = adaptersSR.Cornerstone3D;
|
} = adaptersSR.Cornerstone3D;
|
||||||
|
|
||||||
type InstanceMetadata = Types.InstanceMetadata;
|
type InstanceMetadata = OhifTypes.InstanceMetadata;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* TODO
|
* TODO
|
||||||
@ -69,9 +69,10 @@ function addInstances(instances: InstanceMetadata[], _displaySetService: Display
|
|||||||
* DICOM SR SOP Class Handler
|
* DICOM SR SOP Class Handler
|
||||||
* For all referenced images in the TID 1500/300 sections, add an image to the
|
* For all referenced images in the TID 1500/300 sections, add an image to the
|
||||||
* display.
|
* display.
|
||||||
* @param instances is a set of instances all from the same series
|
* @param {InstanceMetadata[]} instances - A set of instances all from the same series
|
||||||
* @param servicesManager is the services that can be used for creating
|
* @param {AppTypes.ServicesManager} servicesManager - The services that can be used for creating
|
||||||
* @returns The list of display sets created for the given instances object
|
* @param {AppTypes.ExtensionManager} extensionManager - The extension manager
|
||||||
|
* @returns {Types.DisplaySet[]} The list of display sets created for the given instances object
|
||||||
*/
|
*/
|
||||||
function _getDisplaySetsFromSeries(
|
function _getDisplaySetsFromSeries(
|
||||||
instances,
|
instances,
|
||||||
@ -143,7 +144,7 @@ function _getDisplaySetsFromSeries(
|
|||||||
* @param extensionManager - The extension manager containing data sources.
|
* @param extensionManager - The extension manager containing data sources.
|
||||||
*/
|
*/
|
||||||
async function _load(
|
async function _load(
|
||||||
srDisplaySet: Types.DisplaySet,
|
srDisplaySet: OhifTypes.DisplaySet,
|
||||||
servicesManager: AppTypes.ServicesManager,
|
servicesManager: AppTypes.ServicesManager,
|
||||||
extensionManager: AppTypes.ExtensionManager
|
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(
|
function _checkIfCanAddMeasurementsToDisplaySet(
|
||||||
srDisplaySet,
|
srDisplaySet: OhifTypes.DisplaySet,
|
||||||
newDisplaySet,
|
newDisplaySet: OhifTypes.DisplaySet,
|
||||||
dataSource,
|
dataSource,
|
||||||
servicesManager: AppTypes.ServicesManager
|
servicesManager: AppTypes.ServicesManager
|
||||||
) {
|
) {
|
||||||
@ -289,9 +282,7 @@ function _checkIfCanAddMeasurementsToDisplaySet(
|
|||||||
is3DMeasurement &&
|
is3DMeasurement &&
|
||||||
_measurementBelongsToDisplaySet({ measurement, displaySet: newDisplaySet })
|
_measurementBelongsToDisplaySet({ measurement, displaySet: newDisplaySet })
|
||||||
) {
|
) {
|
||||||
_measurementBelongsToDisplaySet({ measurement, displaySet: newDisplaySet })
|
addSRAnnotation({ measurement, displaySet: newDisplaySet });
|
||||||
|
|
||||||
addSRAnnotation(measurement, null, null);
|
|
||||||
measurement.loaded = true;
|
measurement.loaded = true;
|
||||||
measurement.displaySetInstanceUID = newDisplaySet.displaySetInstanceUID;
|
measurement.displaySetInstanceUID = newDisplaySet.displaySetInstanceUID;
|
||||||
unloadedMeasurements.splice(j, 1);
|
unloadedMeasurements.splice(j, 1);
|
||||||
@ -312,15 +303,12 @@ function _checkIfCanAddMeasurementsToDisplaySet(
|
|||||||
imageId &&
|
imageId &&
|
||||||
_measurementReferencesSOPInstanceUID(measurement, ReferencedSOPInstanceUID, frame)
|
_measurementReferencesSOPInstanceUID(measurement, ReferencedSOPInstanceUID, frame)
|
||||||
) {
|
) {
|
||||||
addSRAnnotation(measurement, imageId, frame);
|
addSRAnnotation({ measurement, imageId, frameNumber: frame, displaySet: newDisplaySet });
|
||||||
|
|
||||||
// Update measurement properties
|
|
||||||
measurement.loaded = true;
|
measurement.loaded = true;
|
||||||
measurement.imageId = imageId;
|
measurement.imageId = imageId;
|
||||||
measurement.displaySetInstanceUID = newDisplaySet.displaySetInstanceUID;
|
measurement.displaySetInstanceUID = newDisplaySet.displaySetInstanceUID;
|
||||||
measurement.ReferencedSOPInstanceUID = ReferencedSOPInstanceUID;
|
measurement.ReferencedSOPInstanceUID = ReferencedSOPInstanceUID;
|
||||||
measurement.frameNumber = frame;
|
measurement.frameNumber = frame;
|
||||||
|
|
||||||
unloadedMeasurements.splice(j, 1);
|
unloadedMeasurements.splice(j, 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -328,10 +316,10 @@ function _checkIfCanAddMeasurementsToDisplaySet(
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if a measurement references a specific SOP Instance UID.
|
* Checks if a measurement references a specific SOP Instance UID.
|
||||||
* @param measurement - The measurement object.
|
* @param {any} measurement - The measurement object.
|
||||||
* @param SOPInstanceUID - The SOP Instance UID to check against.
|
* @param {string} sopInstanceUID - The SOP Instance UID to check against.
|
||||||
* @param frameNumber - The frame number to check against (optional).
|
* @param {number} frameNumber - The frame number to check against (optional).
|
||||||
* @returns True if the measurement references the specified SOP Instance UID, false otherwise.
|
* @returns {boolean} True if the measurement references the specified SOP Instance UID, false otherwise.
|
||||||
*/
|
*/
|
||||||
function _measurementReferencesSOPInstanceUID(measurement, SOPInstanceUID, frameNumber) {
|
function _measurementReferencesSOPInstanceUID(measurement, SOPInstanceUID, frameNumber) {
|
||||||
const { coords } = measurement;
|
const { coords } = measurement;
|
||||||
@ -363,10 +351,8 @@ function _measurementReferencesSOPInstanceUID(measurement, SOPInstanceUID, frame
|
|||||||
/**
|
/**
|
||||||
* Retrieves the SOP class handler module.
|
* Retrieves the SOP class handler module.
|
||||||
*
|
*
|
||||||
* @param {Object} options - The options for retrieving the SOP class handler module.
|
* @param {OhifTypes.Extensions.ExtensionParams} params - The extension parameters.
|
||||||
* @param {Object} options.servicesManager - The services manager.
|
* @returns {Array} An array containing the SOP class handler modules.
|
||||||
* @param {Object} options.extensionManager - The extension manager.
|
|
||||||
* @returns {Array} An array containing the SOP class handler module.
|
|
||||||
*/
|
*/
|
||||||
function getSopClassHandlerModule(params: OhifTypes.Extensions.ExtensionParams) {
|
function getSopClassHandlerModule(params: OhifTypes.Extensions.ExtensionParams) {
|
||||||
const { servicesManager, extensionManager } = params;
|
const { servicesManager, extensionManager } = params;
|
||||||
@ -390,8 +376,8 @@ function getSopClassHandlerModule(params: OhifTypes.Extensions.ExtensionParams)
|
|||||||
/**
|
/**
|
||||||
* Retrieves the measurements from the ImagingMeasurementReportContentSequence.
|
* Retrieves the measurements from the ImagingMeasurementReportContentSequence.
|
||||||
*
|
*
|
||||||
* @param {Array} ImagingMeasurementReportContentSequence - The ImagingMeasurementReportContentSequence array.
|
* @param {any[]} imagingMeasurementReportContentSequence - The ImagingMeasurementReportContentSequence array.
|
||||||
* @returns {Array} - The array of measurements.
|
* @returns {any[]} The array of measurements.
|
||||||
*/
|
*/
|
||||||
function _getMeasurements(ImagingMeasurementReportContentSequence) {
|
function _getMeasurements(ImagingMeasurementReportContentSequence) {
|
||||||
const ImagingMeasurements = ImagingMeasurementReportContentSequence.find(
|
const ImagingMeasurements = ImagingMeasurementReportContentSequence.find(
|
||||||
@ -429,8 +415,8 @@ function _getMeasurements(ImagingMeasurementReportContentSequence) {
|
|||||||
/**
|
/**
|
||||||
* Retrieves merged content sequences by tracking unique identifiers.
|
* Retrieves merged content sequences by tracking unique identifiers.
|
||||||
*
|
*
|
||||||
* @param {Array} MeasurementGroups - The measurement groups.
|
* @param {any[]} measurementGroups - The measurement groups.
|
||||||
* @returns {Object} - The merged content sequences by tracking unique identifiers.
|
* @returns {Object} The merged content sequences by tracking unique identifiers.
|
||||||
*/
|
*/
|
||||||
function _getMergedContentSequencesByTrackingUniqueIdentifiers(MeasurementGroups) {
|
function _getMergedContentSequencesByTrackingUniqueIdentifiers(MeasurementGroups) {
|
||||||
const mergedContentSequencesByTrackingUniqueIdentifiers = {};
|
const mergedContentSequencesByTrackingUniqueIdentifiers = {};
|
||||||
@ -477,8 +463,8 @@ function _getMergedContentSequencesByTrackingUniqueIdentifiers(MeasurementGroups
|
|||||||
* it calls the _processTID1410Measurement function.
|
* it calls the _processTID1410Measurement function.
|
||||||
* Otherwise, it calls the _processNonGeometricallyDefinedMeasurement function.
|
* Otherwise, it calls the _processNonGeometricallyDefinedMeasurement function.
|
||||||
*
|
*
|
||||||
* @param {Array<Object>} mergedContentSequence - The merged content sequence to process.
|
* @param {any[]} mergedContentSequence - The merged content sequence to process.
|
||||||
* @returns {any} - The processed measurement result.
|
* @returns {any} The processed measurement result.
|
||||||
*/
|
*/
|
||||||
function _processMeasurement(mergedContentSequence) {
|
function _processMeasurement(mergedContentSequence) {
|
||||||
if (mergedContentSequence.some(group => isScoordOr3d(group) && !isTextPosition(group))) {
|
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,
|
* 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.
|
* and non-geometric representations where each NUM has "INFERRED FROM" SCOORD/SCOORD3D.
|
||||||
*
|
*
|
||||||
* @param mergedContentSequence - The merged content sequence containing the measurements.
|
* @param {any[]} mergedContentSequence - The merged content sequence containing the measurements.
|
||||||
* @returns The measurement object containing the loaded status, labels, coordinates, tracking unique identifier, and tracking identifier.
|
* @returns {any} The measurement object containing the loaded status, labels, coordinates, tracking unique identifier, and tracking identifier.
|
||||||
*/
|
*/
|
||||||
function _processTID1410Measurement(mergedContentSequence) {
|
function _processTID1410Measurement(mergedContentSequence) {
|
||||||
// Need to deal with TID 1410 style measurements, which will have a SCOORD or SCOORD3D at the top level,
|
// 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.
|
* Processes the non-geometrically defined measurement from the merged content sequence.
|
||||||
*
|
*
|
||||||
* @param mergedContentSequence The merged content sequence containing the measurement data.
|
* @param {any[]} mergedContentSequence The merged content sequence containing the measurement data.
|
||||||
* @returns The processed measurement object.
|
* @returns {any} The processed measurement object.
|
||||||
*/
|
*/
|
||||||
function _processNonGeometricallyDefinedMeasurement(mergedContentSequence) {
|
function _processNonGeometricallyDefinedMeasurement(mergedContentSequence) {
|
||||||
const NUMContentItems = mergedContentSequence.filter(group => group.ValueType === 'NUM');
|
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.
|
* Extracts coordinates from a graphic item of type SCOORD or SCOORD3D.
|
||||||
* @param {object} graphicItem - The graphic item containing the coordinates.
|
* @param {any} graphicItem - The graphic item containing the coordinates.
|
||||||
* @returns {object} - The extracted coordinates.
|
* @returns {any} The extracted coordinates.
|
||||||
*/
|
*/
|
||||||
const _getCoordsFromSCOORDOrSCOORD3D = graphicItem => {
|
const _getCoordsFromSCOORDOrSCOORD3D = graphicItem => {
|
||||||
const { ValueType, GraphicType, GraphicData } = graphicItem;
|
const { ValueType, GraphicType, GraphicData } = graphicItem;
|
||||||
@ -683,9 +669,9 @@ const _getCoordsFromSCOORDOrSCOORD3D = graphicItem => {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves the label and value from the provided ConceptNameCodeSequence and MeasuredValueSequence.
|
* Retrieves the label and value from the provided ConceptNameCodeSequence and MeasuredValueSequence.
|
||||||
* @param {Object} ConceptNameCodeSequence - The ConceptNameCodeSequence object.
|
* @param {any} conceptNameCodeSequence - The ConceptNameCodeSequence object.
|
||||||
* @param {Object} MeasuredValueSequence - The MeasuredValueSequence object.
|
* @param {any} measuredValueSequence - The MeasuredValueSequence object.
|
||||||
* @returns {Object} - An object containing the label and value.
|
* @returns {Object} An object containing the label and value.
|
||||||
* The label represents the CodeMeaning from the ConceptNameCodeSequence.
|
* The label represents the CodeMeaning from the ConceptNameCodeSequence.
|
||||||
* The value represents the formatted NumericValue and CodeValue from the MeasuredValueSequence.
|
* The value represents the formatted NumericValue and CodeValue from the MeasuredValueSequence.
|
||||||
* Example: { label: 'Long Axis', value: '31.00 mm' }
|
* 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.
|
* Retrieves a list of referenced images from the Imaging Measurement Report Content Sequence.
|
||||||
*
|
*
|
||||||
* @param {Array} ImagingMeasurementReportContentSequence - The Imaging Measurement Report Content Sequence.
|
* @param {any[]} imagingMeasurementReportContentSequence - The Imaging Measurement Report Content Sequence.
|
||||||
* @returns {Array} - The list of referenced images.
|
* @returns {any[]} The list of referenced images.
|
||||||
*/
|
*/
|
||||||
function _getReferencedImagesList(ImagingMeasurementReportContentSequence) {
|
function _getReferencedImagesList(ImagingMeasurementReportContentSequence) {
|
||||||
const ImageLibrary = ImagingMeasurementReportContentSequence.find(
|
const ImageLibrary = ImagingMeasurementReportContentSequence.find(
|
||||||
@ -752,7 +738,7 @@ function _getReferencedImagesList(ImagingMeasurementReportContentSequence) {
|
|||||||
* Otherwise, the sequence is wrapped in an array and returned.
|
* Otherwise, the sequence is wrapped in an array and returned.
|
||||||
*
|
*
|
||||||
* @param {any} sequence - The DICOM sequence to convert.
|
* @param {any} sequence - The DICOM sequence to convert.
|
||||||
* @returns {any[]} - The converted array.
|
* @returns {any[]} The converted array.
|
||||||
*/
|
*/
|
||||||
function _getSequenceAsArray(sequence) {
|
function _getSequenceAsArray(sequence) {
|
||||||
if (!sequence) {
|
if (!sequence) {
|
||||||
|
|||||||
@ -9,10 +9,10 @@ import {
|
|||||||
LengthTool,
|
LengthTool,
|
||||||
PlanarFreehandROITool,
|
PlanarFreehandROITool,
|
||||||
RectangleROITool,
|
RectangleROITool,
|
||||||
utilities as csToolsUtils,
|
|
||||||
} from '@cornerstonejs/tools';
|
} from '@cornerstonejs/tools';
|
||||||
import { Types, MeasurementService } from '@ohif/core';
|
import { Types } from '@ohif/core';
|
||||||
import { Enums as CSExtensionEnums } from '@ohif/extension-cornerstone';
|
import { Enums as CSExtensionEnums } from '@ohif/extension-cornerstone';
|
||||||
|
|
||||||
import DICOMSRDisplayTool from './tools/DICOMSRDisplayTool';
|
import DICOMSRDisplayTool from './tools/DICOMSRDisplayTool';
|
||||||
import SCOORD3DPointTool from './tools/SCOORD3DPointTool';
|
import SCOORD3DPointTool from './tools/SCOORD3DPointTool';
|
||||||
import SRSCOOR3DProbeMapper from './utils/SRSCOOR3DProbeMapper';
|
import SRSCOOR3DProbeMapper from './utils/SRSCOOR3DProbeMapper';
|
||||||
@ -28,7 +28,7 @@ export default function init({
|
|||||||
configuration = {},
|
configuration = {},
|
||||||
servicesManager,
|
servicesManager,
|
||||||
}: Types.Extensions.ExtensionParams): void {
|
}: Types.Extensions.ExtensionParams): void {
|
||||||
const { measurementService, cornerstoneViewportService } = servicesManager.services;
|
const { measurementService } = servicesManager.services;
|
||||||
|
|
||||||
addToolInstance(toolNames.DICOMSRDisplay, DICOMSRDisplayTool);
|
addToolInstance(toolNames.DICOMSRDisplay, DICOMSRDisplayTool);
|
||||||
addToolInstance(toolNames.SRLength, LengthTool);
|
addToolInstance(toolNames.SRLength, LengthTool);
|
||||||
|
|||||||
@ -7,8 +7,53 @@ import toolNames from '../tools/toolNames';
|
|||||||
|
|
||||||
const { MeasurementReport } = adaptersSR.Cornerstone3D;
|
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;
|
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) => {
|
const renderableData = measurement.coords.reduce((acc, coordProps) => {
|
||||||
acc[coordProps.GraphicType] = acc[coordProps.GraphicType] || [];
|
acc[coordProps.GraphicType] = acc[coordProps.GraphicType] || [];
|
||||||
acc[coordProps.GraphicType].push(getRenderableData({ ...coordProps, imageId }));
|
acc[coordProps.GraphicType].push(getRenderableData({ ...coordProps, imageId }));
|
||||||
@ -52,10 +97,15 @@ export default function addSRAnnotation(measurement, imageId, frameNumber) {
|
|||||||
referencedImageId: imageId,
|
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 = {
|
const SRAnnotation: Types.Annotation = {
|
||||||
annotationUID: TrackingUniqueIdentifier,
|
annotationUID: TrackingUniqueIdentifier,
|
||||||
highlighted: false,
|
highlighted: false,
|
||||||
isLocked: false,
|
isLocked: false,
|
||||||
|
isPreview: toolName === toolNames.DICOMSRDisplay,
|
||||||
invalidated: false,
|
invalidated: false,
|
||||||
metadata: {
|
metadata: {
|
||||||
toolName,
|
toolName,
|
||||||
@ -64,6 +114,7 @@ export default function addSRAnnotation(measurement, imageId, frameNumber) {
|
|||||||
graphicType,
|
graphicType,
|
||||||
FrameOfReferenceUID: frameOfReferenceUID,
|
FrameOfReferenceUID: frameOfReferenceUID,
|
||||||
referencedImageId: imageId,
|
referencedImageId: imageId,
|
||||||
|
displaySetInstanceUID: displaySet.displaySetInstanceUID,
|
||||||
},
|
},
|
||||||
data: {
|
data: {
|
||||||
label: measurement.labels?.[0]?.value || undefined,
|
label: measurement.labels?.[0]?.value || undefined,
|
||||||
@ -81,8 +132,11 @@ export default function addSRAnnotation(measurement, imageId, frameNumber) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* const annotationManager = annotation.annotationState.getAnnotationManager();
|
* Add the annotation to the annotation state manager.
|
||||||
* was not triggering annotation_added events.
|
* 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);
|
annotation.state.addAnnotation(SRAnnotation);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -55,7 +55,7 @@ const DEFAULT_TYPES = [GroupAccordion, Content, Trigger];
|
|||||||
* measurements panel for a practical, working example.
|
* measurements panel for a practical, working example.
|
||||||
*/
|
*/
|
||||||
export function AccordionGroup(props) {
|
export function AccordionGroup(props) {
|
||||||
const { grouping, items, children, sourceChildren, type } = props;
|
const { grouping, items, children, sourceChildren } = props;
|
||||||
const childProps = useSystem();
|
const childProps = useSystem();
|
||||||
let defaultValue = props.defaultValue;
|
let defaultValue = props.defaultValue;
|
||||||
const groups = grouping.groupingFunction(items, grouping, childProps);
|
const groups = grouping.groupingFunction(items, grouping, childProps);
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useActiveViewportDisplaySets, useSystem, utils } from '@ohif/core';
|
import { useActiveViewportDisplaySets, utils } from '@ohif/core';
|
||||||
// import { AccordionContent, AccordionItem, AccordionTrigger } from '@ohif/ui-next';
|
|
||||||
|
|
||||||
import { AccordionGroup } from './AccordionGroup';
|
import { AccordionGroup } from './AccordionGroup';
|
||||||
import MeasurementsOrAdditionalFindings from './MeasurementsOrAdditionalFindings';
|
import MeasurementsOrAdditionalFindings from './MeasurementsOrAdditionalFindings';
|
||||||
@ -63,8 +62,7 @@ export const groupByStudy = (items, grouping, childProps) => {
|
|||||||
export function StudyMeasurements(props): React.ReactNode {
|
export function StudyMeasurements(props): React.ReactNode {
|
||||||
const { items, grouping = {}, children } = props;
|
const { items, grouping = {}, children } = props;
|
||||||
|
|
||||||
const system = useSystem();
|
const activeDisplaySets = useActiveViewportDisplaySets();
|
||||||
const activeDisplaySets = useActiveViewportDisplaySets(system);
|
|
||||||
const activeStudyUID = activeDisplaySets?.[0]?.StudyInstanceUID;
|
const activeStudyUID = activeDisplaySets?.[0]?.StudyInstanceUID;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import debounce from 'lodash.debounce';
|
import debounce from 'lodash.debounce';
|
||||||
import { useSystem } from '@ohif/core';
|
import { useSystem } from '@ohif/core';
|
||||||
|
|
||||||
function mapMeasurementToDisplay(measurement, displaySetService) {
|
function mapMeasurementToDisplay(measurement, displaySetService) {
|
||||||
const { referenceSeriesUID } = measurement;
|
const { referenceSeriesUID } = measurement;
|
||||||
|
|
||||||
|
|||||||
@ -450,7 +450,7 @@ const connectMeasurementServiceToTools = ({
|
|||||||
if (measurement?.metadata?.referencedImageId) {
|
if (measurement?.metadata?.referencedImageId) {
|
||||||
imageId = measurement.metadata.referencedImageId;
|
imageId = measurement.metadata.referencedImageId;
|
||||||
frameNumber = getSOPInstanceAttributes(measurement.metadata.referencedImageId).frameNumber;
|
frameNumber = getSOPInstanceAttributes(measurement.metadata.referencedImageId).frameNumber;
|
||||||
} else {
|
} else if (instance) {
|
||||||
imageId = dataSource.getImageIdsForInstance({ instance });
|
imageId = dataSource.getImageIdsForInstance({ instance });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -78,6 +78,7 @@ export default function PanelMeasurement(props): React.ReactNode {
|
|||||||
);
|
);
|
||||||
return cloned;
|
return cloned;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Need to merge defaults on the content props to ensure they get passed to children
|
// Need to merge defaults on the content props to ensure they get passed to children
|
||||||
return <StudyMeasurements items={displayMeasurements} />;
|
return <StudyMeasurements items={displayMeasurements} />;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -13,7 +13,8 @@ import {
|
|||||||
import { useTrackedMeasurements } from '../getContextModule';
|
import { useTrackedMeasurements } from '../getContextModule';
|
||||||
import { UntrackSeriesModal } from './PanelStudyBrowserTracking/untrackSeriesModal';
|
import { UntrackSeriesModal } from './PanelStudyBrowserTracking/untrackSeriesModal';
|
||||||
|
|
||||||
const { filterMeasurementsBySeriesUID, filterAny } = utils.MeasurementFilters;
|
const { filterMeasurementsBySeriesUID, filterAny } =
|
||||||
|
utils.MeasurementFilters;
|
||||||
|
|
||||||
function PanelMeasurementTableTracking(props) {
|
function PanelMeasurementTableTracking(props) {
|
||||||
const [viewportGrid] = useViewportGrid();
|
const [viewportGrid] = useViewportGrid();
|
||||||
@ -22,7 +23,9 @@ function PanelMeasurementTableTracking(props) {
|
|||||||
|
|
||||||
const [trackedMeasurements, sendTrackedMeasurementsEvent] = useTrackedMeasurements();
|
const [trackedMeasurements, sendTrackedMeasurementsEvent] = useTrackedMeasurements();
|
||||||
const { trackedStudy, trackedSeries } = trackedMeasurements.context;
|
const { trackedStudy, trackedSeries } = trackedMeasurements.context;
|
||||||
const measurementFilter = trackedStudy ? filterMeasurementsBySeriesUID(trackedSeries) : filterAny;
|
const measurementFilter = trackedStudy
|
||||||
|
? filterMeasurementsBySeriesUID(trackedSeries)
|
||||||
|
: filterAny;
|
||||||
|
|
||||||
const onUntrackConfirm = () => {
|
const onUntrackConfirm = () => {
|
||||||
sendTrackedMeasurementsEvent('UNTRACK_ALL', {});
|
sendTrackedMeasurementsEvent('UNTRACK_ALL', {});
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import log from '../../log';
|
import log from '../../log';
|
||||||
import guid from '../../utils/guid';
|
import guid from '../../utils/guid';
|
||||||
import { PubSubService } from '../_shared/pubSubServiceInterface';
|
import { PubSubService } from '../_shared/pubSubServiceInterface';
|
||||||
|
import { DicomMetadataStore } from '../DicomMetadataStore/DicomMetadataStore';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Measurement source schema
|
* Measurement source schema
|
||||||
@ -127,7 +128,6 @@ class MeasurementService extends PubSubService {
|
|||||||
public readonly VALUE_TYPES = VALUE_TYPES;
|
public readonly VALUE_TYPES = VALUE_TYPES;
|
||||||
|
|
||||||
private measurements = new Map();
|
private measurements = new Map();
|
||||||
private unmappedMeasurements = new Map();
|
|
||||||
private isMeasurementDeletedIndividually: boolean;
|
private isMeasurementDeletedIndividually: boolean;
|
||||||
|
|
||||||
private sources = {};
|
private sources = {};
|
||||||
@ -495,6 +495,7 @@ class MeasurementService extends PubSubService {
|
|||||||
);
|
);
|
||||||
if (!sourceMapping) {
|
if (!sourceMapping) {
|
||||||
console.log('No source mapping', source.uid, annotationType, source);
|
console.log('No source mapping', source.uid, annotationType, source);
|
||||||
|
this.addUnmappedMeasurement(sourceAnnotationDetail, source);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const { toMeasurementSchema } = sourceMapping;
|
const { toMeasurementSchema } = sourceMapping;
|
||||||
@ -507,16 +508,7 @@ class MeasurementService extends PubSubService {
|
|||||||
|
|
||||||
measurement.source = source;
|
measurement.source = source;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Todo: handle other
|
this.addUnmappedMeasurement(sourceAnnotationDetail, source);
|
||||||
this.unmappedMeasurements.set(sourceAnnotationDetail.uid, {
|
|
||||||
...sourceAnnotationDetail,
|
|
||||||
source: {
|
|
||||||
name: source.name,
|
|
||||||
version: source.version,
|
|
||||||
uid: source.uid,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('Failed to map', error);
|
console.log('Failed to map', error);
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Failed to map '${sourceInfo}' measurement for annotationType ${annotationType}: ${error.message}`
|
`Failed to map '${sourceInfo}' measurement for annotationType ${annotationType}: ${error.message}`
|
||||||
@ -577,14 +569,98 @@ class MeasurementService extends PubSubService {
|
|||||||
return newMeasurement.uid;
|
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.
|
* Removes a measurement and broadcasts the removed event.
|
||||||
*
|
*
|
||||||
* @param {string} measurementUID The measurement uid
|
* @param {string} measurementUID The measurement uid
|
||||||
*/
|
*/
|
||||||
remove(measurementUID: string): void {
|
remove(measurementUID: string): void {
|
||||||
const measurement =
|
const measurement = this.measurements.get(measurementUID);
|
||||||
this.measurements.get(measurementUID) || this.unmappedMeasurements.get(measurementUID);
|
|
||||||
|
|
||||||
if (!measurementUID || !measurement) {
|
if (!measurementUID || !measurement) {
|
||||||
console.debug(`No uid provided, or unable to find measurement by uid.`);
|
console.debug(`No uid provided, or unable to find measurement by uid.`);
|
||||||
@ -593,7 +669,6 @@ class MeasurementService extends PubSubService {
|
|||||||
|
|
||||||
const source = measurement.source;
|
const source = measurement.source;
|
||||||
|
|
||||||
this.unmappedMeasurements.delete(measurementUID);
|
|
||||||
this.measurements.delete(measurementUID);
|
this.measurements.delete(measurementUID);
|
||||||
this.isMeasurementDeletedIndividually = true;
|
this.isMeasurementDeletedIndividually = true;
|
||||||
this._broadcastEvent(this.EVENTS.MEASUREMENT_REMOVED, {
|
this._broadcastEvent(this.EVENTS.MEASUREMENT_REMOVED, {
|
||||||
@ -609,14 +684,13 @@ class MeasurementService extends PubSubService {
|
|||||||
const measurements = [];
|
const measurements = [];
|
||||||
for (const measurementUID of measurementUIDs) {
|
for (const measurementUID of measurementUIDs) {
|
||||||
const measurement =
|
const measurement =
|
||||||
this.measurements.get(measurementUID) || this.unmappedMeasurements.get(measurementUID);
|
this.measurements.get(measurementUID)
|
||||||
|
|
||||||
if (!measurementUID || !measurement) {
|
if (!measurementUID || !measurement) {
|
||||||
console.debug(`No uid provided, or unable to find measurement by uid.`);
|
console.debug(`No uid provided, or unable to find measurement by uid.`);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.unmappedMeasurements.delete(measurementUID);
|
|
||||||
this.measurements.delete(measurementUID);
|
this.measurements.delete(measurementUID);
|
||||||
measurements.push(measurement);
|
measurements.push(measurement);
|
||||||
}
|
}
|
||||||
@ -634,11 +708,7 @@ class MeasurementService extends PubSubService {
|
|||||||
public clearMeasurements(filter?: MeasurementFilter) {
|
public clearMeasurements(filter?: MeasurementFilter) {
|
||||||
// Make a copy of the measurements
|
// Make a copy of the measurements
|
||||||
const toClear = this.getMeasurements(filter);
|
const toClear = this.getMeasurements(filter);
|
||||||
const unmappedClear = filter
|
const measurements = [...toClear];
|
||||||
? [...this.unmappedMeasurements.values()].filter(filter)
|
|
||||||
: this.unmappedMeasurements;
|
|
||||||
const measurements = [...toClear, ...unmappedClear];
|
|
||||||
unmappedClear.forEach(measurement => this.unmappedMeasurements.delete(measurement.uid));
|
|
||||||
toClear.forEach(measurement => this.measurements.delete(measurement.uid));
|
toClear.forEach(measurement => this.measurements.delete(measurement.uid));
|
||||||
this._broadcastEvent(this.EVENTS.MEASUREMENTS_CLEARED, { measurements });
|
this._broadcastEvent(this.EVENTS.MEASUREMENTS_CLEARED, { measurements });
|
||||||
}
|
}
|
||||||
@ -657,7 +727,8 @@ class MeasurementService extends PubSubService {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
public jumpToMeasurement(viewportId: string, measurementUID: string): void {
|
public jumpToMeasurement(viewportId: string, measurementUID: string): void {
|
||||||
const measurement = this.measurements.get(measurementUID);
|
const measurement =
|
||||||
|
this.measurements.get(measurementUID)
|
||||||
|
|
||||||
if (!measurement) {
|
if (!measurement) {
|
||||||
log.warn(`No measurement uid, or unable to find by uid.`);
|
log.warn(`No measurement uid, or unable to find by uid.`);
|
||||||
@ -779,7 +850,8 @@ class MeasurementService extends PubSubService {
|
|||||||
};
|
};
|
||||||
|
|
||||||
public toggleLockMeasurement(measurementUID: string): void {
|
public toggleLockMeasurement(measurementUID: string): void {
|
||||||
const measurement = this.measurements.get(measurementUID);
|
const measurement =
|
||||||
|
this.measurements.get(measurementUID)
|
||||||
|
|
||||||
if (!measurement) {
|
if (!measurement) {
|
||||||
console.debug(`No measurement found for uid: ${measurementUID}`);
|
console.debug(`No measurement found for uid: ${measurementUID}`);
|
||||||
@ -796,7 +868,8 @@ class MeasurementService extends PubSubService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public toggleVisibilityMeasurement(measurementUID: string, visibility?: boolean): void {
|
public toggleVisibilityMeasurement(measurementUID: string, visibility?: boolean): void {
|
||||||
const measurement = this.measurements.get(measurementUID);
|
const measurement =
|
||||||
|
this.measurements.get(measurementUID)
|
||||||
|
|
||||||
if (!measurement) {
|
if (!measurement) {
|
||||||
console.debug(`No measurement found for uid: ${measurementUID}`);
|
console.debug(`No measurement found for uid: ${measurementUID}`);
|
||||||
@ -820,7 +893,8 @@ class MeasurementService extends PubSubService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public updateColorMeasurement(measurementUID: string, color: number[]): void {
|
public updateColorMeasurement(measurementUID: string, color: number[]): void {
|
||||||
const measurement = this.measurements.get(measurementUID);
|
const measurement =
|
||||||
|
this.measurements.get(measurementUID)
|
||||||
|
|
||||||
if (!measurement) {
|
if (!measurement) {
|
||||||
console.debug(`No measurement found for uid: ${measurementUID}`);
|
console.debug(`No measurement found for uid: ${measurementUID}`);
|
||||||
|
|||||||
@ -10,6 +10,7 @@ export function filterMeasurementsBySeriesUID(selectedSeries: string[]) {
|
|||||||
return measurement => selectedSeries.includes(measurement.referenceSeriesUID);
|
return measurement => selectedSeries.includes(measurement.referenceSeriesUID);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** A filter that filters for measurements belonging to the study */
|
||||||
export function filterMeasurementsByStudyUID(studyUID) {
|
export function filterMeasurementsByStudyUID(studyUID) {
|
||||||
return measurement => measurement.referenceStudyUID == studyUID;
|
return measurement => measurement.referenceStudyUID == studyUID;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -18,6 +18,7 @@ import { cn } from '../../lib/utils';
|
|||||||
* @component
|
* @component
|
||||||
* @example
|
* @example
|
||||||
* ```tsx
|
* ```tsx
|
||||||
|
* // Basic usage without status
|
||||||
* <DataRow
|
* <DataRow
|
||||||
* number={1}
|
* number={1}
|
||||||
* title="My Item"
|
* title="My Item"
|
||||||
@ -33,6 +34,29 @@ import { cn } from '../../lib/utils';
|
|||||||
* onDelete={() => {}}
|
* onDelete={() => {}}
|
||||||
* onColor={() => {}}
|
* onColor={() => {}}
|
||||||
* />
|
* />
|
||||||
|
*
|
||||||
|
* // With warning status using composite pattern
|
||||||
|
* <DataRow
|
||||||
|
* // ... other props
|
||||||
|
* >
|
||||||
|
* <DataRow.Status.Warning tooltip="This structured report is not compatible with this application" />
|
||||||
|
* </DataRow>
|
||||||
|
*
|
||||||
|
* // With success status using composite pattern
|
||||||
|
* <DataRow
|
||||||
|
* // ... other props
|
||||||
|
* >
|
||||||
|
* <DataRow.Status.Success tooltip="Measurement completed successfully" />
|
||||||
|
* </DataRow>
|
||||||
|
*
|
||||||
|
* // Multiple status indicators
|
||||||
|
* <DataRow
|
||||||
|
* // ... other props
|
||||||
|
* >
|
||||||
|
* <DataRow.Status.Warning tooltip="Warning message" />
|
||||||
|
* <DataRow.Status.Info tooltip="Additional info" />
|
||||||
|
* </DataRow>
|
||||||
|
*
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@ -55,6 +79,7 @@ import { cn } from '../../lib/utils';
|
|||||||
* @property {() => void} onRename - Callback when rename is requested
|
* @property {() => void} onRename - Callback when rename is requested
|
||||||
* @property {() => void} onDelete - Callback when delete is requested
|
* @property {() => void} onDelete - Callback when delete is requested
|
||||||
* @property {() => void} onColor - Callback when color change is requested
|
* @property {() => void} onColor - Callback when color change is requested
|
||||||
|
* @property {React.ReactNode} children - Optional children, including Status components
|
||||||
*/
|
*/
|
||||||
interface DataRowProps {
|
interface DataRowProps {
|
||||||
number: number | null;
|
number: number | null;
|
||||||
@ -79,9 +104,10 @@ interface DataRowProps {
|
|||||||
colorHex?: string;
|
colorHex?: string;
|
||||||
onColor: (e) => void;
|
onColor: (e) => void;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
children?: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DataRow: React.FC<DataRowProps> = ({
|
const DataRowComponent: React.FC<DataRowProps> = ({
|
||||||
number,
|
number,
|
||||||
title,
|
title,
|
||||||
colorHex,
|
colorHex,
|
||||||
@ -97,11 +123,20 @@ export const DataRow: React.FC<DataRowProps> = ({
|
|||||||
isVisible = true,
|
isVisible = true,
|
||||||
disableEditing = false,
|
disableEditing = false,
|
||||||
className,
|
className,
|
||||||
|
children,
|
||||||
}) => {
|
}) => {
|
||||||
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
|
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
|
||||||
const isTitleLong = title?.length > 25;
|
const isTitleLong = title?.length > 25;
|
||||||
const rowRef = useRef<HTMLDivElement>(null);
|
const rowRef = useRef<HTMLDivElement>(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(() => {
|
// useEffect(() => {
|
||||||
// if (isSelected && rowRef.current) {
|
// if (isSelected && rowRef.current) {
|
||||||
// setTimeout(() => {
|
// setTimeout(() => {
|
||||||
@ -281,6 +316,9 @@ export const DataRow: React.FC<DataRowProps> = ({
|
|||||||
{/* Lock Icon (if needed) */}
|
{/* Lock Icon (if needed) */}
|
||||||
{isLocked && !disableEditing && <Icons.Lock className="text-muted-foreground h-6 w-6" />}
|
{isLocked && !disableEditing && <Icons.Lock className="text-muted-foreground h-6 w-6" />}
|
||||||
|
|
||||||
|
{/* Status Components */}
|
||||||
|
{statusComponents}
|
||||||
|
|
||||||
{/* Actions Dropdown Menu */}
|
{/* Actions Dropdown Menu */}
|
||||||
{disableEditing && <div className="h-6 w-6"></div>}
|
{disableEditing && <div className="h-6 w-6"></div>}
|
||||||
{!disableEditing && (
|
{!disableEditing && (
|
||||||
@ -369,4 +407,94 @@ export const DataRow: React.FC<DataRowProps> = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
interface StatusProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface StatusIndicatorProps {
|
||||||
|
tooltip?: string;
|
||||||
|
icon: React.ReactNode;
|
||||||
|
defaultTooltip: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const StatusIndicator: React.FC<StatusIndicatorProps> = ({ tooltip, icon, defaultTooltip }) => (
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<div className="flex h-6 w-6 items-center justify-center">{icon}</div>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent side="bottom">
|
||||||
|
<div>{tooltip || defaultTooltip}</div>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
|
||||||
|
const Status: React.FC<StatusProps> & {
|
||||||
|
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 }) => (
|
||||||
|
<StatusIndicator
|
||||||
|
tooltip={tooltip}
|
||||||
|
icon={
|
||||||
|
<Icons.ByName
|
||||||
|
name="status-alert"
|
||||||
|
className="h-4 w-4 text-yellow-500"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
defaultTooltip="Warning"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
const StatusSuccess: React.FC<{ tooltip?: string }> = ({ tooltip }) => (
|
||||||
|
<StatusIndicator
|
||||||
|
tooltip={tooltip}
|
||||||
|
icon={<Icons.Checked className="h-4 w-4 text-green-500" />}
|
||||||
|
defaultTooltip="Success"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
const StatusError: React.FC<{ tooltip?: string }> = ({ tooltip }) => (
|
||||||
|
<StatusIndicator
|
||||||
|
tooltip={tooltip}
|
||||||
|
icon={
|
||||||
|
<Icons.ByName
|
||||||
|
name="status-error"
|
||||||
|
className="h-4 w-4 text-red-500"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
defaultTooltip="Error"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
const StatusInfo: React.FC<{ tooltip?: string }> = ({ tooltip }) => (
|
||||||
|
<StatusIndicator
|
||||||
|
tooltip={tooltip}
|
||||||
|
icon={<Icons.Info className="h-4 w-4 text-blue-500" />}
|
||||||
|
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<DataRowProps> & {
|
||||||
|
Status: typeof Status;
|
||||||
|
};
|
||||||
|
|
||||||
|
DataRow.Status = Status;
|
||||||
|
|
||||||
export default DataRow;
|
export default DataRow;
|
||||||
|
export { DataRow };
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
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';
|
import { createContext } from '../../lib/createContext';
|
||||||
|
|
||||||
interface MeasurementTableContext {
|
interface MeasurementTableContext {
|
||||||
@ -11,7 +12,7 @@ interface MeasurementTableContext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const [MeasurementTableProvider, useMeasurementTableContext] =
|
const [MeasurementTableProvider, useMeasurementTableContext] =
|
||||||
createContext<MeasurementTableContext>('MeasurementTable', { data: [] });
|
createContext<MeasurementTableContext>('MeasurementTable', { data: [], isExpanded: true });
|
||||||
|
|
||||||
interface MeasurementDataProps extends MeasurementTableContext {
|
interface MeasurementDataProps extends MeasurementTableContext {
|
||||||
title: string;
|
title: string;
|
||||||
@ -91,6 +92,8 @@ interface MeasurementItem {
|
|||||||
isLocked: boolean;
|
isLocked: boolean;
|
||||||
toolName: string;
|
toolName: string;
|
||||||
isExpanded: boolean;
|
isExpanded: boolean;
|
||||||
|
isUnmapped?: boolean;
|
||||||
|
statusTooltip?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface RowProps {
|
interface RowProps {
|
||||||
@ -117,11 +120,15 @@ const Row = ({ item, index }: RowProps) => {
|
|||||||
onRename={e => onAction(e, 'renameMeasurement', uid)}
|
onRename={e => onAction(e, 'renameMeasurement', uid)}
|
||||||
onToggleVisibility={e => onAction(e, 'toggleVisibilityMeasurement', uid)}
|
onToggleVisibility={e => onAction(e, 'toggleVisibilityMeasurement', uid)}
|
||||||
onToggleLocked={e => onAction(e, 'toggleLockMeasurement', uid)}
|
onToggleLocked={e => onAction(e, 'toggleLockMeasurement', uid)}
|
||||||
|
onColor={e => onAction(e, 'changeMeasurementColor', uid)}
|
||||||
disableEditing={disableEditing}
|
disableEditing={disableEditing}
|
||||||
isExpanded={isExpanded}
|
|
||||||
isVisible={item.isVisible}
|
isVisible={item.isVisible}
|
||||||
isLocked={item.isLocked}
|
isLocked={item.isLocked}
|
||||||
/>
|
>
|
||||||
|
{item.isUnmapped && (
|
||||||
|
<DataRow.Status.Warning tooltip={item.statusTooltip} />
|
||||||
|
)}
|
||||||
|
</DataRow>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user