tests_dicom_dicomSpatialCoordinate.test.js

import {describe, test, assert} from 'vitest';
import {
  SpatialCoordinate,
  GraphicTypes,
  getSpatialCoordinate,
  getDicomSpatialCoordinateItem,
  getScoordFromShape,
  getShapeFromScoord
} from '../../src/dicom/dicomSpatialCoordinate.js';
import {DataElement} from '../../src/dicom/dataElement.js';
import {Point2D} from '../../src/math/point.js';
import {Line} from '../../src/math/line.js';
import {Protractor} from '../../src/math/protractor.js';
import {ROI} from '../../src/math/roi.js';
import {Circle} from '../../src/math/circle.js';
import {Ellipse} from '../../src/math/ellipse.js';
import {Rectangle} from '../../src/math/rectangle.js';

/**
 * Related DICOM tag keys.
 */
const TagKeys = {
  PixelOriginInterpretation: '00480301',
  GraphicData: '00700022',
  GraphicType: '00700023',
  FiducialUID: '0070031A'
};

/**
 * Tests for the 'dicom/dicomSpatialCoordinate.js' file.
 */

describe('dicom', () => {

  describe('SpatialCoordinate', () => {

    /**
     * Tests for {@link SpatialCoordinate} undefined.
     *
     * @function module:tests/dicom~spatialcoordinate-undefined
     */
    test('undefined', () => {
      const coord = new SpatialCoordinate();
      assert.isUndefined(coord.graphicData);
      assert.isUndefined(coord.graphicType);
      assert.isUndefined(coord.pixelOriginInterpretation);
      assert.isUndefined(coord.fiducialUID);
    });

    /**
     * Tests for {@link SpatialCoordinate} toString with POINT.
     *
     * @function module:tests/dicom~spatialcoordinate-tostring-point
     */
    test('toString POINT', () => {
      const coord = new SpatialCoordinate();
      coord.graphicType = GraphicTypes.point;
      coord.graphicData = ['1.0', '2.0'];

      const result = coord.toString();
      assert.equal(result, 'POINT {1.0,2.0}');
    });

    /**
     * Tests for {@link SpatialCoordinate} toString with POLYLINE.
     *
     * @function module:tests/dicom~spatialcoordinate-tostring-polyline
     */
    test('toString POLYLINE', () => {
      const coord = new SpatialCoordinate();
      coord.graphicType = GraphicTypes.polyline;
      coord.graphicData = ['1.0', '2.0', '3.0', '4.0'];

      const result = coord.toString();
      assert.equal(result, 'POLYLINE {1.0,2.0,3.0,4.0}');
    });

    /**
     * Tests for {@link SpatialCoordinate} round trip.
     *
     * @function module:tests/dicom~spatialcoordinate-round-trip
     */
    test('round trip',
      () => {
        const deGraphicData = new DataElement('IS');
        deGraphicData.value = ['100', '200'];

        const deGraphicType = new DataElement('CS');
        deGraphicType.value = [GraphicTypes.poin];

        const dePixelOrigin = new DataElement('CS');
        dePixelOrigin.value = ['SCREEN'];

        const deFiducialUID = new DataElement('UI');
        deFiducialUID.value = ['1.2.3.4.5'];

        const dataElements = {
          [TagKeys.GraphicData]: deGraphicData,
          [TagKeys.GraphicType]: deGraphicType,
          [TagKeys.PixelOriginInterpretation]: dePixelOrigin,
          [TagKeys.FiducialUID]: deFiducialUID
        };

        const coord1 = getSpatialCoordinate(dataElements);
        const item = getDicomSpatialCoordinateItem(coord1);

        // recreate SpatialCoordinate from item
        const coord2 = new SpatialCoordinate();
        if (typeof item.GraphicData !== 'undefined') {
          coord2.graphicData = item.GraphicData;
        }
        if (typeof item.GraphicType !== 'undefined') {
          coord2.graphicType = item.GraphicType;
        }
        if (typeof item.PixelOriginInterpretation !== 'undefined') {
          coord2.pixelOriginInterpretation = item.PixelOriginInterpretation;
        }
        if (typeof item.FiducialUID !== 'undefined') {
          coord2.fiducialUID = item.FiducialUID;
        }

        // verify round-trip
        assert.deepEqual(coord1.graphicData, coord2.graphicData);
        assert.equal(coord1.graphicType, coord2.graphicType);
        assert.equal(coord1.pixelOriginInterpretation,
          coord2.pixelOriginInterpretation);
        assert.equal(coord1.fiducialUID, coord2.fiducialUID);
      }
    );

  });

  describe('getSpatialCoordinate', () => {

    /**
     * Tests for {@link getSpatialCoordinate}.
     *
     * @function module:tests/dicom~getspatialcoordinate-good-input
     */
    test('good input', () => {
      const deGraphicData = new DataElement('IS');
      deGraphicData.value = ['100', '200'];

      const deGraphicType = new DataElement('CS');
      deGraphicType.value = [GraphicTypes.point];

      const dePixelOrigin = new DataElement('CS');
      dePixelOrigin.value = ['SCREEN'];

      const deFiducialUID = new DataElement('UI');
      deFiducialUID.value = ['1.2.3.4.5'];

      const dataElements = {
        [TagKeys.GraphicData]: deGraphicData,
        [TagKeys.GraphicType]: deGraphicType,
        [TagKeys.PixelOriginInterpretation]: dePixelOrigin,
        [TagKeys.FiducialUID]: deFiducialUID
      };

      const result = getSpatialCoordinate(dataElements);

      assert.deepEqual(result.graphicData, ['100', '200']);
      assert.equal(result.graphicType, GraphicTypes.point);
      assert.equal(result.pixelOriginInterpretation, 'SCREEN');
      assert.equal(result.fiducialUID, '1.2.3.4.5');
    });

    /**
     * Tests for {@link getSpatialCoordinate} with minimum input.
     *
     * @function module:tests/dicom~getspatialcoordinate-minimum-input
     */
    test('minimum input', () => {
      const deGraphicData = new DataElement('IS');
      deGraphicData.value = ['100', '200', '300', '400'];

      const deGraphicType = new DataElement('CS');
      deGraphicType.value = [GraphicTypes.polyline];

      const dataElements = {
        [TagKeys.GraphicData]: deGraphicData,
        [TagKeys.GraphicType]: deGraphicType
      };

      const result = getSpatialCoordinate(dataElements);

      assert.deepEqual(result.graphicData, ['100', '200', '300', '400']);
      assert.equal(result.graphicType, GraphicTypes.polyline);
      assert.isUndefined(result.pixelOriginInterpretation);
      assert.isUndefined(result.fiducialUID);
    });

    /**
     * Tests for {@link getSpatialCoordinate} with empty input.
     *
     * @function module:tests/dicom~getspatialcoordinate-empty-input
     */
    test('empty input', () => {
      const result = getSpatialCoordinate({});

      assert.isUndefined(result.graphicData);
      assert.isUndefined(result.graphicType);
      assert.isUndefined(result.pixelOriginInterpretation);
      assert.isUndefined(result.fiducialUID);
    });

    /**
     * Tests for {@link getSpatialCoordinate} with multiple input.
     *
     * @function module:tests/dicom~getspatialcoordinate-multiple-input
     */
    test('multiple input', () => {
      const deGraphicType = new DataElement('CS');
      deGraphicType.value = [GraphicTypes.point, 'EXTRA'];

      const dataElements = {
        [TagKeys.GraphicType]: deGraphicType
      };

      const result = getSpatialCoordinate(dataElements);

      assert.equal(result.graphicType, GraphicTypes.point);
    });

  });

  describe('getDicomSpatialCoordinateItem', () => {

    /**
     * Tests for {@link getDicomSpatialCoordinateItem}.
     *
     * @function module:tests/dicom~getdicomspatialcoordinateitem-good-input
     */
    test('good input', () => {
      const coord = new SpatialCoordinate();
      coord.graphicData = ['100', '200'];
      coord.graphicType = GraphicTypes.point;
      coord.pixelOriginInterpretation = 'SCREEN';
      coord.fiducialUID = '1.2.3.4.5';

      const item = getDicomSpatialCoordinateItem(coord);

      assert.deepEqual(item.GraphicData, ['100', '200']);
      assert.equal(item.GraphicType, GraphicTypes.point);
      assert.equal(item.PixelOriginInterpretation, 'SCREEN');
      assert.equal(item.FiducialUID, '1.2.3.4.5');
    });

    /**
     * Tests for {@link getDicomSpatialCoordinateItem} with incomplete input.
     *
     * @function module:tests/dicom~getdicomspatialcoordinateitem-incomplete
     */
    test('incomplete', () => {
      const coord = new SpatialCoordinate();
      coord.graphicType = GraphicTypes.circle;
      coord.graphicData = ['50', '50', '150', '150'];
      coord.fiducialUID = '1.2.3.4.5';

      const item = getDicomSpatialCoordinateItem(coord);

      assert.isUndefined(item.PixelOriginInterpretation);
      assert.equal(item.GraphicType, GraphicTypes.circle);
      assert.deepEqual(item.GraphicData, ['50', '50', '150', '150']);
      assert.equal(item.FiducialUID, '1.2.3.4.5');
    });

    /**
     * Tests for {@link getDicomSpatialCoordinateItem} with undefined input.
     *
     * @function module:tests/dicom~getdicomspatialcoordinateitem-undefined
     */
    test('undefined', () => {
      const coord = new SpatialCoordinate();
      const item = getDicomSpatialCoordinateItem(coord);

      assert.deepEqual(item, {});
    });

  });

  describe('getScoordFromShape', () => {

    /**
     * Tests for {@link getScoordFromShape} with Point2D.
     *
     * @function module:tests/dicom~getscoordfromshape-point2d
     */
    test('Point2D', () => {
      const point = new Point2D(10, 20);
      const scoord = getScoordFromShape(point);

      assert.equal(scoord.graphicType, GraphicTypes.point);
      assert.equal(scoord.graphicData.length, 2);
      assert.equal(scoord.graphicData[0], '10');
      assert.equal(scoord.graphicData[1], '20');
    });

    /**
     * Tests for {@link getScoordFromShape} with Line.
     *
     * @function module:tests/dicom~getscoordfromshape-line
     */
    test('Line', () => {
      const line = new Line(new Point2D(10, 20), new Point2D(30, 40));
      const scoord = getScoordFromShape(line);

      assert.equal(scoord.graphicType, GraphicTypes.polyline);
      assert.equal(scoord.graphicData.length, 4);
      assert.equal(scoord.graphicData[0], '10');
      assert.equal(scoord.graphicData[1], '20');
      assert.equal(scoord.graphicData[2], '30');
      assert.equal(scoord.graphicData[3], '40');
    });

    /**
     * Tests for {@link getScoordFromShape} with Circle.
     *
     * @function module:tests/dicom~getscoordfromshape-circle
     */
    test('Circle', () => {
      const circle = new Circle(new Point2D(50, 50), 25);
      const scoord = getScoordFromShape(circle);

      assert.equal(scoord.graphicType, GraphicTypes.circle);
      assert.equal(scoord.graphicData.length, 4);
      // Center
      assert.equal(scoord.graphicData[0], '50');
      assert.equal(scoord.graphicData[1], '50');
      // Perimeter point
      assert.equal(scoord.graphicData[2], '75');
      assert.equal(scoord.graphicData[3], '50');
    });

    /**
     * Tests for {@link getScoordFromShape} with Protractor.
     *
     * @function module:tests/dicom~getscoordfromshape-protractor
     */
    test('Protractor', () => {
      const protractor = new Protractor([
        new Point2D(10, 10),
        new Point2D(20, 20),
        new Point2D(30, 10)
      ]);
      const scoord = getScoordFromShape(protractor);

      assert.equal(scoord.graphicType, GraphicTypes.polyline);
      assert.equal(scoord.graphicData.length, 6);
      // Three points from protractor
      assert.equal(scoord.graphicData[0], '10');
      assert.equal(scoord.graphicData[1], '10');
      assert.equal(scoord.graphicData[2], '20');
      assert.equal(scoord.graphicData[3], '20');
      assert.equal(scoord.graphicData[4], '30');
      assert.equal(scoord.graphicData[5], '10');
    });

    /**
     * Tests for {@link getScoordFromShape} with ROI.
     *
     * @function module:tests/dicom~getscoordfromshape-roi
     */
    test('ROI', () => {
      const roi = new ROI();
      roi.addPoint(new Point2D(10, 10));
      roi.addPoint(new Point2D(20, 10));
      roi.addPoint(new Point2D(20, 20));
      roi.addPoint(new Point2D(10, 20));

      const scoord = getScoordFromShape(roi);

      assert.equal(scoord.graphicType, GraphicTypes.polyline);
      // 4 points + 1 repeated first point to close = 10 coordinates
      assert.equal(scoord.graphicData.length, 10);
      // First point
      assert.equal(scoord.graphicData[0], '10');
      assert.equal(scoord.graphicData[1], '10');
      // Last point should equal first (closed shape)
      assert.equal(scoord.graphicData[8], '10');
      assert.equal(scoord.graphicData[9], '10');
    });

    /**
     * Tests for {@link getScoordFromShape} with Ellipse.
     *
     * @function module:tests/dicom~getscoordfromshape-ellipse
     */
    test('Ellipse', () => {
      const ellipse = new Ellipse(new Point2D(50, 50), 30, 20);
      const scoord = getScoordFromShape(ellipse);

      assert.equal(scoord.graphicType, GraphicTypes.ellipse);
      // 4 points (left, right, top, bottom) = 8 coordinates
      assert.equal(scoord.graphicData.length, 8);
      // Left point
      assert.equal(scoord.graphicData[0], '20');
      assert.equal(scoord.graphicData[1], '50');
      // Right point
      assert.equal(scoord.graphicData[2], '80');
      assert.equal(scoord.graphicData[3], '50');
      // Top point
      assert.equal(scoord.graphicData[4], '50');
      assert.equal(scoord.graphicData[5], '30');
      // Bottom point
      assert.equal(scoord.graphicData[6], '50');
      assert.equal(scoord.graphicData[7], '70');
    });

    /**
     * Tests for {@link getScoordFromShape} with Rectangle.
     *
     * @function module:tests/dicom~getscoordfromshape-rectangle
     */
    test('Rectangle', () => {
      const rectangle = new Rectangle(
        new Point2D(10, 10),
        new Point2D(30, 30)
      );
      const scoord = getScoordFromShape(rectangle);

      assert.equal(scoord.graphicType, GraphicTypes.polyline);
      // 4 corner points + 1 repeated first point to close = 10 coordinates
      assert.equal(scoord.graphicData.length, 10);
      // First point (top-left, begin)
      assert.equal(scoord.graphicData[0], '10');
      assert.equal(scoord.graphicData[1], '10');
      // Second point (bottom-left)
      assert.equal(scoord.graphicData[2], '10');
      assert.equal(scoord.graphicData[3], '30');
      // Third point (bottom-right, end)
      assert.equal(scoord.graphicData[4], '30');
      assert.equal(scoord.graphicData[5], '30');
      // Fourth point (top-right)
      assert.equal(scoord.graphicData[6], '30');
      assert.equal(scoord.graphicData[7], '10');
      // Last point (closed, repeated first)
      assert.equal(scoord.graphicData[8], '10');
      assert.equal(scoord.graphicData[9], '10');
    });

  });

  describe('getShapeFromScoord', () => {

    /**
     * Tests for {@link getShapeFromScoord} with no data.
     *
     * @function module:tests/dicom~getshapefromscoord-no-data
     */
    test('no data', () => {
      const scoord = new SpatialCoordinate();
      scoord.graphicType = GraphicTypes.point;

      const shape = getShapeFromScoord(scoord);

      assert.isUndefined(shape);
    });

    /**
     * Tests for {@link getShapeFromScoord} with no coord.
     *
     * @function module:tests/dicom~getshapefromscoord-no-coord
     */
    test('no coord', () => {
      const scoord = new SpatialCoordinate();
      scoord.graphicType = GraphicTypes.poin;
      scoord.graphicData = [];

      assert.throws(() => {
        getShapeFromScoord(scoord);
      }, 'No coordinates in scoord data');
    });

    /**
     * Tests for {@link getShapeFromScoord} with odd coord.
     *
     * @function module:tests/dicom~getshapefromscoord-odd-coord
     */
    test('odd coord', () => {
      const scoord = new SpatialCoordinate();
      scoord.graphicType = GraphicTypes.poin;
      scoord.graphicData = ['1', '2', '3'];

      assert.throws(() => {
        getShapeFromScoord(scoord);
      }, 'Expecting even number of coordinates in scoord data');
    });

    /**
     * Tests for {@link getShapeFromScoord} with point.
     *
     * @function module:tests/dicom~getshapefromscoord-point
     */
    test('point', () => {
      const scoord = new SpatialCoordinate();
      scoord.graphicType = GraphicTypes.point;
      scoord.graphicData = ['10', '20'];

      const shape = getShapeFromScoord(scoord);

      assert.ok(shape instanceof Point2D);
      assert.equal(shape.getX(), 10);
      assert.equal(shape.getY(), 20);
    });

    /**
     * Tests for {@link getShapeFromScoord} with polyline 2 points.
     *
     * @function module:tests/dicom~getshapefromscoord-polyline-2-points
     */
    test('polyline 2 points', () => {
      const scoord = new SpatialCoordinate();
      scoord.graphicType = GraphicTypes.polyline;
      scoord.graphicData = ['10', '20', '30', '40'];

      const shape = getShapeFromScoord(scoord);

      assert.ok(shape instanceof Line);
      assert.equal(shape.getBegin().getX(), 10);
      assert.equal(shape.getBegin().getY(), 20);
      assert.equal(shape.getEnd().getX(), 30);
      assert.equal(shape.getEnd().getY(), 40);
    });

    /**
     * Tests for {@link getShapeFromScoord} with circle.
     *
     * @function module:tests/dicom~getshapefromscoord-circle
     */
    test('circle', () => {
      const scoord = new SpatialCoordinate();
      scoord.graphicType = GraphicTypes.circle;
      scoord.graphicData = ['50', '50', '75', '50'];

      const shape = getShapeFromScoord(scoord);

      assert.ok(shape instanceof Circle);
      assert.equal(shape.getCenter().getX(), 50);
      assert.equal(shape.getCenter().getY(), 50);
      assert.equal(shape.getRadius(), 25);
    });

    /**
     * Tests for {@link getShapeFromScoord} with bad circle.
     *
     * @function module:tests/dicom~getshapefromscoord-bad-circle
     */
    test('bad circle', () => {
      const scoord = new SpatialCoordinate();
      scoord.graphicType = GraphicTypes.circle;
      scoord.graphicData = ['50', '50'];

      assert.throws(() => {
        getShapeFromScoord(scoord);
      }, 'Expecting 2 points for circles, got 1');
    });

    /**
     * Tests for {@link getShapeFromScoord} with ellipse.
     *
     * @function module:tests/dicom~getshapefromscoord-ellipse
     */
    test('ellipse', () => {
      const scoord = new SpatialCoordinate();
      scoord.graphicType = GraphicTypes.ellipse;
      // Left, right, top, bottom points
      scoord.graphicData = ['20', '50', '80', '50', '50', '30', '50', '70'];

      const shape = getShapeFromScoord(scoord);

      assert.ok(shape instanceof Ellipse);
      assert.equal(shape.getCenter().getX(), 50);
      assert.equal(shape.getCenter().getY(), 50);
      assert.equal(shape.getA(), 30);
      assert.equal(shape.getB(), 20);
    });

    /**
     * Tests for {@link getShapeFromScoord} with bad ellipse.
     *
     * @function module:tests/dicom~getshapefromscoord-bad-ellipse
     */
    test('bad ellipse', () => {
      const scoord = new SpatialCoordinate();
      scoord.graphicType = GraphicTypes.ellipse;
      scoord.graphicData = ['20', '50', '80', '50', '50', '30'];

      assert.throws(() => {
        getShapeFromScoord(scoord);
      }, 'Expecting 4 points for ellipses, got 3');
    });

    /**
     * Tests for {@link getShapeFromScoord} with polyline 3 points.
     *
     * @function module:tests/dicom~getshapefromscoord-polyline-3-points
     */
    test('polyline 3 points', () => {
      const scoord = new SpatialCoordinate();
      scoord.graphicType = GraphicTypes.polyline;
      // 3 points not closed
      scoord.graphicData = ['10', '10', '20', '20', '30', '10'];

      const shape = getShapeFromScoord(scoord);

      assert.ok(shape instanceof Protractor);
      assert.equal(shape.getPoint(0).getX(), 10);
      assert.equal(shape.getPoint(0).getY(), 10);
      assert.equal(shape.getPoint(1).getX(), 20);
      assert.equal(shape.getPoint(1).getY(), 20);
      assert.equal(shape.getPoint(2).getX(), 30);
      assert.equal(shape.getPoint(2).getY(), 10);
    });

    /**
     * Tests for {@link getShapeFromScoord} with polyline 4 points.
     *
     * @function module:tests/dicom~getshapefromscoord-polyline-4-points
     */
    test('polyline 4 points',
      () => {
        const scoord = new SpatialCoordinate();
        scoord.graphicType = GraphicTypes.polyline;
        // 5 points: 4 non-orthogonal corners with first point
        // repeated (closed but not rectangle)
        scoord.graphicData = [
          '10', '10', '25', '15', '30', '25', '15', '30', '10', '10'
        ];

        const shape = getShapeFromScoord(scoord);

        assert.ok(shape instanceof ROI);
        assert.equal(shape.getLength(), 4);
        assert.equal(shape.getPoint(0).getX(), 10);
        assert.equal(shape.getPoint(0).getY(), 10);
        assert.equal(shape.getPoint(1).getX(), 25);
        assert.equal(shape.getPoint(1).getY(), 15);
        assert.equal(shape.getPoint(2).getX(), 30);
        assert.equal(shape.getPoint(2).getY(), 25);
        assert.equal(shape.getPoint(3).getX(), 15);
        assert.equal(shape.getPoint(3).getY(), 30);
      }
    );

    /**
     * Tests for {@link getShapeFromScoord} with polyline 6 points.
     *
     * @function module:tests/dicom~getshapefromscoord-polyline-6-points
     */
    test('polyline 6 points',
      () => {
        const scoord = new SpatialCoordinate();
        scoord.graphicType = GraphicTypes.polyline;
        // 7 points: 6 points with first repeated (closed)
        scoord.graphicData = [
          '0',
          '0',
          '10',
          '5',
          '20',
          '0',
          '25',
          '15',
          '15',
          '25',
          '5',
          '20',
          '0',
          '0'
        ];

        const shape = getShapeFromScoord(scoord);

        assert.ok(shape instanceof ROI);
        assert.equal(shape.getLength(), 6);
        assert.equal(shape.getPoint(0).getX(), 0);
        assert.equal(shape.getPoint(0).getY(), 0);
        assert.equal(shape.getPoint(5).getX(), 5);
        assert.equal(shape.getPoint(5).getY(), 20);
      }
    );

    /**
     * Tests for {@link getShapeFromScoord} with mulitpoint.
     *
     * @function module:tests/dicom~getshapefromscoord-multipoint
     */
    test('multipoint', () => {
      const scoord = new SpatialCoordinate();
      scoord.graphicType = GraphicTypes.multipoint;
      // Multiple points not closed
      scoord.graphicData = ['10', '10', '20', '20', '30', '30'];

      const shape = getShapeFromScoord(scoord);

      // multipoint type is not yet implemented, returns undefined
      assert.isUndefined(shape);
    });

  });

});