Files
ohif-viewer/extensions/cornerstone-dicom-sr/src/getSopClassHandlerModule.ts
T

585 lines
19 KiB
TypeScript
Raw Normal View History

2020-07-03 19:18:59 +02:00
import { SOPClassHandlerName, SOPClassHandlerId } from './id';
import { utils, classes, DisplaySetService, Types } from '@ohif/core';
2020-07-01 11:08:56 +02:00
import addMeasurement from './utils/addMeasurement';
import isRehydratable from './utils/isRehydratable';
2023-02-06 17:05:13 -05:00
import { adaptersSR } from '@cornerstonejs/adapters';
2020-06-05 17:36:11 +02:00
type InstanceMetadata = Types.InstanceMetadata;
2023-02-06 17:05:13 -05:00
const { CodeScheme: Cornerstone3DCodeScheme } = adaptersSR.Cornerstone3D;
2020-06-03 19:12:13 +02:00
const { ImageSet, MetadataProvider: metadataProvider } = classes;
// TODO ->
// Add SR thumbnail
// Make viewport
// Get stacks from referenced displayInstanceUID and load into wrapped CornerStone viewport.
2020-06-03 19:12:13 +02:00
const sopClassUids = [
'1.2.840.10008.5.1.4.1.1.88.11', //BASIC_TEXT_SR:
'1.2.840.10008.5.1.4.1.1.88.22', //ENHANCED_SR:
'1.2.840.10008.5.1.4.1.1.88.33', //COMPREHENSIVE_SR:
'1.2.840.10008.5.1.4.1.1.88.34', //COMPREHENSIVE_3D_SR:
2020-06-03 19:12:13 +02:00
];
const CORNERSTONE_3D_TOOLS_SOURCE_NAME = 'Cornerstone3DTools';
const CORNERSTONE_3D_TOOLS_SOURCE_VERSION = '0.1';
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}`);
}
});
};
2020-06-03 19:12:13 +02:00
const CodeNameCodeSequenceValues = {
ImagingMeasurementReport: '126000',
ImageLibrary: '111028',
ImagingMeasurements: '126010',
MeasurementGroup: '125007',
ImageLibraryGroup: '126200',
2020-06-04 14:26:06 +02:00
TrackingUniqueIdentifier: '112040',
2020-06-29 19:22:19 +02:00
TrackingIdentifier: '112039',
Finding: '121071',
FindingSite: 'G-C0E3', // SRT
CornerstoneFreeText: Cornerstone3DCodeScheme.codeValues.CORNERSTONEFREETEXT, //
};
const CodingSchemeDesignators = {
SRT: 'SRT',
CornerstoneCodeSchemes: [Cornerstone3DCodeScheme.CodingSchemeDesignator, 'CST4'],
2020-06-03 19:12:13 +02:00
};
2020-06-05 17:36:11 +02:00
const RELATIONSHIP_TYPE = {
INFERRED_FROM: 'INFERRED FROM',
CONTAINS: 'CONTAINS',
2020-06-05 17:36:11 +02:00
};
const CORNERSTONE_FREETEXT_CODE_VALUE = 'CORNERSTONEFREETEXT';
/**
* 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;
}
2020-06-03 19:12:13 +02:00
/**
* 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
2020-06-03 19:12:13 +02:00
*/
function _getDisplaySetsFromSeries(instances, servicesManager, extensionManager) {
2020-06-03 19:12:13 +02:00
// 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];
2020-06-03 19:12:13 +02:00
const {
StudyInstanceUID,
SeriesInstanceUID,
SOPInstanceUID,
SeriesDescription,
SeriesNumber,
SeriesDate,
ConceptNameCodeSequence,
SOPClassUID,
} = instance;
validateSameStudyUID(instance.StudyInstanceUID, instances);
2020-06-03 19:12:13 +02:00
if (
!ConceptNameCodeSequence ||
ConceptNameCodeSequence.CodeValue !== CodeNameCodeSequenceValues.ImagingMeasurementReport
2020-06-03 19:12:13 +02:00
) {
servicesManager.services.uiNotificationService.show({
title: 'DICOM SR',
message:
'OHIF only supports TID1500 Imaging Measurement Report Structured Reports. The SR you’re trying to view is not supported.',
type: 'warning',
duration: 6000,
});
2020-06-03 19:12:13 +02:00
return [];
}
const displaySet = {
2020-07-03 19:18:59 +02:00
//plugin: id,
2020-06-03 19:12:13 +02:00
Modality: 'SR',
displaySetInstanceUID: utils.guid(),
SeriesDescription,
SeriesNumber,
SeriesDate,
2020-06-03 19:12:13 +02:00
SOPInstanceUID,
SeriesInstanceUID,
StudyInstanceUID,
2020-07-03 19:18:59 +02:00
SOPClassHandlerId,
SOPClassUID,
instances,
referencedImages: null,
measurements: null,
isDerivedDisplaySet: true,
isLoaded: false,
2020-06-10 10:09:33 +02:00
sopClassUids,
instance,
addInstances,
2020-06-03 19:12:13 +02:00
};
displaySet.load = () => _load(displaySet, servicesManager, extensionManager);
return [displaySet];
}
function _load(displaySet, servicesManager, extensionManager) {
const { displaySetService, measurementService } = servicesManager.services;
const dataSources = extensionManager.getDataSources();
const dataSource = dataSources[0];
const { ContentSequence } = displaySet.instance;
displaySet.referencedImages = _getReferencedImagesList(ContentSequence);
displaySet.measurements = _getMeasurements(ContentSequence);
const mappings = measurementService.getSourceMappings(
CORNERSTONE_3D_TOOLS_SOURCE_NAME,
CORNERSTONE_3D_TOOLS_SOURCE_VERSION
2020-07-01 11:08:56 +02:00
);
displaySet.isHydrated = false;
displaySet.isRehydratable = isRehydratable(displaySet, mappings);
2020-07-03 19:18:59 +02:00
displaySet.isLoaded = true;
2020-06-29 19:22:19 +02:00
2020-06-05 17:36:11 +02:00
// Check currently added displaySets and add measurements if the sources exist.
displaySetService.activeDisplaySets.forEach(activeDisplaySet => {
_checkIfCanAddMeasurementsToDisplaySet(displaySet, activeDisplaySet, dataSource);
2020-06-05 17:36:11 +02:00
});
// 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(displaySet, newDisplaySet, dataSource);
});
});
2020-06-03 19:12:13 +02:00
}
function _checkIfCanAddMeasurementsToDisplaySet(srDisplaySet, newDisplaySet, dataSource) {
2020-06-05 17:36:11 +02:00
let unloadedMeasurements = srDisplaySet.measurements.filter(
measurement => measurement.loaded === false
);
if (unloadedMeasurements.length === 0) {
// All already loaded!
return;
}
if (!newDisplaySet instanceof ImageSet) {
// This also filters out _this_ displaySet, as it is not an ImageSet.
return;
}
const { sopClassUids, images } = newDisplaySet;
// Check if any have the newDisplaySet is the correct SOPClass.
unloadedMeasurements = unloadedMeasurements.filter(measurement =>
measurement.coords.some(coord =>
sopClassUids.includes(coord.ReferencedSOPSequence.ReferencedSOPClassUID)
)
);
if (unloadedMeasurements.length === 0) {
// New displaySet isn't the correct SOPClass, so can't contain the referenced images.
return;
}
const SOPInstanceUIDs = [];
unloadedMeasurements.forEach(measurement => {
const { coords } = measurement;
coords.forEach(coord => {
const SOPInstanceUID = coord.ReferencedSOPSequence.ReferencedSOPInstanceUID;
2020-06-05 17:36:11 +02:00
if (!SOPInstanceUIDs.includes(SOPInstanceUID)) {
SOPInstanceUIDs.push(SOPInstanceUID);
}
});
});
const imageIdsForDisplaySet = dataSource.getImageIdsForDisplaySet(newDisplaySet);
2020-06-05 17:36:11 +02:00
for (const imageId of imageIdsForDisplaySet) {
2020-06-05 17:36:11 +02:00
if (!unloadedMeasurements.length) {
// All measurements loaded.
return;
2020-06-05 17:36:11 +02:00
}
const { SOPInstanceUID, frameNumber } = metadataProvider.getUIDsFromImageID(imageId);
2020-06-05 17:36:11 +02:00
if (SOPInstanceUIDs.includes(SOPInstanceUID)) {
2020-06-05 17:36:11 +02:00
for (let j = unloadedMeasurements.length - 1; j >= 0; j--) {
const measurement = unloadedMeasurements[j];
if (_measurementReferencesSOPInstanceUID(measurement, SOPInstanceUID, frameNumber)) {
addMeasurement(measurement, imageId, newDisplaySet.displaySetInstanceUID);
2020-06-05 17:36:11 +02:00
unloadedMeasurements.splice(j, 1);
}
}
}
}
}
function _measurementReferencesSOPInstanceUID(measurement, SOPInstanceUID, frameNumber) {
2020-06-05 17:36:11 +02:00
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[0]?.ReferencedFrameNumber) ||
1;
if (frameNumber && Number(frameNumber) !== Number(ReferencedFrameNumber)) {
return false;
}
2020-06-05 17:36:11 +02:00
for (let j = 0; j < coords.length; j++) {
const coord = coords[j];
const { ReferencedSOPInstanceUID } = coord.ReferencedSOPSequence;
if (ReferencedSOPInstanceUID === SOPInstanceUID) {
return true;
}
}
}
function getSopClassHandlerModule({ servicesManager, extensionManager }) {
const getDisplaySetsFromSeries = instances => {
return _getDisplaySetsFromSeries(instances, servicesManager, extensionManager);
2020-06-05 17:36:11 +02:00
};
2020-06-03 19:12:13 +02:00
return [
{
2020-07-03 19:18:59 +02:00
name: SOPClassHandlerName,
2020-06-03 19:12:13 +02:00
sopClassUids,
getDisplaySetsFromSeries,
},
];
}
2020-06-05 17:36:11 +02:00
function _getMeasurements(ImagingMeasurementReportContentSequence) {
2020-06-03 19:12:13 +02:00
const ImagingMeasurements = ImagingMeasurementReportContentSequence.find(
item =>
item.ConceptNameCodeSequence.CodeValue === CodeNameCodeSequenceValues.ImagingMeasurements
2020-06-03 19:12:13 +02:00
);
const MeasurementGroups = _getSequenceAsArray(ImagingMeasurements.ContentSequence).filter(
item => item.ConceptNameCodeSequence.CodeValue === CodeNameCodeSequenceValues.MeasurementGroup
2020-06-03 19:12:13 +02:00
);
const mergedContentSequencesByTrackingUniqueIdentifiers =
_getMergedContentSequencesByTrackingUniqueIdentifiers(MeasurementGroups);
2020-06-04 14:26:06 +02:00
const measurements = [];
2020-06-04 14:26:06 +02:00
Object.keys(mergedContentSequencesByTrackingUniqueIdentifiers).forEach(
trackingUniqueIdentifier => {
const mergedContentSequence =
mergedContentSequencesByTrackingUniqueIdentifiers[trackingUniqueIdentifier];
2020-06-04 14:26:06 +02:00
2020-06-05 17:36:11 +02:00
const measurement = _processMeasurement(mergedContentSequence);
2020-06-04 14:26:06 +02:00
2020-06-05 17:36:11 +02:00
if (measurement) {
measurements.push(measurement);
2020-06-04 14:26:06 +02:00
}
}
);
2020-06-05 17:36:11 +02:00
return measurements;
2020-06-04 14:26:06 +02:00
}
function _getMergedContentSequencesByTrackingUniqueIdentifiers(MeasurementGroups) {
2020-06-04 14:26:06 +02:00
const mergedContentSequencesByTrackingUniqueIdentifiers = {};
2020-06-03 19:12:13 +02:00
2020-06-04 11:34:43 +02:00
MeasurementGroups.forEach(MeasurementGroup => {
const ContentSequence = _getSequenceAsArray(MeasurementGroup.ContentSequence);
2020-06-04 14:26:06 +02:00
const TrackingUniqueIdentifierItem = ContentSequence.find(
item =>
item.ConceptNameCodeSequence.CodeValue ===
CodeNameCodeSequenceValues.TrackingUniqueIdentifier
);
2020-06-04 11:34:43 +02:00
2020-06-04 14:26:06 +02:00
if (!TrackingUniqueIdentifierItem) {
console.warn('No Tracking Unique Identifier, skipping ambiguous measurement.');
2020-06-04 14:26:06 +02:00
}
const trackingUniqueIdentifier = TrackingUniqueIdentifierItem.UID;
if (mergedContentSequencesByTrackingUniqueIdentifiers[trackingUniqueIdentifier] === undefined) {
2020-06-04 14:26:06 +02:00
// Add the full ContentSequence
mergedContentSequencesByTrackingUniqueIdentifiers[trackingUniqueIdentifier] = [
...ContentSequence,
];
2020-06-04 14:26:06 +02:00
} 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);
2020-06-04 14:26:06 +02:00
}
});
2020-06-04 11:34:43 +02:00
}
});
2020-06-03 19:12:13 +02:00
2020-06-04 14:26:06 +02:00
return mergedContentSequencesByTrackingUniqueIdentifiers;
2020-06-03 19:12:13 +02:00
}
2020-06-04 14:26:06 +02:00
function _processMeasurement(mergedContentSequence) {
if (
mergedContentSequence.some(
group => group.ValueType === 'SCOORD' || group.ValueType === 'SCOORD3D'
)
) {
return _processTID1410Measurement(mergedContentSequence);
}
2020-06-05 17:36:11 +02:00
return _processNonGeometricallyDefinedMeasurement(mergedContentSequence);
}
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');
2020-06-05 17:36:11 +02:00
const UIDREFContentItem = mergedContentSequence.find(group => group.ValueType === 'UIDREF');
2020-06-29 19:22:19 +02:00
const TrackingIdentifierContentItem = mergedContentSequence.find(
item => item.ConceptNameCodeSequence.CodeValue === CodeNameCodeSequenceValues.TrackingIdentifier
2020-06-29 19:22:19 +02:00
);
2020-06-05 17:36:11 +02:00
if (!graphicItem) {
console.warn(
`graphic ValueType ${graphicItem.ValueType} not currently supported, skipping annotation.`
);
return;
}
const NUMContentItems = mergedContentSequence.filter(group => group.ValueType === 'NUM');
2020-06-05 17:36:11 +02:00
const measurement = {
loaded: false,
labels: [],
coords: [_getCoordsFromSCOORDOrSCOORD3D(graphicItem)],
TrackingUniqueIdentifier: UIDREFContentItem.UID,
2020-06-29 19:22:19 +02:00
TrackingIdentifier: TrackingIdentifierContentItem.TextValue,
2020-06-05 17:36:11 +02:00
};
NUMContentItems.forEach(item => {
const { ConceptNameCodeSequence, MeasuredValueSequence } = item;
if (MeasuredValueSequence) {
measurement.labels.push(
_getLabelFromMeasuredValueSequence(ConceptNameCodeSequence, MeasuredValueSequence)
2020-06-05 17:36:11 +02:00
);
}
});
return measurement;
}
function _processNonGeometricallyDefinedMeasurement(mergedContentSequence) {
const NUMContentItems = mergedContentSequence.filter(group => group.ValueType === 'NUM');
2020-06-04 14:26:06 +02:00
const UIDREFContentItem = mergedContentSequence.find(group => group.ValueType === 'UIDREF');
2020-06-29 19:22:19 +02:00
const TrackingIdentifierContentItem = mergedContentSequence.find(
item => item.ConceptNameCodeSequence.CodeValue === CodeNameCodeSequenceValues.TrackingIdentifier
2020-06-29 19:22:19 +02:00
);
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
);
2020-06-05 17:36:11 +02:00
const measurement = {
loaded: false,
labels: [],
coords: [],
TrackingUniqueIdentifier: UIDREFContentItem.UID,
2020-06-29 19:22:19 +02:00
TrackingIdentifier: TrackingIdentifierContentItem.TextValue,
2020-06-05 17:36:11 +02:00
};
2020-06-05 10:19:43 +02:00
if (
finding &&
CodingSchemeDesignators.CornerstoneCodeSchemes.includes(
finding.ConceptCodeSequence.CodingSchemeDesignator
) &&
finding.ConceptCodeSequence.CodeValue === CodeNameCodeSequenceValues.CornerstoneFreeText
) {
measurement.labels.push({
label: CORNERSTONE_FREETEXT_CODE_VALUE,
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 === CodeNameCodeSequenceValues.CornerstoneFreeText
);
if (cornerstoneFreeTextFindingSite) {
measurement.labels.push({
label: CORNERSTONE_FREETEXT_CODE_VALUE,
value: cornerstoneFreeTextFindingSite.ConceptCodeSequence.CodeMeaning,
});
}
}
2020-06-04 14:26:06 +02:00
NUMContentItems.forEach(item => {
const { ConceptNameCodeSequence, ContentSequence, MeasuredValueSequence } = item;
2020-06-05 17:36:11 +02:00
const { ValueType } = ContentSequence;
if (!ValueType === 'SCOORD') {
console.warn(`Graphic ${ValueType} not currently supported, skipping annotation.`);
2020-06-05 17:36:11 +02:00
return;
}
const coords = _getCoordsFromSCOORDOrSCOORD3D(ContentSequence);
2020-06-04 20:39:32 +02:00
2020-06-05 17:36:11 +02:00
if (coords) {
measurement.coords.push(coords);
}
2020-06-04 20:39:32 +02:00
2020-06-05 17:36:11 +02:00
if (MeasuredValueSequence) {
measurement.labels.push(
_getLabelFromMeasuredValueSequence(ConceptNameCodeSequence, MeasuredValueSequence)
2020-06-05 17:36:11 +02:00
);
}
2020-06-04 14:26:06 +02:00
});
2020-06-05 17:36:11 +02:00
return measurement;
}
2020-06-04 11:34:43 +02:00
2020-06-05 17:36:11 +02:00
function _getCoordsFromSCOORDOrSCOORD3D(item) {
const { ValueType, RelationshipType, GraphicType, GraphicData } = item;
if (
!(
RelationshipType == RELATIONSHIP_TYPE.INFERRED_FROM ||
RelationshipType == RELATIONSHIP_TYPE.CONTAINS
)
) {
2020-06-05 17:36:11 +02:00
console.warn(
`Relationshiptype === ${RelationshipType}. Cannot deal with NON TID-1400 SCOORD group with RelationshipType !== "INFERRED FROM" or "CONTAINS"`
2020-06-05 17:36:11 +02:00
);
return;
}
2020-06-04 11:34:43 +02:00
2020-06-05 17:36:11 +02:00
const coords = { ValueType, GraphicType, GraphicData };
// ContentSequence has length of 1 as RelationshipType === 'INFERRED FROM'
if (ValueType === 'SCOORD') {
const { ReferencedSOPSequence } = item.ContentSequence;
coords.ReferencedSOPSequence = ReferencedSOPSequence;
} else if (ValueType === 'SCOORD3D') {
const { ReferencedFrameOfReferenceSequence } = item.ContentSequence;
coords.ReferencedFrameOfReferenceSequence = ReferencedFrameOfReferenceSequence;
}
return coords;
2020-06-04 14:26:06 +02:00
}
function _getLabelFromMeasuredValueSequence(ConceptNameCodeSequence, MeasuredValueSequence) {
2020-06-05 17:36:11 +02:00
const { CodeMeaning } = ConceptNameCodeSequence;
const { NumericValue, MeasurementUnitsCodeSequence } = MeasuredValueSequence;
const { CodeValue } = MeasurementUnitsCodeSequence;
2020-06-04 14:26:06 +02:00
const formatedNumericValue = NumericValue ? Number(NumericValue).toFixed(2) : '';
2020-07-01 15:32:54 +02:00
return {
label: CodeMeaning,
value: `${formatedNumericValue} ${CodeValue}`,
}; // E.g. Long Axis: 31.0 mm
2020-06-04 11:34:43 +02:00
}
2020-06-03 19:12:13 +02:00
function _getReferencedImagesList(ImagingMeasurementReportContentSequence) {
const ImageLibrary = ImagingMeasurementReportContentSequence.find(
item => item.ConceptNameCodeSequence.CodeValue === CodeNameCodeSequenceValues.ImageLibrary
2020-06-03 19:12:13 +02:00
);
const ImageLibraryGroup = _getSequenceAsArray(ImageLibrary.ContentSequence).find(
item => item.ConceptNameCodeSequence.CodeValue === CodeNameCodeSequenceValues.ImageLibraryGroup
2020-06-03 19:12:13 +02:00
);
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,
});
}
}
2020-06-03 19:12:13 +02:00
});
return referencedImages;
}
function _getSequenceAsArray(sequence) {
if (!sequence) {
return [];
}
2020-06-03 19:12:13 +02:00
return Array.isArray(sequence) ? sequence : [sequence];
}
export default getSopClassHandlerModule;