shared/Point2D.js

'use strict';

/**
 * Defines a set of basic operations on a point in a two dimensional space.
 *
 * @memberof module:shared
 *
 * @param {number} [x=0] - x coordinate of the point.
 * @param {number} [y=0] - y coordinate of the point.
 */
class Point2D {
  constructor(x = 0, y = 0) {
    /**
     * X coordinate of the point.
     *
     * @type {number}
     */
    this.x = x;

    /**
     * Y coordinate of the point.
     *
     * @type {number}
     */
    this.y = y;
  }

  /**
   * Add the given point to this point.
   *
   * @param {module:shared.Point2D} point - The point to add.
   *
   * @return {module:shared.Point2D} this
   */
  add({ x = 0, y = 0 }) {
    this.x += x;
    this.y += y;
    return this;
  }

  /**
   * Calculates the angle between this point and the given point.
   *
   * @param {!module:shared.Point2D} point - Projected point for calculating
   * the angle.
   *
   * @return {number} Radians along the unit circle where the projected point
   * lies.
   */
  angleTo(point) {
    return Math.atan2(point.y - this.y, point.x - this.x);
  }

  /**
   * Determine the average distance from this point to the provided array of
   * points.
   *
   * @param {!module:shared.Point2D[]} points - the Point2D objects to
   * calculate the average distance to.
   *
   * @return {number} The average distance from this point to the provided
   * points.
   */
  averageDistanceTo(points) {
    return this.totalDistanceTo(points) / points.length;
  }

  /**
   * Clones this point.
   *
   * @returns {module:shared.Point2D} An exact clone of this point.
   */
  clone() {
    return new Point2D(this.x, this.y);
  }

  /**
   * Calculates the distance between two points.
   *
   * @param {!module:shared.Point2D} point - Point to which the distance is
   * calculated.
   *
   * @return {number} The distance between the two points, a.k.a. the
   * hypoteneuse.
   */
  distanceTo(point) {
    return Math.hypot(point.x - this.x, point.y - this.y);
  }

  /**
   * Divide the point's values by the given amount.
   *
   * @param {number} coefficient - divide x,y by this amount.
   *
   * @return {module:shared.Point2D} this
   */
  divideBy(coefficient = 1) {
    this.x /= coefficient;
    this.y /= coefficient;
    return this;
  }

  /**
   * Tests if a point is Left|On|Right of an infinite line. Assumes that the
   * given points are such that one is above and one is below this point. Note
   * that the semantics of left/right is based on the normal coordinate space,
   * not the y-axis-inverted coordinate space of images and the canvas.
   *
   * @see {@link http://geomalgorithms.com/a03-_inclusion.html}
   *
   * @param {module:shared.Point2D} p0 - first point of the line.
   * @param {module:shared.Point2D} p1 - second point of the line.
   *
   * @return {number} >0 if this point is left of the line through p0 and p1
   * @return {number} =0 if this point is on the line
   * @return {number} <0 if this point is right of the line
   */
  isLeftOf(p0, p1) {
    const dl = p1.minus(p0);
    const dp = this.minus(p0);
    return dl.x * dp.y - dl.y * dp.x;
  }

  /**
   * Subtracts the given point from this point to form a new point.
   *
   * @param {module:shared.Point2D} p - Point to subtract from this point.
   *
   * @return {module:shared.Point2D} A new point which is the simple subraction
   * of the given point from this point.
   */
  minus({ x = 0, y = 0 }) {
    return new Point2D(this.x - x, this.y - y);
  }

  /**
   * Multiply this point by the given point, in-place.
   *
   * @param {number} coefficient - Amount by which to multiply the values in
   * this point.
   *
   * @return {module:shared.Point2D} this
   */
  multiplyBy(coefficient = 1) {
    this.x *= coefficient;
    this.y *= coefficient;
    return this;
  }

  /**
   * Rotate the point by theta radians.
   *
   * @param {number} theta - Amount of rotation to apply, in radians.
   *
   * @return {module:shared.Point2D} this
   */
  rotate(theta = 0) {
    const { x, y } = this;
    const cosTheta = Math.cos(theta);
    const sinTheta = Math.sin(theta);

    this.x = x * cosTheta - y * sinTheta;
    this.y = x * sinTheta + y * cosTheta;

    return this;
  }

  /**
   * Calculates the total distance from this point to an array of points.
   *
   * @param {!module:shared.Point2D[]} points - The array of Point2D objects to
   * calculate the total distance to.
   *
   * @return {number} The total distance from this point to the provided points.
   */
  totalDistanceTo(points) {
    return points.reduce((d, p) => d + this.distanceTo(p), 0);
  }

  /**
   * Calculates the midpoint of a list of points.
   *
   * @param {module:shared.Point2D[]} points - The array of Point2D objects for
   * which to calculate the midpoint
   *
   * @return {?module:shared.Point2D} The midpoint of the provided points. Null
   * if the provided array is empty.
   */
  static midpoint(points = []) {
    if (points.length === 0) return null;

    const total = Point2D.sum(points);
    return new Point2D(total.x / points.length, total.y / points.length);
  }

  /**
   * Calculates the sum of the given points.
   *
   * @param {module:shared.Point2D[]} points - The Point2D objects to sum up.
   *
   * @return {module:shared.Point2D} A new Point2D representing the sum of the
   * given points.
   */
  static sum(points = []) {
    return points.reduce((total, pt) => total.add(pt), new Point2D(0, 0));
  }
}

module.exports = Point2D;