import { utils, classes, DisplaySetService, Types as OhifTypes } from '@ohif/core'; import i18n from '@ohif/i18n'; import { Enums as CSExtensionEnums } from '@ohif/extension-cornerstone'; import { adaptersSR } from '@cornerstonejs/adapters'; import addSRAnnotation from './utils/addSRAnnotation'; import isRehydratable from './utils/isRehydratable'; import { SOPClassHandlerName, SOPClassHandlerId, SOPClassHandlerId3D, SOPClassHandlerName3D, } from './id'; import { CodeNameCodeSequenceValues, CodingSchemeDesignators } from './enums'; const { sopClassDictionary } = utils; const { CORNERSTONE_3D_TOOLS_SOURCE_NAME, CORNERSTONE_3D_TOOLS_SOURCE_VERSION } = CSExtensionEnums; const { MetadataProvider: metadataProvider } = classes; const { TEXT_ANNOTATION_POSITION, COMMENT_CODE, CodeScheme: Cornerstone3DCodeScheme, } = adaptersSR.Cornerstone3D; type InstanceMetadata = Types.InstanceMetadata; /** * TODO * - [ ] Add SR thumbnail * - [ ] Make viewport * - [ ] Get stacks from referenced displayInstanceUID and load into wrapped CornerStone viewport */ const sopClassUids = [ sopClassDictionary.BasicTextSR, sopClassDictionary.EnhancedSR, sopClassDictionary.ComprehensiveSR, ]; const validateSameStudyUID = (uid: string, instances): void => { instances.forEach(it => { if (it.StudyInstanceUID !== uid) { console.warn('Not all instances have the same UID', uid, it); throw new Error(`Instances ${it.SOPInstanceUID} does not belong to ${uid}`); } }); }; /** * Adds instances to the DICOM SR series, rather than creating a new * series, so that as SR's are saved, they append to the series, and the * key image display set gets updated as well, containing just the new series. * @param instances is a list of instances from THIS series that are not * in this DICOM SR Display Set already. */ function addInstances(instances: InstanceMetadata[], displaySetService: DisplaySetService) { this.instances.push(...instances); utils.sortStudyInstances(this.instances); // The last instance is the newest one, so is the one most interesting. // Eventually, the SR viewer should have the ability to choose which SR // gets loaded, and to navigate among them. this.instance = this.instances[this.instances.length - 1]; this.isLoaded = false; return this; } /** * 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 */ function _getDisplaySetsFromSeries( instances, servicesManager: AppTypes.ServicesManager, extensionManager ) { // If the series has no instances, stop here if (!instances || !instances.length) { throw new Error('No instances were provided'); } utils.sortStudyInstances(instances); // The last instance is the newest one, so is the one most interesting. // Eventually, the SR viewer should have the ability to choose which SR // gets loaded, and to navigate among them. const instance = instances[instances.length - 1]; const { StudyInstanceUID, SeriesInstanceUID, SOPInstanceUID, SeriesDescription, SeriesNumber, SeriesDate, SeriesTime, ConceptNameCodeSequence, SOPClassUID, } = instance; validateSameStudyUID(instance.StudyInstanceUID, instances); const is3DSR = SOPClassUID === sopClassDictionary.Comprehensive3DSR; const isImagingMeasurementReport = ConceptNameCodeSequence?.CodeValue === CodeNameCodeSequenceValues.ImagingMeasurementReport; const displaySet = { Modality: 'SR', displaySetInstanceUID: utils.guid(), SeriesDescription, SeriesNumber, SeriesDate, SeriesTime, SOPInstanceUID, SeriesInstanceUID, StudyInstanceUID, SOPClassHandlerId: is3DSR ? SOPClassHandlerId3D : SOPClassHandlerId, SOPClassUID, instances, referencedImages: null, measurements: null, isDerivedDisplaySet: true, isLoaded: false, isImagingMeasurementReport, sopClassUids, instance, addInstances, label: SeriesDescription || `${i18n.t('Series')} ${SeriesNumber} - ${i18n.t('SR')}`, }; displaySet.load = () => _load(displaySet, servicesManager, extensionManager); return [displaySet]; } /** * Loads the display set with the given services and extension manager. * @param srDisplaySet - The display set to load. * @param servicesManager - The services manager containing displaySetService and measurementService. * @param extensionManager - The extension manager containing data sources. */ async function _load( srDisplaySet: Types.DisplaySet, servicesManager: AppTypes.ServicesManager, extensionManager: AppTypes.ExtensionManager ) { const { displaySetService, measurementService } = servicesManager.services; const dataSources = extensionManager.getDataSources(); const dataSource = dataSources[0]; const { ContentSequence } = srDisplaySet.instance; async function retrieveBulkData(obj, parentObj = null, key = null) { for (const prop in obj) { if (typeof obj[prop] === 'object' && obj[prop] !== null) { await retrieveBulkData(obj[prop], obj, prop); } else if (Array.isArray(obj[prop])) { await Promise.all(obj[prop].map(item => retrieveBulkData(item, obj, prop))); } else if (prop === 'BulkDataURI') { const value = await dataSource.retrieve.bulkDataURI({ BulkDataURI: obj[prop], StudyInstanceUID: srDisplaySet.instance.StudyInstanceUID, SeriesInstanceUID: srDisplaySet.instance.SeriesInstanceUID, SOPInstanceUID: srDisplaySet.instance.SOPInstanceUID, }); if (parentObj && key) { parentObj[key] = new Float32Array(value); } } } } if (srDisplaySet.isLoaded !== true) { await retrieveBulkData(ContentSequence); } if (srDisplaySet.isImagingMeasurementReport) { srDisplaySet.referencedImages = _getReferencedImagesList(ContentSequence); srDisplaySet.measurements = _getMeasurements(ContentSequence); } else { srDisplaySet.referencedImages = []; srDisplaySet.measurements = []; } const mappings = measurementService.getSourceMappings( CORNERSTONE_3D_TOOLS_SOURCE_NAME, CORNERSTONE_3D_TOOLS_SOURCE_VERSION ); srDisplaySet.isHydrated = false; srDisplaySet.isRehydratable = isRehydratable(srDisplaySet, mappings); srDisplaySet.isLoaded = true; /** Check currently added displaySets and add measurements if the sources exist */ displaySetService.activeDisplaySets.forEach(activeDisplaySet => { _checkIfCanAddMeasurementsToDisplaySet( srDisplaySet, activeDisplaySet, dataSource, servicesManager ); }); /** Subscribe to new displaySets as the source may come in after */ displaySetService.subscribe(displaySetService.EVENTS.DISPLAY_SETS_ADDED, data => { const { displaySetsAdded } = data; /** * 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( srDisplaySet, newDisplaySet, dataSource, servicesManager ); }); }); } function _measurementBelongsToDisplaySet({ measurement, displaySet }) { return ( measurement.coords[0].ReferencedFrameOfReferenceSequence === displaySet.FrameOfReferenceUID ); } /** * 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, dataSource, servicesManager: AppTypes.ServicesManager ) { const { customizationService } = servicesManager.services; const unloadedMeasurements = srDisplaySet.measurements.filter( measurement => measurement.loaded === false ); if (!unloadedMeasurements.length || newDisplaySet.unsupported) { return; } // Create a Map to efficiently look up ImageIds by SOPInstanceUID and frame number const imageIdMap = new Map(); const imageIds = dataSource.getImageIdsForDisplaySet(newDisplaySet); for (const imageId of imageIds) { const { SOPInstanceUID, frameNumber } = metadataProvider.getUIDsFromImageID(imageId); const key = `${SOPInstanceUID}:${frameNumber || 1}`; imageIdMap.set(key, imageId); } if (!unloadedMeasurements?.length) { return; } const is3DSR = srDisplaySet.SOPClassUID === sopClassDictionary.Comprehensive3DSR; for (let j = unloadedMeasurements.length - 1; j >= 0; j--) { let measurement = unloadedMeasurements[j]; const is3DMeasurement = measurement.coords?.[0]?.ValueType === 'SCOORD3D'; const onBeforeSRAddMeasurement = customizationService.getCustomization( 'onBeforeSRAddMeasurement' ); if (typeof onBeforeSRAddMeasurement === 'function') { measurement = onBeforeSRAddMeasurement({ measurement, StudyInstanceUID: srDisplaySet.StudyInstanceUID, SeriesInstanceUID: srDisplaySet.SeriesInstanceUID, }); } // if it is 3d SR we can just add the SR annotation if ( is3DSR && is3DMeasurement && _measurementBelongsToDisplaySet({ measurement, displaySet: newDisplaySet }) ) { addSRAnnotation(measurement, null, null); measurement.loaded = true; measurement.displaySetInstanceUID = newDisplaySet.displaySetInstanceUID; unloadedMeasurements.splice(j, 1); continue; } const referencedSOPSequence = measurement.coords[0].ReferencedSOPSequence; if (!referencedSOPSequence) { continue; } const { ReferencedSOPInstanceUID } = referencedSOPSequence; const frame = referencedSOPSequence.ReferencedFrameNumber || 1; const key = `${ReferencedSOPInstanceUID}:${frame}`; const imageId = imageIdMap.get(key); if ( imageId && _measurementReferencesSOPInstanceUID(measurement, ReferencedSOPInstanceUID, frame) ) { addSRAnnotation(measurement, imageId, frame); // Update measurement properties measurement.loaded = true; measurement.imageId = imageId; measurement.displaySetInstanceUID = newDisplaySet.displaySetInstanceUID; measurement.ReferencedSOPInstanceUID = ReferencedSOPInstanceUID; measurement.frameNumber = frame; unloadedMeasurements.splice(j, 1); } } } /** * 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. */ function _measurementReferencesSOPInstanceUID(measurement, SOPInstanceUID, frameNumber) { const { coords } = measurement; /** * NOTE: The ReferencedFrameNumber can be multiple values according to the DICOM * Standard. But for now, we will support only one ReferenceFrameNumber. */ const ReferencedFrameNumber = (measurement.coords[0].ReferencedSOPSequence && measurement.coords[0].ReferencedSOPSequence?.ReferencedFrameNumber) || 1; if (frameNumber && Number(frameNumber) !== Number(ReferencedFrameNumber)) { return false; } for (let j = 0; j < coords.length; j++) { const coord = coords[j]; const { ReferencedSOPInstanceUID } = coord.ReferencedSOPSequence; if (ReferencedSOPInstanceUID === SOPInstanceUID) { return true; } } return false; } /** * 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. */ function getSopClassHandlerModule(params: OhifTypes.Extensions.ExtensionParams) { const { servicesManager, extensionManager } = params; const getDisplaySetsFromSeries = instances => { return _getDisplaySetsFromSeries(instances, servicesManager, extensionManager); }; return [ { name: SOPClassHandlerName, sopClassUids, getDisplaySetsFromSeries, }, { name: SOPClassHandlerName3D, sopClassUids: [sopClassDictionary.Comprehensive3DSR], getDisplaySetsFromSeries, }, ]; } /** * Retrieves the measurements from the ImagingMeasurementReportContentSequence. * * @param {Array} ImagingMeasurementReportContentSequence - The ImagingMeasurementReportContentSequence array. * @returns {Array} - The array of measurements. */ function _getMeasurements(ImagingMeasurementReportContentSequence) { const ImagingMeasurements = ImagingMeasurementReportContentSequence.find( item => item.ConceptNameCodeSequence.CodeValue === CodeNameCodeSequenceValues.ImagingMeasurements ); if (!ImagingMeasurements) { return []; } const MeasurementGroups = _getSequenceAsArray(ImagingMeasurements.ContentSequence).filter( item => item.ConceptNameCodeSequence.CodeValue === CodeNameCodeSequenceValues.MeasurementGroup ); const mergedContentSequencesByTrackingUniqueIdentifiers = _getMergedContentSequencesByTrackingUniqueIdentifiers(MeasurementGroups); const measurements = []; Object.keys(mergedContentSequencesByTrackingUniqueIdentifiers).forEach( trackingUniqueIdentifier => { const mergedContentSequence = mergedContentSequencesByTrackingUniqueIdentifiers[trackingUniqueIdentifier]; const measurement = _processMeasurement(mergedContentSequence); if (measurement) { measurements.push(measurement); } } ); return measurements; } /** * Retrieves merged content sequences by tracking unique identifiers. * * @param {Array} MeasurementGroups - The measurement groups. * @returns {Object} - The merged content sequences by tracking unique identifiers. */ function _getMergedContentSequencesByTrackingUniqueIdentifiers(MeasurementGroups) { const mergedContentSequencesByTrackingUniqueIdentifiers = {}; MeasurementGroups.forEach(MeasurementGroup => { const ContentSequence = _getSequenceAsArray(MeasurementGroup.ContentSequence); const TrackingUniqueIdentifierItem = ContentSequence.find( item => item.ConceptNameCodeSequence.CodeValue === CodeNameCodeSequenceValues.TrackingUniqueIdentifier ); if (!TrackingUniqueIdentifierItem) { console.warn('No Tracking Unique Identifier, skipping ambiguous measurement.'); } const trackingUniqueIdentifier = TrackingUniqueIdentifierItem.UID; if (mergedContentSequencesByTrackingUniqueIdentifiers[trackingUniqueIdentifier] === undefined) { // Add the full ContentSequence mergedContentSequencesByTrackingUniqueIdentifiers[trackingUniqueIdentifier] = [ ...ContentSequence, ]; } else { // Add the ContentSequence minus the tracking identifier, as we have this // Information in the merged ContentSequence anyway. ContentSequence.forEach(item => { if ( item.ConceptNameCodeSequence.CodeValue !== CodeNameCodeSequenceValues.TrackingUniqueIdentifier ) { mergedContentSequencesByTrackingUniqueIdentifiers[trackingUniqueIdentifier].push(item); } }); } }); return mergedContentSequencesByTrackingUniqueIdentifiers; } /** * Processes the measurement based on the merged content sequence. * If the merged content sequence contains SCOORD or SCOORD3D value types, * 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. */ function _processMeasurement(mergedContentSequence) { if (mergedContentSequence.some(group => isScoordOr3d(group) && !isTextPosition(group))) { return _processTID1410Measurement(mergedContentSequence); } return _processNonGeometricallyDefinedMeasurement(mergedContentSequence); } /** * Processes TID 1410 style measurements from the 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. */ function _processTID1410Measurement(mergedContentSequence) { // Need to deal with TID 1410 style measurements, which will have a SCOORD or SCOORD3D at the top level, // And non-geometric representations where each NUM has "INFERRED FROM" SCOORD/SCOORD3D const graphicItem = mergedContentSequence.find( group => group.ValueType === 'SCOORD' || group.ValueType === 'SCOORD3D' ); const UIDREFContentItem = mergedContentSequence.find(group => group.ValueType === 'UIDREF'); const TrackingIdentifierContentItem = mergedContentSequence.find( item => item.ConceptNameCodeSequence.CodeValue === CodeNameCodeSequenceValues.TrackingIdentifier ); if (!graphicItem) { console.warn( `graphic ValueType ${graphicItem.ValueType} not currently supported, skipping annotation.` ); return; } const NUMContentItems = mergedContentSequence.filter(group => group.ValueType === 'NUM'); const { ConceptNameCodeSequence: conceptNameItem } = graphicItem; const { CodeValue: graphicValue, CodingSchemeDesignator: graphicDesignator } = conceptNameItem; const graphicCode = `${graphicDesignator}:${graphicValue}`; const pointDataItem = _getCoordsFromSCOORDOrSCOORD3D(graphicItem); const is3DMeasurement = pointDataItem.ValueType === 'SCOORD3D'; const pointLength = is3DMeasurement ? 3 : 2; const pointsLength = pointDataItem.GraphicData.length / pointLength; const measurement = { loaded: false, labels: [], coords: [pointDataItem], TrackingUniqueIdentifier: UIDREFContentItem.UID, TrackingIdentifier: TrackingIdentifierContentItem.TextValue, graphicCode, is3DMeasurement, pointsLength, graphicType: pointDataItem.GraphicType, }; NUMContentItems.forEach(item => { const { ConceptNameCodeSequence, MeasuredValueSequence } = item; if (MeasuredValueSequence) { measurement.labels.push( _getLabelFromMeasuredValueSequence(ConceptNameCodeSequence, MeasuredValueSequence) ); } }); const findingSites = mergedContentSequence.filter( item => item.ConceptNameCodeSequence.CodingSchemeDesignator === CodingSchemeDesignators.SCT && item.ConceptNameCodeSequence.CodeValue === CodeNameCodeSequenceValues.FindingSiteSCT ); if (findingSites.length) { measurement.labels.push({ label: CodeNameCodeSequenceValues.FindingSiteSCT, value: findingSites[0].ConceptCodeSequence.CodeMeaning, }); } return measurement; } /** * 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. */ function _processNonGeometricallyDefinedMeasurement(mergedContentSequence) { const NUMContentItems = mergedContentSequence.filter(group => group.ValueType === 'NUM'); const UIDREFContentItem = mergedContentSequence.find(group => group.ValueType === 'UIDREF'); const TrackingIdentifierContentItem = mergedContentSequence.find( item => item.ConceptNameCodeSequence.CodeValue === CodeNameCodeSequenceValues.TrackingIdentifier ); const finding = mergedContentSequence.find( item => item.ConceptNameCodeSequence.CodeValue === CodeNameCodeSequenceValues.Finding ); const findingSites = mergedContentSequence.filter( item => item.ConceptNameCodeSequence.CodingSchemeDesignator === CodingSchemeDesignators.SRT && item.ConceptNameCodeSequence.CodeValue === CodeNameCodeSequenceValues.FindingSite ); const commentSites = mergedContentSequence.filter( item => item.ConceptNameCodeSequence.CodingSchemeDesignator === COMMENT_CODE.schemeDesignator && item.ConceptNameCodeSequence.CodeValue === COMMENT_CODE.value ); const measurement = { loaded: false, labels: [], coords: [], TrackingUniqueIdentifier: UIDREFContentItem.UID, TrackingIdentifier: TrackingIdentifierContentItem.TextValue, }; if (commentSites) { for (const group of commentSites) { if (group.TextValue) { measurement.labels.push({ label: group.TextValue, value: '' }); } } } if ( finding && CodingSchemeDesignators.CornerstoneCodeSchemes.includes( finding.ConceptCodeSequence.CodingSchemeDesignator ) && finding.ConceptCodeSequence.CodeValue === Cornerstone3DCodeScheme.codeValues.CORNERSTONEFREETEXT ) { measurement.labels.push({ label: Cornerstone3DCodeScheme.codeValues.CORNERSTONEFREETEXT, value: finding.ConceptCodeSequence.CodeMeaning, }); } // TODO -> Eventually hopefully support SNOMED or some proper code library, just free text for now. if (findingSites.length) { const cornerstoneFreeTextFindingSite = findingSites.find( FindingSite => CodingSchemeDesignators.CornerstoneCodeSchemes.includes( FindingSite.ConceptCodeSequence.CodingSchemeDesignator ) && FindingSite.ConceptCodeSequence.CodeValue === Cornerstone3DCodeScheme.codeValues.CORNERSTONEFREETEXT ); if (cornerstoneFreeTextFindingSite) { measurement.labels.push({ label: Cornerstone3DCodeScheme.codeValues.CORNERSTONEFREETEXT, value: cornerstoneFreeTextFindingSite.ConceptCodeSequence.CodeMeaning, }); } } NUMContentItems.forEach(item => { const { ConceptNameCodeSequence, ContentSequence, MeasuredValueSequence } = item; const { ValueType } = ContentSequence; if (!ValueType === 'SCOORD') { console.warn(`Graphic ${ValueType} not currently supported, skipping annotation.`); return; } const coords = _getCoordsFromSCOORDOrSCOORD3D(ContentSequence); if (coords) { measurement.coords.push(coords); } if (MeasuredValueSequence) { measurement.labels.push( _getLabelFromMeasuredValueSequence(ConceptNameCodeSequence, MeasuredValueSequence) ); } }); return measurement; } /** * 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. */ const _getCoordsFromSCOORDOrSCOORD3D = graphicItem => { const { ValueType, GraphicType, GraphicData } = graphicItem; const coords = { ValueType, GraphicType, GraphicData }; coords.ReferencedSOPSequence = graphicItem.ContentSequence?.ReferencedSOPSequence; coords.ReferencedFrameOfReferenceSequence = graphicItem.ReferencedFrameOfReferenceUID || graphicItem.ContentSequence?.ReferencedFrameOfReferenceSequence; return coords; }; /** * 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. * 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' } */ function _getLabelFromMeasuredValueSequence(ConceptNameCodeSequence, MeasuredValueSequence) { const { CodeMeaning } = ConceptNameCodeSequence; const { NumericValue, MeasurementUnitsCodeSequence } = MeasuredValueSequence; const { CodeValue } = MeasurementUnitsCodeSequence; const formatedNumericValue = NumericValue ? Number(NumericValue).toFixed(2) : ''; return { label: CodeMeaning, value: `${formatedNumericValue} ${CodeValue}`, }; // E.g. Long Axis: 31.0 mm } /** * 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. */ function _getReferencedImagesList(ImagingMeasurementReportContentSequence) { const ImageLibrary = ImagingMeasurementReportContentSequence.find( item => item.ConceptNameCodeSequence.CodeValue === CodeNameCodeSequenceValues.ImageLibrary ); if (!ImageLibrary) { return []; } const ImageLibraryGroup = _getSequenceAsArray(ImageLibrary.ContentSequence).find( item => item.ConceptNameCodeSequence.CodeValue === CodeNameCodeSequenceValues.ImageLibraryGroup ); if (!ImageLibraryGroup) { return []; } const referencedImages = []; _getSequenceAsArray(ImageLibraryGroup.ContentSequence).forEach(item => { const { ReferencedSOPSequence } = item; if (!ReferencedSOPSequence) { return; } for (const ref of _getSequenceAsArray(ReferencedSOPSequence)) { if (ref.ReferencedSOPClassUID) { const { ReferencedSOPClassUID, ReferencedSOPInstanceUID } = ref; referencedImages.push({ ReferencedSOPClassUID, ReferencedSOPInstanceUID, }); } } }); return referencedImages; } /** * Converts a DICOM sequence to an array. * If the sequence is null or undefined, an empty array is returned. * If the sequence is already an array, it is returned as is. * Otherwise, the sequence is wrapped in an array and returned. * * @param {any} sequence - The DICOM sequence to convert. * @returns {any[]} - The converted array. */ function _getSequenceAsArray(sequence) { if (!sequence) { return []; } return Array.isArray(sequence) ? sequence : [sequence]; } function isScoordOr3d(group) { return group.ValueType === 'SCOORD' || group.ValueType === 'SCOORD3D'; } function isTextPosition(group) { const concept = group.ConceptNameCodeSequence[0]; return ( concept && concept.CodeValue === TEXT_ANNOTATION_POSITION.value && concept.CodingSchemeDesignator === TEXT_ANNOTATION_POSITION.schemeDesignator ); } export default getSopClassHandlerModule;