src_dicom_dicomSRContent.js
import {
NumericMeasurement,
getNumericMeasurement,
getDicomNumericMeasurementItem
} from './dicomNumericMeasurement.js';
import {
isEqualCode,
getCode,
getDicomCodeItem,
getConceptNameCode,
getMeasurementUnitsCode
} from './dicomCode.js';
import {
getImageReference,
getDicomImageReferenceItem
} from './dicomImageReference.js';
import {
getSopInstanceReference,
getDicomSopInstanceReferenceItem
} from './dicomSopInstanceReference.js';
import {
getSpatialCoordinate,
getDicomSpatialCoordinateItem
} from './dicomSpatialCoordinate.js';
import {
getSpatialCoordinate3D,
getDicomSpatialCoordinate3DItem
} from './dicomSpatialCoordinate3D.js';
// doc imports
/* eslint-disable no-unused-vars */
import {
safeGet,
safeGetAll,
DataElement
} from './dataElement.js';
import {DicomCode} from './dicomCode.js';
import {MeasuredValue} from './dicomMeasuredValue.js';
/* eslint-enable no-unused-vars */
/**
* Related DICOM tag keys.
*/
const TagKeys = {
ReferencedSOPSequence: '00081199',
RelationshipType: '0040A010',
ValueType: '0040A040',
ConceptNameCodeSequence: '0040A043',
ConceptCodeSequence: '0040A168',
ContentSequence: '0040A730',
DateTime: '0040A120',
Date: '0040A121',
Time: '0040A122',
UID: '0040A124',
PersonName: '0040A123',
TextValue: '0040A160',
ContinuityOfContent: '0040A050',
ContentTemplateSequence: '0040A504',
MappingResource: '00080105',
TemplateIdentifier: '0040DB00'
};
/**
* DICOM relationship types.
*
* Ref: {@link https://dicom.nema.org/medical/dicom/2022a/output/chtml/part03/sect_C.17.3.2.4.html#sect_C.17.3.2.4}.
*/
export const RelationshipTypes = {
contains: 'CONTAINS',
hasProperties: 'HAS PROPERTIES',
hasObsContext: 'HAS OBS CONTEXT',
hasAcqContext: 'HAS ACQ CONTEXT',
inferredFrom: 'INFERRED FROM',
selectedFrom: 'SELECTED FROM',
hasConceptMod: 'HAS CONCEPT MOD'
};
/**
* DICOM value types.
*
* Ref: {@link https://dicom.nema.org/medical/dicom/2022a/output/chtml/part03/sect_C.17.3.2.html#sect_C.17.3.2.1}.
*/
export const ValueTypes = {
text: 'TEXT',
num: 'NUM',
code: 'CODE',
date: 'DATE',
time: 'TIME',
datetime: 'DATETIME',
uidref: 'UIDREF',
pname: 'PNAME',
composite: 'COMPOSITE',
image: 'IMAGE',
waveform: 'WAVEFORM',
scoord: 'SCOORD',
scoord3d: 'SCOORD3D',
tcoord: 'TCOORD',
container: 'CONTAINER',
table: 'TABLE',
};
/**
* DICOM Continuity Of Contents.
*
* Ref: {@link https://dicom.nema.org/medical/dicom/2022a/output/chtml/part03/sect_C.18.8.html#table_C.18.8-1}.
*/
export const ContinuityOfContents = {
separate: 'SEPARATE',
continuous: 'CONTINUOUS'
};
/**
* DICOM value type to associated tag name.
*/
export const ValueTypeValueTagName = {
TEXT: 'TextValue',
DATE: 'Date',
TIME: 'Time',
DATETIME: 'DateTime',
UIDREF: 'UID',
PNAME: 'PersonName',
CONTAINER: 'ContinuityOfContent',
};
/**
* Get the content template value.
*
* @param {Object<string, DataElement>} dataElements The dicom elements.
* @returns {string|undefined} The template as
* 'MappingResource'-'TemplateIdentifier'.
*/
export function getContentTemplate(dataElements) {
let template;
// should only be one item
const templateItem =
safeGet(dataElements, TagKeys.ContentTemplateSequence);
if (typeof templateItem !== 'undefined') {
const mappingResource = safeGet(templateItem, TagKeys.MappingResource);
const templateId = safeGet(templateItem, TagKeys.TemplateIdentifier);
if (typeof mappingResource !== 'undefined' &&
typeof templateId !== 'undefined') {
template = mappingResource + '-' + templateId;
}
}
return template;
}
/**
* DICOM SR content: item of a SR content sequence.
*
* Ref: {@link https://dicom.nema.org/medical/dicom/2022a/output/chtml/part03/sect_C.17.3.html}.
*/
export class DicomSRContent {
/**
* Value type.
*
* @type {string}
*/
valueType;
/**
* Concept name code.
*
* @type {DicomCode|undefined}
*/
conceptNameCode;
/**
* Relationship Type.
*
* @type {string}
*/
relationshipType;
/**
* Content sequence.
*
* @type {DicomSRContent[]}
*/
contentSequence = [];
/**
* Value.
*
* @type {object}
*/
value;
/**
* @param {string} valueType The content item value type.
*/
constructor(valueType) {
this.valueType = valueType;
}
/**
* Get a string representation of this object.
*
* @param {string} [prefix] An optional prefix for recursive content.
* @returns {string} The object as string.
*/
toString(prefix) {
if (typeof prefix === 'undefined') {
prefix = '';
}
let res = '';
if (typeof this.relationshipType !== 'undefined') {
res += '(' + this.relationshipType + ') ';
}
res += this.valueType + ': ';
if (typeof this.conceptNameCode !== 'undefined') {
res += this.conceptNameCode.toString();
}
res += ' = ' + this.value.toString();
for (const item of this.contentSequence) {
res += '\n' + prefix + '- ' + item.toString(prefix + ' ');
}
return res;
}
/**
* Check if this content has input header values.
*
* @param {string} valueType The value type.
* @param {DicomCode} conceptNameCode The concept name code.
* @param {string} relationshipType The relationship type.
* @returns {boolean} True if equal.
*/
hasHeader(valueType, conceptNameCode, relationshipType) {
return this.valueType === valueType &&
isEqualCode(this.conceptNameCode, conceptNameCode) &&
this.relationshipType === relationshipType;
}
}
/**
* Check if two content item objects are equal.
*
* @param {DicomSRContent} item1 The first content item.
* @param {DicomSRContent} item2 The second content item.
* @returns {boolean} True if both content items are equal.
*/
export function isEqualContentItem(item1, item2) {
return Object.keys(item1).length === Object.keys(item2).length &&
Object.keys(item1).every(key =>
Object.prototype.hasOwnProperty.call(item2, key) &&
item1[key] === item2[key]
);
}
/**
* Get a content item object from a dicom element.
*
* @param {Object<string, DataElement>} dataElements The dicom element.
* @returns {DicomSRContent} A content item object.
*/
export function getSRContent(dataElements) {
// valueType -> ValueType (type1)
let valueType = safeGet(dataElements, TagKeys.ValueType);
if (typeof valueType === 'undefined') {
valueType = '';
}
const content = new DicomSRContent(valueType);
// relationshipType -> RelationType (type1)
content.relationshipType =
safeGet(dataElements, TagKeys.RelationshipType);
const conceptNameCode =
safeGet(dataElements, TagKeys.ConceptNameCodeSequence);
if (typeof conceptNameCode !== 'undefined') {
content.conceptNameCode = getCode(conceptNameCode);
}
// set value acording to valueType
// (date and time are stored as string)
if (valueType === ValueTypes.code) {
content.value = getCode(
safeGet(dataElements, TagKeys.ConceptCodeSequence)
);
} else if (valueType === ValueTypes.num) {
content.value = getNumericMeasurement(dataElements);
} else if (valueType === ValueTypes.image) {
content.value = getImageReference(dataElements);
} else if (valueType === ValueTypes.composite) {
content.value = getSopInstanceReference(
safeGet(dataElements, TagKeys.ReferencedSOPSequence)
);
} else if (valueType === ValueTypes.scoord) {
content.value = getSpatialCoordinate(dataElements);
} else if (valueType === ValueTypes.scoord3d) {
content.value = getSpatialCoordinate3D(dataElements);
} else {
const valueTagName = ValueTypeValueTagName[valueType];
if (typeof valueTagName !== 'undefined') {
content.value =
safeGet(dataElements, TagKeys[valueTagName]);
} else {
console.warn('Unsupported input ValueType: ' + valueType);
}
}
const contentSq = safeGetAll(dataElements, TagKeys.ContentSequence);
if (typeof contentSq !== 'undefined') {
for (const item of contentSq) {
content.contentSequence.push(getSRContent(item));
}
}
return content;
}
/**
* Get a simple dicom element item from a content item object.
*
* @param {DicomSRContent} content The content item object.
* @returns {Object<string, any>} The item as a list of (key, value) pairs.
*/
export function getDicomSRContentItem(content) {
// dicom item (tags are in ~group/element order)
let contentItem = {};
if (typeof content.relationshipType !== 'undefined') {
contentItem.RelationshipType = content.relationshipType;
}
if (typeof content.valueType !== 'undefined') {
contentItem.ValueType = content.valueType;
}
if (typeof content.conceptNameCode !== 'undefined') {
contentItem.ConceptNameCodeSequence = {
value: [getDicomCodeItem(content.conceptNameCode)]
};
}
// set appropriate value tag (data and time are stored as string)
if (content.valueType === 'CODE') {
contentItem.ConceptCodeSequence = {
value: [getDicomCodeItem(content.value)]
};
} else if (content.valueType === ValueTypes.num) {
contentItem = {
...contentItem,
...getDicomNumericMeasurementItem(content.value)
};
} else if (content.valueType === ValueTypes.image) {
contentItem = {
...contentItem,
...getDicomImageReferenceItem(content.value)
};
} else if (content.valueType === ValueTypes.composite) {
contentItem = {
...contentItem,
...getDicomSopInstanceReferenceItem(content.value)
};
} else if (content.valueType === ValueTypes.scoord) {
contentItem = {
...contentItem,
...getDicomSpatialCoordinateItem(content.value)
};
} else if (content.valueType === ValueTypes.scoord3d) {
contentItem = {
...contentItem,
...getDicomSpatialCoordinate3DItem(content.value)
};
} else {
const valueTagName = ValueTypeValueTagName[content.valueType];
if (typeof valueTagName !== 'undefined') {
contentItem[valueTagName] = content.value;
} else {
console.warn('Unsupported output ValueType: ' + content.valueType);
}
}
if (content.contentSequence.length !== 0) {
contentItem.ContentSequence = {
value: []
};
for (const item of content.contentSequence) {
contentItem.ContentSequence.value.push(getDicomSRContentItem(item));
}
}
return contentItem;
}
/**
* Get a DicomSRContent from a value.
*
* @param {string} name The value name.
* @param {object} value The value.
* @param {string} unit The values' unit.
* @param {string} [relation] Optional content relationhip.
* @returns {DicomSRContent|undefined} The SR content.
*/
export function getSRContentFromValue(
name, value, unit, relation) {
const conceptNameCode = getConceptNameCode(name);
if (typeof conceptNameCode === 'undefined') {
return undefined;
}
let relationShip = RelationshipTypes.contains;
if (typeof relation !== 'undefined') {
relationShip = relation;
}
const content = new DicomSRContent(ValueTypes.num);
content.relationshipType = relationShip;
content.conceptNameCode = conceptNameCode;
const measure = new MeasuredValue();
measure.numericValue = value;
measure.measurementUnitsCode = getMeasurementUnitsCode(unit);
const numMeasure = new NumericMeasurement();
numMeasure.measuredValue = measure;
content.value = numMeasure;
return content;
}