UNPKG

dicom-microscopy-viewer

Version:
397 lines (376 loc) 12.5 kB
import { _generateUID } from './utils.js' const _coordinates = Symbol('coordinates') const _frameOfReferenceUID = Symbol('frameOfReferenceUID') const _fiducialUID = Symbol('fiducialUID') /** * 3D spatial coordinates. * * @class * @abstract * @memberof scoord3d */ class Scoord3D { /** * @param {Object} options - Options * @param {string} options.frameOfReferenceUID - Frame of Reference UID * @param {number[][]} options.coordinates - (x, y, z) coordinates for * each point * @param {string} options.fiducialUID - Fiducial UID */ constructor(options) { if ( !( typeof options.frameOfReferenceUID === 'string' || options.frameOfReferenceUID instanceof String ) ) { throw new Error( 'Argument "frameOfReferenceUID" of Scoord3D must be a string.', ) } this[_frameOfReferenceUID] = options.frameOfReferenceUID options.fiducialUID = options.fiducialUID || _generateUID() if ( !( typeof options.fiducialUID === 'string' || options.fiducialUID instanceof String ) ) { throw new Error('Argument "fiducialUID" of Scoord3D must be a string.') } this[_fiducialUID] = options.fiducialUID if (!Array.isArray(options.coordinates)) { throw new Error('Argument "coordinates" of Scoord3D must be an array.') } this[_coordinates] = options.coordinates } /** * Graphic Data * * @type {number[][]} */ get graphicData() { return this[_coordinates] } /** * Graphic Type * * @type {string} */ get graphicType() { throw new Error('Prototype property "graphicType" must be implemented') } /** * Frame of Reference UID * * @type {string} */ get frameOfReferenceUID() { return this[_frameOfReferenceUID] } /** * Fiducial UID * * @type {string} */ get fiducialUID() { return this[_fiducialUID] } } /** * POINT graphic denoted by a single (x, y, z) triplet. * * @class * @extends scoord3d.Scoord3D * @memberof scoord3d */ class Point extends Scoord3D { /** * @param {Object} options * @param {string} options.frameOfReferenceUID - Unique identifier of the Frame of Reference * @param {number[]} options.coordinates - X, Y and Z coordinate. * @param {string} [options.fiducialUID] - Unique identifier of an imaging fiducial */ constructor(options) { if (!Array.isArray(options.coordinates)) { throw new Error('Argument "coordinates" of Point must be an array.') } if (options.coordinates.length !== 3) { throw new Error( 'Argument "coordinates" of Point must be an array of length 3.', ) } if (options.coordinates.some((c) => c < 0)) { console.warn('coordinates of Point are negative numbers') } super({ coordinates: options.coordinates, frameOfReferenceUID: options.frameOfReferenceUID, fiducialUID: options.fiducialUID, }) Object.freeze(this) } /** Graphic Data * * @type {number[]} */ get graphicData() { return this[_coordinates] } get graphicType() { return 'POINT' } } /** * MULTIPOINT graphic denoted by multiple, coplanar (x, y, z) coordinates that * represent individual points. * * @class * @extends scoord3d.Scoord3D * @memberof scoord3d */ class Multipoint extends Scoord3D { /** * @param {Object} options * @param {string} options.frameOfReferenceUID - Unique identifier of the Frame of Reference * @param {number[][]} options.coordinates - (x, y, z) coordinates of each point. * @param {string} [options.fiducialUID] - Unique identifier of an imaging fiducial */ constructor(options) { if (!Array.isArray(options.coordinates)) { throw new Error('Argument "coordinates" of Multipoint must be an array.') } if (options.coordinates.find((c) => c.length !== 3) !== undefined) { throw new Error( 'Argument "coordinates" of Multipoint must be an array of ' + '(x, y, z) triplets.', ) } if (options.coordinates.find((c) => c.some((item) => item < 0))) { console.warn('coordinates of Multipoint contain negative numbers') } super({ coordinates: options.coordinates, frameOfReferenceUID: options.frameOfReferenceUID, fiducialUID: options.fiducialUID, }) Object.freeze(this) } get graphicType() { return 'MULTIPOINT' } } /** * POLYLINE graphic denoted by multiple, ordered (x, y, z) coordinates that * represent vertices of connected line segments. * * @class * @extends scoord3d.Scoord3D * @memberof scoord3d */ class Polyline extends Scoord3D { /** * @param {Object} options * @param {string} options.frameOfReferenceUID - Unique identifier of the Frame of Reference * @param {number[][]} options.coordinates - (x, y, z) coordinates of point on the line * @param {string} [options.fiducialUID] - Unique identifier of an imaging fiducial */ constructor(options) { if (!Array.isArray(options.coordinates)) { throw new Error('Argument "coordinates" of Polyline must be an array.') } if (options.coordinates.find((c) => c.length !== 3) !== undefined) { throw new Error( 'Argument "coordinates" of Polyline must be an array of ' + '(x, y, z) triplets.', ) } if (options.coordinates.find((c) => c.some((item) => item < 0))) { console.warn('coordinates of Polyline contain negative numbers') } super({ coordinates: options.coordinates, frameOfReferenceUID: options.frameOfReferenceUID, fiducialUID: options.fiducialUID, }) Object.freeze(this) } get graphicType() { return 'POLYLINE' } } /** * POLYGON graphic denoted by multiple, ordered, coplaner (x, y, z) coordinates * that represent vertices of connected line segments. * * @class * @extends scoord3d.Scoord3D * @memberof scoord3d */ class Polygon extends Scoord3D { /** * @param {Object} options * @param {string} options.frameOfReferenceUID - Unique identifier of the Frame of Reference * @param {number[][]} options.coordinates - (x, y, z) coordinates of points on the perimeter of the polygon (first and last coordinate must be the same). * @param {string} [options.fiducialUID] - Unique identifier of an imaging fiducial */ constructor(options) { if (!Array.isArray(options.coordinates)) { throw new Error('Argument "coordinates" of Polygon must be an array.') } if (options.coordinates.find((c) => c.length !== 3) !== undefined) { throw new Error( 'Argument "coordinates" of Polygon must be an array of ' + '(x, y, z) triplets.', ) } if (options.coordinates.find((c) => c.some((item) => item < 0))) { console.warn('coordinates of Polygon contain negative numbers') } // Note: the POLYGON GraphicType value for ANN specifies that the // first and last points are implicitly joined, so the first point // should not be repeated at the end (unlike other uses in other IODs). // Reference: https://github.com/ImagingDataCommons/slim/issues/298#issuecomment-2959241315 // // const n = options.coordinates.length // if ((options.coordinates[0][0] !== options.coordinates[n - 1][0]) || // (options.coordinates[0][1] !== options.coordinates[n - 1][1]) || // (options.coordinates[0][2] !== options.coordinates[n - 1][2])) { // throw new Error('First and last coordinate of Polygon must be the same.') // } super({ coordinates: options.coordinates, frameOfReferenceUID: options.frameOfReferenceUID, fiducialUID: options.fiducialUID, }) Object.freeze(this) } get graphicType() { return 'POLYGON' } } /** * ELLIPSOID graphic denoted by six (x, y, z) coordinates that represent * endpoints of the three orthogonal geometric axes, where the first and second * coordinates represent the endpoints of the first axis, the third and forth * coordinates represent the endpoints of the second axis and the fifth and * sixth coordinates represent the endpoints of the third axis. * * @class * @extends scoord3d.Scoord3D * @memberof scoord3d */ class Ellipsoid extends Scoord3D { /** * @param {Object} options * @param {string} options.frameOfReferenceUID - Unique identifier of the Frame of Reference * @param {number[][]} options.coordinates - (x, y, z) coordinates of the three axes endpoints * @param {string} [options.fiducialUID] - Unique identifier of an imaging fiducial */ constructor(options) { if (!Array.isArray(options.coordinates)) { throw new Error('Argument "coordinates" of Ellipsoid must be an array.') } if (options.coordinates.length !== 6) { throw new Error( 'Argument "coordinates" of Ellipsoid must be an array of length 6.', ) } if (options.coordinates.find((c) => c.length !== 3) !== undefined) { throw new Error( 'Argument "coordinates" of Ellipsoid must be an array of ' + '(x, y, z) triplets.', ) } if (options.coordinates.find((c) => c.some((item) => item < 0))) { console.warn('coordinates of Ellipsoid contain negative numbers') } super({ coordinates: options.coordinates, frameOfReferenceUID: options.frameOfReferenceUID, fiducialUID: options.fiducialUID, }) Object.freeze(this) } get graphicType() { return 'ELLIPSOID' } } /** * ELLIPSE graphic denoted by four, coplaner (x, y, z) coordinates that represent * the endpoints of the major and minor axes, where the first and second * coordinates represent the endpoints of the major axis and the third and * forth coordinates represent the endpoints of the minor axis. * * @class * @extends scoord3d.Scoord3D * @memberof scoord3d */ class Ellipse extends Scoord3D { /** * @param {Object} options * @param {string} options.frameOfReferenceUID - Unique identifier of the Frame of Reference * @param {number[][]} options.coordinates - (x, y, z) coordinates of the major and minor axes endpoints * @param {string} [options.fiducialUID] - Unique identifier of an imaging fiducial */ constructor(options) { if (!Array.isArray(options.coordinates)) { throw new Error('Argument "coordinates" of Ellipse must be an array.') } if (options.coordinates.length !== 4) { throw new Error( 'Argument "coordinates" of Ellipse must be an array of length 4.', ) } if (options.coordinates.find((c) => c.length !== 3) !== undefined) { throw new Error( 'Argument "coordinates" of Ellipse must be an array of ' + '(x, y, z) triplets.', ) } if (options.coordinates.find((c) => c.some((item) => item < 0))) { console.warn('coordinates of Ellipse contain negative numbers') } const firstAxis = [ options.coordinates[0][0] - options.coordinates[1][0], options.coordinates[0][1] - options.coordinates[1][1], ] const secondAxis = [ options.coordinates[2][0] - options.coordinates[3][0], options.coordinates[2][1] - options.coordinates[3][1], ] const firstAxisNorm = Math.sqrt(firstAxis[0] ** 2 + firstAxis[1] ** 2) const secondAxisNorm = Math.sqrt(secondAxis[0] ** 2 + secondAxis[1] ** 2) // An ellipse with one or both axes as the zero vector is legitimate. // This might typically occur when an ellipse is first created in a viewer. // Only check for perpendicular axes if both axes are not zero. const ZERO_EPS = 1e-6 if (firstAxisNorm > ZERO_EPS && secondAxisNorm > ZERO_EPS) { const ORTHO_EPS = 1e-6 const dot = firstAxis[0] * secondAxis[0] + firstAxis[1] * secondAxis[1] const normalizedDot = dot / (firstAxisNorm * secondAxisNorm) if (Math.abs(normalizedDot) > ORTHO_EPS) { throw new Error('Ellipse axes must be perpendicular') } } let coordinates = options.coordinates if (firstAxisNorm < secondAxisNorm) { coordinates = [ coordinates[2], coordinates[3], coordinates[0], coordinates[1], ] } super({ coordinates, frameOfReferenceUID: options.frameOfReferenceUID, fiducialUID: options.fiducialUID, }) Object.freeze(this) } get graphicType() { return 'ELLIPSE' } } export { Point, Multipoint, Polyline, Polygon, Ellipse, Ellipsoid }