import { _fetchBulkdata } from './utils.js'
const _attrs = Symbol('attrs')
/**
* Annotation Group.
*
* Describes an item of the Annotation Group Sequence of a DICOM Microscopy Bulk
* Simple Annotations instance.
*
* @class
* @memberof annotation
*/
class AnnotationGroup {
/**
* @param {Object} options - Options
* @param {string} options.uid - Annotation Group UID
* @param {number} options.number - Annotation Group Number (one-based index value)
* @param {string} options.label - Annotation Group Label
* @param {string} options.algorithmName - Annotation Group Algorithm Name
* @param {Object} options.algorithmType - Annotation Group Algorithm Type
* @param {Object} options.propertyCategory - Annotation Property Category Code
* @param {Object} options.propertyType - Annotation Property Type Code
* @param {string} options.studyInstanceUID - Study Instance UID of Annotation instances
* @param {string} options.seriesInstanceUID - Series Instance UID of Annotation instances
* @param {string[]} options.sopInstanceUIDs - SOP Instance UIDs of Annotation instances
*/
constructor ({
uid,
number,
label,
propertyCategory,
propertyType,
algorithmType,
algorithmName,
studyInstanceUID,
seriesInstanceUID,
sopInstanceUIDs
}) {
this[_attrs] = {}
if (uid == null) {
throw new Error('Annotation Group UID is required.')
} else {
this[_attrs].uid = uid
}
if (number == null) {
throw new Error('Annotation Group Number is required.')
}
this[_attrs].number = number
if (label == null) {
throw new Error('Annotation Group Label is required.')
}
this[_attrs].label = label
if (propertyCategory == null) {
throw new Error('Annotation Property Category is required.')
}
this[_attrs].propertyCategory = propertyCategory
if (propertyType == null) {
throw new Error('Annotation Property Type is required.')
}
this[_attrs].propertyType = propertyType
if (algorithmName == null) {
throw new Error('Annotation Group Algorithm Name is required.')
}
this[_attrs].algorithmType = algorithmType
if (algorithmType == null) {
throw new Error('Annotation Group Generation Type is required.')
}
this[_attrs].algorithmName = algorithmName
if (studyInstanceUID == null) {
throw new Error('Study Instance UID is required.')
}
this[_attrs].studyInstanceUID = studyInstanceUID
if (seriesInstanceUID == null) {
throw new Error('Series Instance UID is required.')
}
this[_attrs].seriesInstanceUID = seriesInstanceUID
if (sopInstanceUIDs == null) {
throw new Error('SOP Instance UIDs are required.')
}
this[_attrs].sopInstanceUIDs = sopInstanceUIDs
Object.freeze(this)
}
/**
* Annotation Group UID
*
* @type string
*/
get uid () {
return this[_attrs].uid
}
/**
* Annotation Group Number.
*
* @type number
*/
get number () {
return this[_attrs].number
}
/**
* Annotation Group Label
*
* @type string
*/
get label () {
return this[_attrs].label
}
/**
* Annotation Group Algorithm Identification
*
* @type string
*/
get algorithmName () {
return this[_attrs].algorithmName
}
/**
* Annotation Group Generation Type
*
* @type Object
*/
get algorithmType () {
return this[_attrs].algorithmType
}
/**
* Annotation Property Category
*
* @type Object
*/
get propertyCategory () {
return this[_attrs].propertyCategory
}
/**
* Annotation Property Type
*
* @type Object
*/
get propertyType () {
return this[_attrs].propertyType
}
/**
* Study Instance UID of DICOM Microscopy Bulk Simple Annotations objects.
*
* @type string
*/
get studyInstanceUID () {
return this[_attrs].studyInstanceUID
}
/**
* Series Instance UID of DICOM Microscopy Bulk Simple Annotations objects.
*
* @type string
*/
get seriesInstanceUID () {
return this[_attrs].seriesInstanceUID
}
/**
* SOP Instance UIDs of DICOM Microscopy Bulk Simple Annotations objects.
*
* @type string[]
*/
get sopInstanceUIDs () {
return this[_attrs].sopInstanceUIDs
}
}
/**
* Fetch graphic data (point coordinates) of an annotation group.
*
* @param {Object} options
* @param {Object} options.metadataItem - Metadata of Annotation Group Sequence item
* @param {Object} options.bulkdataItem - Bulkdata of Annotation Group Sequence item
* @param {Object} options.client - DICOMweb client
*
* @returns {Promise<TypedArray>} Graphic data
*
* @private
*/
async function _fetchGraphicData ({
metadataItem,
bulkdataItem,
client
}) {
const uid = metadataItem.AnnotationGroupUID
if ('PointCoordinatesData' in metadataItem) {
return metadataItem.PointCoordinatesData
} else if ('DoublePointCoordinatesData' in metadataItem) {
return metadataItem.DoublePointCoordinatesData
} else {
if (bulkdataItem == null) {
throw new Error(
`Could not find bulkdata of annotation group "${uid}".`
)
} else {
if ('PointCoordinatesData' in bulkdataItem) {
console.info(`fetch point coordinate data of annotation group "${uid}"`)
return await _fetchBulkdata({
client,
reference: bulkdataItem.PointCoordinatesData
})
} else if ('DoublePointCoordinatesData' in bulkdataItem) {
console.info(`fetch point coordinate data of annotation group "${uid}"`)
return await _fetchBulkdata({
client,
reference: bulkdataItem.DoublePointCoordinatesData
})
} else {
throw new Error(
'Could not find "PointCoordinatesData" or ' +
'"DoublePointCoordinatesData" in bulkdata ' +
`of annotation group "${uid}".`
)
}
}
}
}
/**
* Fetch graphic index (long primitive point index) of an annotation group.
*
* @param {object} options
* @param {object} options.metadataItem - Metadata of Annotation Group Sequence item
* @param {object} options.bulkdataItem - Bulkdata of Annotation Group Sequence item
* @param {object} options.client - DICOMweb client
*
* @returns {Promise<TypedArray|null>} Graphic index
*
* @private
*/
async function _fetchGraphicIndex ({
metadataItem,
bulkdataItem,
client
}) {
const uid = metadataItem.AnnotationGroupUID
const graphicType = metadataItem.GraphicType
if ('LongPrimitivePointIndexList' in metadataItem) {
return metadataItem.LongPrimitivePointIndexList
} else {
if (bulkdataItem == null) {
if (graphicType === 'POLYGON') {
throw new Error(
`Could not find bulkdata of annotation group "${uid}".`
)
} else {
return null
}
} else {
if ('LongPrimitivePointIndexList' in bulkdataItem) {
console.info(`fetch point index list of annotation group "${uid}"`)
return await _fetchBulkdata({
client,
reference: bulkdataItem.LongPrimitivePointIndexList
})
} else {
if (graphicType === 'POLYGON') {
throw new Error(
'Could not find "LongPrimitivePointIndexList" ' +
`in bulkdata of annotation group "${uid}".`
)
} else {
return null
}
}
}
}
}
/**
* Fetch measurement values of an annotation group.
*
* @param {object} options
* @param {object} options.metadataItem - Metadata of Annotation Group Sequence item
* @param {object} options.bulkdataItem - Bulkdata of Annotation Group Sequence item
* @param {number} options.index - Zero-based index in the Measurements Sequence
* @param {object} options.client - DICOMweb client
*
* @returns {Promise<TypeArray>} Values
*
* @private
*/
async function _fetchMeasurementValues ({
metadataItem,
bulkdataItem,
index,
client
}) {
const uid = metadataItem.AnnotationGroupUID
const measurementMetadataItem = metadataItem.MeasurementsSequence[index]
const valuesMetadataItem = measurementMetadataItem.MeasurementValuesSequence[0]
if ('FloatingPointValues' in valuesMetadataItem) {
return valuesMetadataItem.FloatingPointValues
} else {
if (bulkdataItem == null) {
throw new Error(
`Could not find bulkdata of annotation group "${uid}".`
)
} else if (bulkdataItem.MeasurementsSequence == null) {
throw new Error(
`Could not find item #${index + 1} of "MeasurementSequence" ` +
`in bulkdata of annotation group "${uid}".`
)
} else {
const measurementBulkdataItem = bulkdataItem.MeasurementsSequence[index]
const valuesBulkdataItem = (
measurementBulkdataItem.MeasurementValuesSequence[0]
)
if ('FloatingPointValues' in valuesBulkdataItem) {
const nameItem = measurementMetadataItem.ConceptNameCodeSequence[0]
const name = nameItem.CodeMeaning
console.info(
`fetch measurement values for measurement #${index} "${name}"`
)
return await _fetchBulkdata({
client,
reference: valuesBulkdataItem.FloatingPointValues
})
} else {
throw new Error(
`Could not find "FloatingPointValues" in item #${index + 1} ` +
'of "MeasurementSequence" in bulkdata ' +
`of annotation group "${uid}".`
)
}
}
}
}
/**
* Fetch measurement annotation indices of an annotation group.
*
* @param {object} options
* @param {object} options.metadataItem - Metadata of Annotation Group Sequence item
* @param {object} options.bulkdataItem - Bulkdata of Annotation Group Sequence item
* @param {number} options.index - Zero-based index in the Measurements Sequence
* @param {object} options.client - DICOMweb client
*
* @returns {Promise<TypeArray|null>} Annotation indices
*
* @private
*/
async function _fetchMeasurementIndices ({
metadataItem,
bulkdataItem,
index,
client
}) {
const uid = metadataItem.AnnotationGroupUID
const measurementMetadataItem = metadataItem.MeasurementsSequence[index]
const valuesMetadataItem = measurementMetadataItem.MeasurementValuesSequence[0]
if ('AnnotationIndexList' in valuesMetadataItem) {
return valuesMetadataItem.AnnotationIndexList
} else {
if (bulkdataItem == null) {
throw new Error(
`Could not find bulkdata of annotation group "${uid}".`
)
} else if (bulkdataItem.MeasurementsSequence == null) {
throw new Error(
`Could not find item #${index + 1} of "MeasurementSequence" ` +
`in bulkdata of annotation group "${uid}".`
)
} else {
const measurementBulkdataItem = bulkdataItem.MeasurementsSequence[index]
const valuesBulkdataItem = (
measurementBulkdataItem
.MeasurementValuesSequence[0]
)
if ('AnnotationIndexList' in valuesBulkdataItem) {
const nameItem = measurementMetadataItem.ConceptNameCodeSequence[0]
const name = nameItem.CodeMeaning
console.info(
`fetch measurement indices for measurement #${index} "${name}"`
)
return await _fetchBulkdata({
client,
reference: valuesBulkdataItem.AnnotationIndexList
})
} else {
return null
}
}
}
}
/**
* Fetch all measurements of an annotation group.
*
* @param {object} options
* @param {object} options.metadataItem - Metadata of Annotation Group Sequence item
* @param {object} options.bulkdataItem - Bulkdata of Annotation Group Sequence item
* @param {object} options.client - DICOMweb client
*
* @returns {Promise<Array<object>>} Name, values, and indices of measurements
*
* @private
*/
async function _fetchMeasurements ({
metadataItem,
bulkdataItem,
client
}) {
const measurements = []
if (metadataItem.MeasurementsSequence !== undefined) {
for (let i = 0; i < metadataItem.MeasurementsSequence.length; i++) {
const item = metadataItem.MeasurementsSequence[i]
const name = item.ConceptNameCodeSequence[0]
const unit = item.MeasurementUnitsCodeSequence[0]
const values = await _fetchMeasurementValues({
metadataItem,
bulkdataItem,
index: i,
client
})
const indices = await _fetchMeasurementIndices({
metadataItem,
bulkdataItem,
index: i,
client
})
measurements.push({
name,
unit,
values,
indices
})
}
}
return measurements
}
/**
* Fetch an individual measurement of an annotation group.
*
* @param {object} options
* @param {object} options.metadataItem - Metadata of Annotation Group Sequence item
* @param {object} options.bulkdataItem - Bulkdata of Annotation Group Sequence item
* @param {object} options.index - Index of the Measurements Sequence item
* @param {object} options.client - DICOMweb client
*
* @returns {Promise<Array<object>>} Name, values, and indices of measurements
*
* @private
*/
async function _fetchMeasurement ({
metadataItem,
bulkdataItem,
index,
client
}) {
if (metadataItem.MeasurementsSequence == null) {
throw new Error(
'Measurements Sequence element is not contained in metadata.'
)
}
if (metadataItem.MeasurementsSequence.length === 0) {
throw new Error(
'Measurements Sequence element in empty.'
)
}
const item = metadataItem.MeasurementsSequence[index]
if (item == null) {
throw new Error(
`Measurements Sequence does not contain an item #${index}.`
)
}
const name = item.ConceptNameCodeSequence[0]
const unit = item.MeasurementUnitsCodeSequence[0]
const values = await _fetchMeasurementValues({
metadataItem,
bulkdataItem,
index,
client
})
const indices = await _fetchMeasurementIndices({
metadataItem,
bulkdataItem,
index,
client
})
return { name, unit, values, indices }
}
/**
* Get dimensionality of coordinates.
*
* @param {object} metadataItem - Metadata of Annotation Group Sequence item
*
* @returns {number} Dimensionality (2 or 3)
*
* @private
*/
function _getCoordinateDimensionality (metadataItem) {
if (metadataItem.CommonZCoordinateValue == null) {
if (metadataItem.AnnotationCoordinateType === '2D') {
return 2
}
return 3
}
return 2
}
/**
* Get common Z coordinate value that is shared across all annotations.
*
* @param {object} metadataItem - Metadata of Annotation Group Sequence item
*
* @returns {number} Value (NaN in case there is no common Z coordinate)
*
* @private
*/
function _getCommonZCoordinate (metadataItem) {
if (metadataItem.CommonZCoordinateValue == null) {
return Number.NaN
}
return Number(metadataItem.CommonZCoordinateValue)
}
/**
* Get coordinates of an annotation.
*
* @param {TypedArray} graphicData - Points coordinates of all annotations
* @param {number} offset - Offset for the annotation of interest.
* @param {number} commonZCoordinate - Z coordinate that is shared across annotations.
*
* @returns {Array<number>} (x, y, z) or (column, row, slice) coordinates
*
* @private
*/
function _getCoordinates (graphicData, offset, commonZCoordinate) {
const point = [
graphicData[offset],
graphicData[offset + 1]
]
if (isNaN(commonZCoordinate)) {
point.push(graphicData[offset + 2])
} else {
point.push(commonZCoordinate)
}
return point
}
/**
* Get coordinates of a POINT annotation.
*
* @param {TypedArray} graphicData - Point coordinates of all annotations
* @param {TypedArray} graphicIndex - Annotation index of all annotations
* @param {number} coordinateDimensionality - Dimensionality of stored coordinates.
* @param {number} commonZCoordinate - Z coordinate that is shared across annotations.
* @param {number} annotationIndex - Index of the annotation of interest.
* @param {number} numberOfAnnotations - Total number of annotations.
*
* @returns {Array<number>} (x, y, z) or (column, row, slice) coordinates
*
* @private
*/
function _getPoint (
graphicData,
graphicIndex,
coordinateDimensionality,
commonZCoordinate,
annotationIndex,
numberOfAnnotations
) {
const length = coordinateDimensionality
const offset = annotationIndex * length
return _getCoordinates(graphicData, offset, commonZCoordinate)
}
/**
* Get coordinates of the centroid point of a RECTANGLE annotation.
*
* @param {TypedArray} graphicData - Point coordinates of all annotations
* @param {TypedArray} graphicIndex - Annotation index of all annotations
* @param {number} coordinateDimensionality - Dimensionality of stored coordinates.
* @param {number} commonZCoordinate - Z coordinate that is shared across annotations.
* @param {number} annotationIndex - Index of the annotation of interest.
* @param {number} numberOfAnnotations - Total number of annotations.
*
* @returns {Array<number>} (x, y, z) or (column, row, slice) coordinates
*
* @private
*/
function _getRectangleCentroid (
graphicData,
graphicIndex,
coordinateDimensionality,
commonZCoordinate,
annotationIndex,
numberOfAnnotations
) {
const length = coordinateDimensionality * 4
const offset = annotationIndex * length
const coordinates = []
for (let j = offset; j < offset + length; j++) {
const p = _getCoordinates(graphicData, j, commonZCoordinate)
coordinates.push(p)
j += coordinateDimensionality - 1
}
const topLeft = coordinates[0]
const topRight = coordinates[1]
const bottomLeft = coordinates[3]
return [
topLeft[0] + (topRight[0] - topLeft[0]) / 2,
topLeft[1] + (topLeft[1] - bottomLeft[1]) / 2,
0
]
}
/**
* Get coordinates of the centroid point of an ELLIPSE annotation.
*
* @param {TypedArray} graphicData - Point coordinates of all annotations
* @param {TypedArray} graphicIndex - Annotation index of all annotations
* @param {number} coordinateDimensionality - Dimensionality of stored coordinates.
* @param {number} commonZCoordinate - Z coordinate that is shared across annotations.
* @param {number} annotationIndex - Index of the annotation of interest.
* @param {number} numberOfAnnotations - Total number of annotations.
*
* @returns {Array<number>} (x, y, z) or (column, row, slice) coordinates
*
* @private
*/
function _getEllipseCentroid (
graphicData,
graphicIndex,
coordinateDimensionality,
commonZCoordinate,
annotationIndex,
numberOfAnnotations
) {
const length = coordinateDimensionality * 4
const offset = annotationIndex * length
const coordinates = []
for (let j = offset; j < offset + length; j++) {
const p = _getCoordinates(graphicData, j, commonZCoordinate)
coordinates.push(p)
j += coordinateDimensionality - 1
}
const majorAxisFirstEndpoint = coordinates[0]
const majorAxisSecondEndpoint = coordinates[1]
return [
(majorAxisSecondEndpoint[0] - majorAxisFirstEndpoint[0]) / 2,
(majorAxisSecondEndpoint[1] - majorAxisFirstEndpoint[1]) / 2,
0
]
}
/**
* Get coordinates of the centroid point of a POLYGON annotation.
*
* @param {TypedArray} graphicData - Point coordinates of all annotations
* @param {TypedArray} graphicIndex - Annotation index of all annotations
* @param {number} coordinateDimensionality - Dimensionality of stored coordinates.
* @param {number} commonZCoordinate - Z coordinate that is shared across annotations.
* @param {number} annotationIndex - Index of the annotation of interest.
* @param {number} numberOfAnnotations - Total number of annotations.
*
* @returns {Array<number>} (x, y, z) or (column, row, slice) coordinates
*
* @private
*/
function _getPolygonCentroid (
graphicData,
graphicIndex,
coordinateDimensionality,
commonZCoordinate,
annotationIndex,
numberOfAnnotations
) {
const offset = graphicIndex[annotationIndex] - 1
let length
if (annotationIndex < (numberOfAnnotations - 1)) {
length = graphicIndex[annotationIndex + 1] - offset
} else {
length = graphicData.length
}
// https://en.wikipedia.org/wiki/Centroid#Of_a_polygon
const point = [0, 0, 0]
let area = 0
for (let j = offset; j < offset + length; j++) {
const p0 = _getCoordinates(graphicData, j, commonZCoordinate)
let p1
if (j === (offset + length - 1)) {
p1 = _getCoordinates(graphicData, offset, commonZCoordinate)
} else {
const nextOffset = j + coordinateDimensionality
p1 = _getCoordinates(graphicData, nextOffset, commonZCoordinate)
}
const a = p0[0] * p1[1] - p1[0] * p0[1]
area += a
point[0] += (p0[0] + p1[0]) * a
point[1] += (p0[1] + p1[1]) * a
j += coordinateDimensionality - 1
}
area *= 0.5
point[0] /= 6 * area
point[1] /= 6 * area
return point
}
/**
* Get coordinates of the centroid point of an annotation.
*
* @param {string} graphicType - Type of annotations (POINT, POLYGON, etc.)
* @param {TypedArray} graphicData - Point coordinates of all annotations
* @param {TypedArray} graphicIndex - Annotation index of all annotations
* @param {number} coordinateDimensionality - Dimensionality of stored coordinates.
* @param {number} commonZCoordinate - Z coordinate that is shared across annotations.
* @param {number} annotationIndex - Index of the annotation of interest.
* @param {number} numberOfAnnotations - Total number of annotations.
*
* @returns {Array<number>} (x, y, z) or (column, row, slice) coordinates
*
* @private
*/
const _getCentroid = (
graphicType,
graphicData,
graphicIndex,
coordinateDimensionality,
commonZCoordinate,
annotationIndex,
numberOfAnnotations
) => {
if (graphicType === 'POINT') {
return _getPoint(
graphicData,
graphicIndex,
coordinateDimensionality,
commonZCoordinate,
annotationIndex,
numberOfAnnotations
)
} else if (graphicType === 'RECTANGLE') {
return _getRectangleCentroid(
graphicData,
graphicIndex,
coordinateDimensionality,
commonZCoordinate,
annotationIndex,
numberOfAnnotations
)
} else if (graphicType === 'ELLIPSE') {
return _getEllipseCentroid(
graphicData,
graphicIndex,
coordinateDimensionality,
commonZCoordinate,
annotationIndex,
numberOfAnnotations
)
} else if (graphicType === 'POLYGON') {
return _getPolygonCentroid(
graphicData,
graphicIndex,
coordinateDimensionality,
commonZCoordinate,
annotationIndex,
numberOfAnnotations
)
} else {
throw new Error(
`Encountered unexpected graphic type "${graphicType}".`
)
}
}
export {
AnnotationGroup,
_fetchGraphicData,
_fetchGraphicIndex,
_fetchMeasurements,
_fetchMeasurement,
_getCentroid,
_getCommonZCoordinate,
_getCoordinateDimensionality
}