# Source: scoord3d.js

    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.
     * The first and last coordinate should be identical.
     *
     * @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')
        }
        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(
          Math.pow(firstAxis[0], 2) + Math.pow(firstAxis[1], 2)
        )
        const secondAxisNorm = Math.sqrt(
          Math.pow(secondAxis[0], 2) + Math.pow(secondAxis[1], 2)
        )
        const dotProduct = firstAxis[0] * secondAxis[0] + firstAxis[1] * secondAxis[1]
        const angle = Math.acos(dotProduct / (firstAxisNorm * secondAxisNorm))
        const degrees = angle * 180 / Math.PI
        if (degrees !== 90) {
          throw new Error('Two axis of Ellipse must have right angle')
        }
        let coordinates = options.coordinates
        if (firstAxisNorm < secondAxisNorm) {
          coordinates = [
            coordinates[2],
            coordinates[3],
            coordinates[0],
            coordinates[1]
          ]
        }
        super({
          coordinates: coordinates,
          frameOfReferenceUID: options.frameOfReferenceUID,
          fiducialUID: options.fiducialUID
        })
        Object.freeze(this)
      }

      get graphicType () {
        return 'ELLIPSE'
      }
    }

    export { Point, Multipoint, Polyline, Polygon, Ellipse, Ellipsoid }