Home Reference Source

src/Point.js

import {isNumeric, EPSILON, AngularSystem, getRadians, roundNumber, PRECISION} from './utils'

// noinspection SpellCheckingInspection
/**
 * Class representing  a point in 2 dimension cartesian space
 */
export default class Point {
  /**
   * Creates a point
   * @param {number} x coordinates in cartesian space or array with [x, y] numbers
   * @param {number} y coordinates in cartesian space, ignored if first argument is an array
   */
  constructor (x = 0, y = 0) {
    // allow first argument to be an array if values in 2 first elements are numbers
    if ((typeof x[0] === 'number') && (typeof x[1] === 'number')) {
      // here i am using x and y attribute setter so that correct checks are done
      this.x = x[0]
      this.y = x[1]
    } else {
      // using x and y attribute setter
      this.x = x
      this.y = y
    }
  }

  /**
   * creates a new Point in cartesian space from polar coordinates
   * @param {number} radius is the distance from origin to the point
   * @param {number} theta is the angle from x axes origin to point in mathematical order Counter-Clockwise
   * @param {Object} angleSystem your choice of one of AngularSystem Enum Radian, Degree or Gradians
   * @returns {Point} a new Point(x,y) located at the given polar coordinates
   */
  static fromPolar (radius, theta, angleSystem = AngularSystem.DEGREE) {
    if ((isNumeric(radius)) && (isNumeric(theta))) {
      let angle = 0
      if (angleSystem === AngularSystem.RADIAN) {
        angle = theta
      } else {
        angle = getRadians(theta)
      }
      let tmpPoint = new Point()
      tmpPoint._radius = radius
      tmpPoint._theta = angle // we store angle in radians
      const tmpX = radius * Math.cos(angle)
      tmpPoint.x = Math.abs(tmpX) <= EPSILON ? 0 : roundNumber(tmpX, PRECISION)
      const tmpY = radius * Math.sin(angle)
      // noinspection JSSuspiciousNameCombination
      tmpPoint.y = Math.abs(tmpY) <= EPSILON ? 0 : roundNumber(tmpY, PRECISION)
      return tmpPoint
    } else {
      throw new TypeError('fromPolar needs radius and theta to be valid numbers !')
    }
  }

  /**
   * get a new Point that is a copy (clone) of the otherPoint passed has parameter
   * @param {Point} otherPoint is the Point you want to copy
   * @returns {Point} a new Point located at the same cartesian coordinates as otherPoint
   */
  static fromPoint (otherPoint) {
    if (otherPoint instanceof Point) {
      return new Point(otherPoint.x, otherPoint.y)
    } else {
      throw new TypeError('fromPoint needs parameter otherPoint of type Point')
    }
  }

  /**
   * Get the x value.
   * @return {number} The x value.
   */
  get x () {
    return this._x
  }

  /**
   * Set the x value
   * @param {number} value is the new numeric value for x
   */
  set x (value) {
    if (isNumeric(value)) {
      // noinspection JSCheckFunctionSignatures
      this._x = parseFloat(value)
    } else {
      this._x = NaN
      this.isInvalid = true
      this.InvalidReason = `cannot set x to ${value} because it is not a numeric value`
      throw new TypeError(`Point.x setter needs a numeric value and ${value} is not`)
    }
  }

  /**
   * Get the y value.
   * @return {number} The y value.
   */
  get y () {
    return this._y
  }

  /**
   * Set the y value
   * @param {number} value is the new numeric value for y
   */
  set y (value) {
    if (isNumeric(value)) {
      // noinspection JSCheckFunctionSignatures
      this._y = parseFloat(value)
    } else {
      this._y = NaN
      this.isInvalid = true
      this.InvalidReason = `cannot set y to ${value} because it is not a numeric value`
      throw new TypeError(`Point.y setter needs a numeric value and ${value} is not`)
    }
  }

  /**
   * give a string representation of this class instance
   * @param {string} separator placed between x and y values ', ' by default
   * @param {boolean} surroundingParenthesis allow to tell if result string should be surrounded with parenthesis (True by default)
   * @param {number} precision defines the number of decimals for the coordinates (2 by default)
   * @returns {string}
   */
  toString (separator = ',', surroundingParenthesis = true, precision = 2) {
    if (surroundingParenthesis) {
      return `(${roundNumber(this.x, precision)}${separator} ${roundNumber(this.y, 2)})`
    } else {
      return `${roundNumber(this.x, precision)}${separator} ${roundNumber(this.y, 2)}`
    }
  }

  /**
   * give an array representation of this Point class instance [x, y]
   * @returns {Array} [x, y]
   */
  toArray () {
    return [this.x, this.y]
  }

  /**
   * give an OGC Well-known text (WKT) representation of this class instance
   * https://en.wikipedia.org/wiki/Well-known_text
   * @returns {string}
   */
  toWKT () {
    return `POINT(${this.x} ${this.y})`
  }

  /**
   * give an Postgis Extended Well-known text (EWKT) representation of this class instance
   * https://postgis.net/docs/using_postgis_dbmanagement.html#EWKB_EWKT
   * @param {number} srid is the Spatial reference systems identifier EPSG code default is 21781 for Switzerland MN03
   * @returns {string}
   */
  toEWKT (srid = 21781) {
    return `SRID=${srid};POINT(${this.x} ${this.y})`
  }

  // TO implement toEWKB I can maybe use this lib : https://github.com/cschwarz/wkx

  /**
   * give a GeoJSON (http://geojson.org/) representation of this class instance geometry
   * @returns {string}
   */
  toGeoJSON () {
    return `{"type":"Point","coordinates":[${this.x},${this.y}]}`
  }

  /**
   * will move this Point to the new position in cartesian space given by the arrCoordinates
   * @param {Array} arrCoordinates is an array with the 2 cartesian coordinates [x, y]
   * @returns {Point} return this instance of the object (to allow function chaining)
   */
  moveToArray (arrCoordinates) {
    if ((isNumeric(arrCoordinates[0])) && (isNumeric(arrCoordinates[1]))) {
      this.x = arrCoordinates[0]
      this.y = arrCoordinates[1]
      return this
    } else {
      throw new TypeError('moveToArray needs an array of 2 numbers like this [1.0, 2.0]')
    }
  }

  /**
   * will move this Point to the new position in cartesian space given by the newX and newY values
   * @param {number} newX is the new x coordinates in cartesian space of this Point
   * @param {number} newY is the new y coordinates in cartesian space of this Point
   * @returns {Point} return this instance of the object (to allow function chaining)
   */
  moveTo (newX, newY) {
    if ((isNumeric(newX)) && (isNumeric(newY))) {
      this.x = newX
      this.y = newY
      return this
    } else {
      throw new TypeError('moveTo needs newX and newY to be valid numbers !')
    }
  }

  /**
   * move this Point relative to its position by the arrVector displacement in cartesian space
   * @param {Array} arrVector is an array representing the vector displacement to apply to actual coordinates [deltaX, deltaY]
   * @returns {Point} return this instance of the object (to allow function chaining)
   */
  moveRelArray (arrVector) {
    if ((isNumeric(arrVector[0])) && (isNumeric(arrVector[1]))) {
      this.x = this.x + arrVector[0]
      this.y = this.y + arrVector[1]
      return this
    } else {
      throw new TypeError('moveRelArray needs an array of 2 numbers like this [1.0, 2.0]')
    }
  }

  /**
   * move this Point relative to its position by the deltaX, deltaY displacement in cartesian space
   * @param {number} deltaX is the new x coordinates in cartesian space of this Point
   * @param {number} deltaY is the new y coordinates in cartesian space of this Point
   * @returns {Point} return this instance of the object (to allow function chaining)
   */
  moveRel (deltaX, deltaY) {
    if ((isNumeric(deltaX)) && (isNumeric(deltaY))) {
      this.x = this.x + deltaX
      this.y = this.y + deltaY
      return this
    } else {
      throw new TypeError('moveRel needs deltaX and deltaY to be valid numbers !')
    }
  }

  /**
   * move this Point relative to its position by the polar displacement in cartesian space
   * @param {number} radius is the distance from origin to the point
   * @param {number} theta is the angle from x axes origin to point in mathematical order Counter-Clockwise
   * @param {Object} angleSystem your choice of one of AngularSystem Enum Radian, Degree or Gradians
   * @returns {Point} return this instance of the object (to allow function chaining)
   */
  moveRelPolar (radius, theta, angleSystem = AngularSystem.DEGREE) {
    let tmpPoint = Point.fromPolar(radius, theta, angleSystem)
    this.x = this.x + tmpPoint.x
    this.y = this.y + tmpPoint.y
    return this
  }

  /**
   * copy this Point relative to its position by the arrVector displacement in cartesian space
   * @param {Array} arrVector is an array representing the vector displacement to apply to actual coordinates [deltaX, deltaY]
   * @returns {Point} a new Point object at the relative displacement arrVector from original Point
   */
  copyRelArray (arrVector) {
    if ((isNumeric(arrVector[0])) && (isNumeric(arrVector[1]))) {
      let tmpPoint = Point.fromPoint(this)
      tmpPoint.x = tmpPoint.x + arrVector[0]
      tmpPoint.y = tmpPoint.y + arrVector[1]
      return tmpPoint
    } else {
      throw new TypeError('copyRelArray needs an array of 2 numbers like this [1.0, 2.0]')
    }
  }

  /**
   * copy this Point relative to its position by the deltaX, deltaY displacement in cartesian space
   * @param {number} deltaX is the increment to x coordinates to this Point
   * @param {number} deltaY is the increment to y coordinates to this Point
   * @returns {Point} a new Point object at the relative deltaX, deltaY displacement from original Point
   */
  copyRel (deltaX, deltaY) {
    if ((isNumeric(deltaX)) && (isNumeric(deltaY))) {
      let tmpPoint = Point.fromPoint(this)
      tmpPoint.x = tmpPoint.x + deltaX
      tmpPoint.y = tmpPoint.y + deltaY
      return tmpPoint
    } else {
      throw new TypeError('copyRel needs deltaX and deltaY to be valid numbers !')
    }
  }

  /**
   * copy this Point relative to its position by the polar displacement in cartesian space
   * @param {number} radius is the distance from origin to the point
   * @param {number} theta is the angle from x axes origin to point in mathematical order Counter-Clockwise
   * @param {Object} angleSystem your choice of one of AngularSystem Enum Radian, Degree or Gradians
   * @returns {Point} a new Point at the polar displacement from original Point
   */
  copyRelPolar (radius, theta, angleSystem = AngularSystem.DEGREE) {
    let tmpPoint = Point.fromPolar(radius, theta, angleSystem)
    let tmpPoint2 = Point.fromPoint(this)
    tmpPoint2.x = tmpPoint2.x + tmpPoint.x
    tmpPoint2.y = tmpPoint2.y + tmpPoint.y
    return tmpPoint2
  }

  /**
   * allows to compare equality with otherPoint, they should have the same values for x and y
   * Math.sqrt(2) * Math.sqrt(2) should give 2 but gives instead 2.0000000000000004
   * Math.sqrt(3) * Math.sqrt(3) should give 2 but gives instead 2.9999999999999996
   * i found
   * So the Point Class equality should take this fact account to test near equality with EPSILON=0.0000000001
   *  feel free to adapt EPSILON value to your needs in utils.js
   * @param {Point} otherPoint
   * @returns {boolean}
   */
  equal (otherPoint) {
    if (otherPoint instanceof Point) {
      return (
        (Math.abs(this.x - otherPoint.x) <= EPSILON) &&
        (Math.abs(this.y - otherPoint.y) <= EPSILON)
      )
    } else {
      throw new TypeError('A Point can only be compared to another Point')
    }
  }

  /**
   * get the distance from this point to otherPoint
   * @param {Point} otherPoint
   * @return {Number}
   */
  distance (otherPoint) {
    if (otherPoint instanceof Point) {
      let distance = Math.sqrt(
        ((this.x - otherPoint.x) * (this.x - otherPoint.x)) +
        ((this.y - otherPoint.y) * (this.y - otherPoint.y))
      )
      if (distance <= EPSILON) {
        return 0
      } else {
        return distance
      }
    } else {
      throw new TypeError('Point.distance(otherPoint) expects a Point as parameter')
    }
  }
}