2020-07-03 19:18:59 +02:00
|
|
|
import { SOPClassHandlerName, SOPClassHandlerId } from './id';
|
2020-06-05 17:36:11 +02:00
|
|
|
import { utils, classes } from '@ohif/core';
|
2020-07-01 11:08:56 +02:00
|
|
|
import addMeasurement from './utils/addMeasurement';
|
|
|
|
|
import isRehydratable from './utils/isRehydratable';
|
2023-02-06 23:05:13 +01:00
|
|
|
import { adaptersSR } from '@cornerstonejs/adapters';
|
2020-06-05 17:36:11 +02:00
|
|
|
|
2023-02-06 23:05:13 +01:00
|
|
|
const { CodeScheme: Cornerstone3DCodeScheme } = adaptersSR.Cornerstone3D;
|
2020-06-03 19:12:13 +02:00
|
|
|
|
2022-10-13 17:07:56 +02:00
|
|
|
const { ImageSet, MetadataProvider: metadataProvider } = classes;
|
2020-06-05 18:59:07 +02:00
|
|
|
// 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:
|
2022-10-11 05:09:38 +02:00
|
|
|
'1.2.840.10008.5.1.4.1.1.88.34', //COMPREHENSIVE_3D_SR:
|
2020-06-03 19:12:13 +02:00
|
|
|
];
|
|
|
|
|
|
2022-07-27 18:39:04 +02:00
|
|
|
const CORNERSTONE_3D_TOOLS_SOURCE_NAME = 'Cornerstone3DTools';
|
|
|
|
|
const CORNERSTONE_3D_TOOLS_SOURCE_VERSION = '0.1';
|
|
|
|
|
|
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',
|
2020-07-01 12:59:29 +02:00
|
|
|
Finding: '121071',
|
2020-07-10 21:11:22 +02:00
|
|
|
FindingSite: 'G-C0E3', // SRT
|
2022-07-27 18:39:04 +02:00
|
|
|
CornerstoneFreeText: Cornerstone3DCodeScheme.codeValues.CORNERSTONEFREETEXT, //
|
2020-07-10 21:11:22 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const CodingSchemeDesignators = {
|
|
|
|
|
SRT: 'SRT',
|
2022-07-27 18:39:04 +02:00
|
|
|
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',
|
2022-10-11 05:09:38 +02:00
|
|
|
CONTAINS: 'CONTAINS',
|
2020-06-05 17:36:11 +02:00
|
|
|
};
|
|
|
|
|
|
2020-07-01 12:59:29 +02:00
|
|
|
const CORNERSTONE_FREETEXT_CODE_VALUE = 'CORNERSTONEFREETEXT';
|
|
|
|
|
|
2020-06-03 19:12:13 +02:00
|
|
|
/**
|
2023-02-17 22:44:21 +01:00
|
|
|
* DICOM SR SOP Class Handler
|
|
|
|
|
* For all referenced images in the TID 1500/300 sections, add an image to the
|
|
|
|
|
* display (this is TODO - it is not the actual behaviour below unfortunately)
|
2020-06-03 19:12:13 +02:00
|
|
|
*
|
2023-02-17 22:44:21 +01:00
|
|
|
* This will only display and rehydrate the latest DICOM SR in the given series
|
|
|
|
|
* It would be possible to add the ability to view older series rehydrations
|
|
|
|
|
* in the future.
|
|
|
|
|
*
|
|
|
|
|
* @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
|
|
|
*/
|
2020-06-05 17:36:11 +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');
|
|
|
|
|
}
|
|
|
|
|
|
2023-02-17 22:44:21 +01:00
|
|
|
utils.sortStudyInstances(instances);
|
|
|
|
|
const instance = instances[instances.length - 1];
|
2020-06-03 19:12:13 +02:00
|
|
|
|
2020-06-10 15:10:00 +02:00
|
|
|
const {
|
|
|
|
|
StudyInstanceUID,
|
|
|
|
|
SeriesInstanceUID,
|
|
|
|
|
SOPInstanceUID,
|
|
|
|
|
SeriesDescription,
|
|
|
|
|
SeriesNumber,
|
|
|
|
|
SeriesDate,
|
2020-07-02 16:17:04 +02:00
|
|
|
ConceptNameCodeSequence,
|
2022-11-16 20:19:52 +01:00
|
|
|
SOPClassUID,
|
2020-06-10 15:10:00 +02:00
|
|
|
} = instance;
|
2020-06-03 19:12:13 +02:00
|
|
|
|
|
|
|
|
if (
|
2020-07-01 12:59:29 +02:00
|
|
|
!ConceptNameCodeSequence ||
|
2020-06-03 19:12:13 +02:00
|
|
|
ConceptNameCodeSequence.CodeValue !==
|
2023-02-17 22:44:21 +01:00
|
|
|
CodeNameCodeSequenceValues.ImagingMeasurementReport
|
2020-06-03 19:12:13 +02:00
|
|
|
) {
|
2023-02-17 22:44:21 +01:00
|
|
|
console.log(
|
|
|
|
|
'Only support Imaging Measurement Report SRs (TID1500) for this renderer.'
|
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(),
|
2020-06-10 15:10:00 +02:00
|
|
|
SeriesDescription,
|
|
|
|
|
SeriesNumber,
|
|
|
|
|
SeriesDate,
|
2020-06-03 19:12:13 +02:00
|
|
|
SOPInstanceUID,
|
|
|
|
|
SeriesInstanceUID,
|
|
|
|
|
StudyInstanceUID,
|
2020-07-03 19:18:59 +02:00
|
|
|
SOPClassHandlerId,
|
2022-11-16 20:19:52 +01:00
|
|
|
SOPClassUID,
|
2023-02-17 22:44:21 +01:00
|
|
|
instances,
|
|
|
|
|
// Others is a historical value used for instances which is deprecated and will be removed
|
|
|
|
|
others: instances,
|
2020-07-02 16:17:04 +02:00
|
|
|
referencedImages: null,
|
|
|
|
|
measurements: null,
|
|
|
|
|
isDerivedDisplaySet: true,
|
|
|
|
|
isLoaded: false,
|
2020-06-10 10:09:33 +02:00
|
|
|
sopClassUids,
|
2020-07-02 16:17:04 +02:00
|
|
|
instance,
|
2020-06-03 19:12:13 +02:00
|
|
|
};
|
|
|
|
|
|
2020-07-02 16:17:04 +02:00
|
|
|
displaySet.load = () => _load(displaySet, servicesManager, extensionManager);
|
|
|
|
|
|
|
|
|
|
return [displaySet];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function _load(displaySet, servicesManager, extensionManager) {
|
2023-02-10 22:46:09 +01:00
|
|
|
const { displaySetService, measurementService } = servicesManager.services;
|
2020-07-02 16:17:04 +02:00
|
|
|
const dataSources = extensionManager.getDataSources();
|
|
|
|
|
const dataSource = dataSources[0];
|
|
|
|
|
|
|
|
|
|
const { ContentSequence } = displaySet.instance;
|
|
|
|
|
|
|
|
|
|
displaySet.referencedImages = _getReferencedImagesList(ContentSequence);
|
|
|
|
|
displaySet.measurements = _getMeasurements(ContentSequence);
|
|
|
|
|
|
2023-02-10 22:46:09 +01:00
|
|
|
const mappings = measurementService.getSourceMappings(
|
2022-07-27 18:39:04 +02:00
|
|
|
CORNERSTONE_3D_TOOLS_SOURCE_NAME,
|
|
|
|
|
CORNERSTONE_3D_TOOLS_SOURCE_VERSION
|
2020-07-01 11:08:56 +02:00
|
|
|
);
|
|
|
|
|
|
2020-07-02 16:17:04 +02:00
|
|
|
displaySet.isHydrated = false;
|
2020-11-25 13:35:43 +01:00
|
|
|
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.
|
2023-02-10 22:46:09 +01:00
|
|
|
displaySetService.activeDisplaySets.forEach(activeDisplaySet => {
|
2020-06-05 17:36:11 +02:00
|
|
|
_checkIfCanAddMeasurementsToDisplaySet(
|
|
|
|
|
displaySet,
|
|
|
|
|
activeDisplaySet,
|
|
|
|
|
dataSource
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Subscribe to new displaySets as the source may come in after.
|
2023-02-10 22:46:09 +01:00
|
|
|
displaySetService.subscribe(
|
|
|
|
|
displaySetService.EVENTS.DISPLAY_SETS_ADDED,
|
2020-06-30 17:27:35 +02:00
|
|
|
data => {
|
|
|
|
|
const { displaySetsAdded } = data;
|
2020-06-05 17:36:11 +02:00
|
|
|
// 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.
|
2020-06-30 17:27:35 +02:00
|
|
|
displaySetsAdded.forEach(newDisplaySet => {
|
2020-06-05 17:36:11 +02:00
|
|
|
_checkIfCanAddMeasurementsToDisplaySet(
|
|
|
|
|
displaySet,
|
|
|
|
|
newDisplaySet,
|
|
|
|
|
dataSource
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
);
|
2020-06-03 19:12:13 +02:00
|
|
|
}
|
|
|
|
|
|
2020-06-05 17:36:11 +02:00
|
|
|
function _checkIfCanAddMeasurementsToDisplaySet(
|
|
|
|
|
srDisplaySet,
|
|
|
|
|
newDisplaySet,
|
|
|
|
|
dataSource
|
|
|
|
|
) {
|
|
|
|
|
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;
|
|
|
|
|
|
|
|
|
|
if (!SOPInstanceUIDs.includes(SOPInstanceUID)) {
|
|
|
|
|
SOPInstanceUIDs.push(SOPInstanceUID);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const imageIdsForDisplaySet = dataSource.getImageIdsForDisplaySet(
|
|
|
|
|
newDisplaySet
|
|
|
|
|
);
|
|
|
|
|
|
2022-10-13 17:07:56 +02:00
|
|
|
for (const imageId of imageIdsForDisplaySet) {
|
2020-06-05 17:36:11 +02:00
|
|
|
if (!unloadedMeasurements.length) {
|
|
|
|
|
// All measurements loaded.
|
2022-10-13 17:07:56 +02:00
|
|
|
return;
|
2020-06-05 17:36:11 +02:00
|
|
|
}
|
|
|
|
|
|
2022-10-13 17:07:56 +02:00
|
|
|
const { SOPInstanceUID, frameNumber } = metadataProvider.getUIDsFromImageID(
|
|
|
|
|
imageId
|
|
|
|
|
);
|
2020-06-05 17:36:11 +02:00
|
|
|
|
2022-10-13 17:07:56 +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];
|
2022-10-13 17:07:56 +02:00
|
|
|
if (
|
|
|
|
|
_measurementReferencesSOPInstanceUID(
|
|
|
|
|
measurement,
|
|
|
|
|
SOPInstanceUID,
|
|
|
|
|
frameNumber
|
|
|
|
|
)
|
|
|
|
|
) {
|
2020-06-05 17:36:11 +02:00
|
|
|
addMeasurement(
|
|
|
|
|
measurement,
|
|
|
|
|
imageId,
|
|
|
|
|
newDisplaySet.displaySetInstanceUID
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
unloadedMeasurements.splice(j, 1);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2022-10-13 17:07:56 +02:00
|
|
|
function _measurementReferencesSOPInstanceUID(
|
|
|
|
|
measurement,
|
|
|
|
|
SOPInstanceUID,
|
|
|
|
|
frameNumber
|
|
|
|
|
) {
|
2020-06-05 17:36:11 +02:00
|
|
|
const { coords } = measurement;
|
|
|
|
|
|
2022-10-13 17:07:56 +02:00
|
|
|
// 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-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-04 11:34:43 +02:00
|
|
|
const MeasurementGroups = _getSequenceAsArray(
|
2020-06-03 19:12:13 +02:00
|
|
|
ImagingMeasurements.ContentSequence
|
2020-06-04 11:34:43 +02:00
|
|
|
).filter(
|
2020-06-03 19:12:13 +02:00
|
|
|
item =>
|
|
|
|
|
item.ConceptNameCodeSequence.CodeValue ===
|
|
|
|
|
CodeNameCodeSequenceValues.MeasurementGroup
|
|
|
|
|
);
|
|
|
|
|
|
2020-06-04 14:26:06 +02:00
|
|
|
const mergedContentSequencesByTrackingUniqueIdentifiers = _getMergedContentSequencesByTrackingUniqueIdentifiers(
|
|
|
|
|
MeasurementGroups
|
|
|
|
|
);
|
|
|
|
|
|
2023-02-17 22:44:21 +01:00
|
|
|
const measurements = [];
|
2020-06-04 14:26:06 +02:00
|
|
|
|
|
|
|
|
Object.keys(mergedContentSequencesByTrackingUniqueIdentifiers).forEach(
|
|
|
|
|
trackingUniqueIdentifier => {
|
|
|
|
|
const mergedContentSequence =
|
|
|
|
|
mergedContentSequencesByTrackingUniqueIdentifiers[
|
2023-02-17 22:44:21 +01:00
|
|
|
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
|
|
|
|
|
) {
|
|
|
|
|
const mergedContentSequencesByTrackingUniqueIdentifiers = {};
|
2020-06-03 19:12:13 +02:00
|
|
|
|
2020-06-04 11:34:43 +02:00
|
|
|
MeasurementGroups.forEach(MeasurementGroup => {
|
2020-06-04 14:26:06 +02:00
|
|
|
const ContentSequence = _getSequenceAsArray(
|
|
|
|
|
MeasurementGroup.ContentSequence
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
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.'
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const trackingUniqueIdentifier = TrackingUniqueIdentifierItem.UID;
|
|
|
|
|
|
|
|
|
|
if (
|
|
|
|
|
mergedContentSequencesByTrackingUniqueIdentifiers[
|
2023-02-17 22:44:21 +01:00
|
|
|
trackingUniqueIdentifier
|
2020-06-04 14:26:06 +02:00
|
|
|
] === 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);
|
|
|
|
|
}
|
|
|
|
|
});
|
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 18:59:07 +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-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'
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const measurement = {
|
|
|
|
|
loaded: false,
|
|
|
|
|
labels: [],
|
|
|
|
|
coords: [_getCoordsFromSCOORDOrSCOORD3D(graphicItem)],
|
2020-06-05 18:59:07 +02:00
|
|
|
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
|
|
|
|
|
)
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return measurement;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function _processNonGeometricallyDefinedMeasurement(mergedContentSequence) {
|
2020-06-04 14:26:06 +02:00
|
|
|
const NUMContentItems = mergedContentSequence.filter(
|
|
|
|
|
group => group.ValueType === 'NUM'
|
|
|
|
|
);
|
|
|
|
|
|
2020-06-05 18:59:07 +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-07-10 21:11:22 +02:00
|
|
|
const Finding = mergedContentSequence.find(
|
2020-07-01 12:59:29 +02:00
|
|
|
item =>
|
|
|
|
|
item.ConceptNameCodeSequence.CodeValue ===
|
|
|
|
|
CodeNameCodeSequenceValues.Finding
|
|
|
|
|
);
|
|
|
|
|
|
2020-07-10 21:11:22 +02:00
|
|
|
const FindingSites = mergedContentSequence.filter(
|
|
|
|
|
item =>
|
|
|
|
|
item.ConceptNameCodeSequence.CodingSchemeDesignator ===
|
2023-02-17 22:44:21 +01:00
|
|
|
CodingSchemeDesignators.SRT &&
|
2020-07-10 21:11:22 +02:00
|
|
|
item.ConceptNameCodeSequence.CodeValue ===
|
2023-02-17 22:44:21 +01:00
|
|
|
CodeNameCodeSequenceValues.FindingSite
|
2020-07-10 21:11:22 +02:00
|
|
|
);
|
|
|
|
|
|
2020-06-05 17:36:11 +02:00
|
|
|
const measurement = {
|
|
|
|
|
loaded: false,
|
|
|
|
|
labels: [],
|
|
|
|
|
coords: [],
|
2020-06-05 18:59:07 +02:00
|
|
|
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
|
|
|
|
2020-07-10 21:11:22 +02:00
|
|
|
if (
|
|
|
|
|
Finding &&
|
2022-07-27 18:39:04 +02:00
|
|
|
CodingSchemeDesignators.CornerstoneCodeSchemes.includes(
|
|
|
|
|
Finding.ConceptCodeSequence.CodingSchemeDesignator
|
|
|
|
|
) &&
|
2020-07-10 21:11:22 +02:00
|
|
|
Finding.ConceptCodeSequence.CodeValue ===
|
2023-02-17 22:44:21 +01:00
|
|
|
CodeNameCodeSequenceValues.CornerstoneFreeText
|
2020-07-10 21:11:22 +02:00
|
|
|
) {
|
|
|
|
|
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 =>
|
2022-07-27 18:39:04 +02:00
|
|
|
CodingSchemeDesignators.CornerstoneCodeSchemes.includes(
|
|
|
|
|
FindingSite.ConceptCodeSequence.CodingSchemeDesignator
|
|
|
|
|
) &&
|
2020-07-10 21:11:22 +02:00
|
|
|
FindingSite.ConceptCodeSequence.CodeValue ===
|
2023-02-17 22:44:21 +01:00
|
|
|
CodeNameCodeSequenceValues.CornerstoneFreeText
|
2020-07-01 12:59:29 +02:00
|
|
|
);
|
2020-07-10 21:11:22 +02:00
|
|
|
|
|
|
|
|
if (cornerstoneFreeTextFindingSite) {
|
2020-07-01 12:59:29 +02:00
|
|
|
measurement.labels.push({
|
|
|
|
|
label: CORNERSTONE_FREETEXT_CODE_VALUE,
|
2020-07-10 21:11:22 +02:00
|
|
|
value: cornerstoneFreeTextFindingSite.ConceptCodeSequence.CodeMeaning,
|
2020-07-01 12:59:29 +02:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-06-04 14:26:06 +02:00
|
|
|
NUMContentItems.forEach(item => {
|
2020-06-05 17:36:11 +02:00
|
|
|
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);
|
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-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;
|
|
|
|
|
|
2022-10-11 05:09:38 +02:00
|
|
|
if (
|
|
|
|
|
!(
|
|
|
|
|
RelationshipType == RELATIONSHIP_TYPE.INFERRED_FROM ||
|
|
|
|
|
RelationshipType == RELATIONSHIP_TYPE.CONTAINS
|
|
|
|
|
)
|
|
|
|
|
) {
|
2020-06-05 17:36:11 +02:00
|
|
|
console.warn(
|
2022-10-11 05:09:38 +02:00
|
|
|
`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
|
|
|
}
|
|
|
|
|
|
2020-06-05 17:36:11 +02:00
|
|
|
function _getLabelFromMeasuredValueSequence(
|
|
|
|
|
ConceptNameCodeSequence,
|
|
|
|
|
MeasuredValueSequence
|
|
|
|
|
) {
|
|
|
|
|
const { CodeMeaning } = ConceptNameCodeSequence;
|
|
|
|
|
const { NumericValue, MeasurementUnitsCodeSequence } = MeasuredValueSequence;
|
|
|
|
|
const { CodeValue } = MeasurementUnitsCodeSequence;
|
2020-06-04 14:26:06 +02:00
|
|
|
|
2020-07-01 16:12:49 +02:00
|
|
|
const formatedNumericValue = NumericValue
|
2023-02-06 03:03:19 +01:00
|
|
|
? Number(NumericValue).toFixed(2)
|
2020-07-01 16:12:49 +02:00
|
|
|
: '';
|
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
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const ImageLibraryGroup = _getSequenceAsArray(
|
|
|
|
|
ImageLibrary.ContentSequence
|
|
|
|
|
).find(
|
|
|
|
|
item =>
|
|
|
|
|
item.ConceptNameCodeSequence.CodeValue ===
|
|
|
|
|
CodeNameCodeSequenceValues.ImageLibraryGroup
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const referencedImages = [];
|
|
|
|
|
|
|
|
|
|
_getSequenceAsArray(ImageLibraryGroup.ContentSequence).forEach(item => {
|
|
|
|
|
const { ReferencedSOPSequence } = item;
|
|
|
|
|
|
2022-10-11 05:09:38 +02:00
|
|
|
if (item.hasOwnProperty('ReferencedSOPClassUID')) {
|
|
|
|
|
const {
|
|
|
|
|
ReferencedSOPClassUID,
|
|
|
|
|
ReferencedSOPInstanceUID,
|
|
|
|
|
} = ReferencedSOPSequence;
|
|
|
|
|
|
|
|
|
|
referencedImages.push({
|
|
|
|
|
ReferencedSOPClassUID,
|
|
|
|
|
ReferencedSOPInstanceUID,
|
|
|
|
|
});
|
|
|
|
|
}
|
2020-06-03 19:12:13 +02:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return referencedImages;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function _getSequenceAsArray(sequence) {
|
|
|
|
|
return Array.isArray(sequence) ? sequence : [sequence];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export default getSopClassHandlerModule;
|