﻿var geometry = {
                     EARTH_RADIUS_KM: 6371
                   };

    var pi = 3.14159265;
  var pi2 = 6.28318531;

  geometry.PI = pi;
  geometry.PI2 = pi2;

  geometry.MapDirection = {
    North:     {Name: "North"},
    NorthEast: {Name: "NorthEast"},
    East:      {Name: "East"},
    SouthEast: {Name: "SouthEast"},
    South:     {Name: "South"},
    SouthWest: {Name: "SouthWest"},
    West:      {Name: "West"},
    NorthWest: {Name: "NorthWest"}
  };

    // Returns pixel distance between two points
  geometry.distance = function(x1, y1, x2, y2) { return Math.sqrt(Math.pow(x1 - x2, 2) + Math.pow(y1 - y2, 2)); };

    // Returns pixel distance between two points
  geometry.distancePoints = function(p1, p2) {  return geometry.distance(p1.x(), p1.y(),  p2.x(), p2.y()); };


  // Converts radians to degrees
  geometry.radToDeg = function(rad){ return (rad / pi) * 180; };

  // Converts defrees to rads
  geometry.degToRad = function(deg){ return (deg / 180) * pi; };


    // Returns azimuth angle (theta) in radians
  geometry.azimuthRad = function(xc, yc, xd, yd) {
    var angle = Math.atan2(yd - yc, xd - xc);
    if (angle < 0) angle = pi2 + angle;
    return angle;
  };

    // Returns azimuth angle (theta) in radians for two points: center and destination
  geometry.azimuthRadPoints = function(pc, pd) {  return geometry.azimuthRad(pc.x(), pc.y(),  pd.x(), pd.y()); };

    // Returns azimuth angle (theta) in degrees
  geometry.azimuthDeg = function(xc, yc, xd, yd){ return (geometry.azimuthRad(xc, yc, xd, yd) / pi2) * 360; };

    // Returns azimuth angle (theta) in degrees for twho points: center and destination
  geometry.azimuthDegPoints = function(pc, pd){ return geometry.azimuthDeg(pc.x(), pc.y(),  pd.x(), pd.y()); };

    // Returns azimuth angle (theta) in radix units
  geometry.azimuthOfRadix = function(xc, yc, xd, yd, radix) {
    if (radix < 2) radix = 2;
    var angle = geometry.azimuthRad(xc, yc, xd, yd);
    var half = pi / radix;
    angle = geometry.wrapAngle(angle, half);
    return Math.floor((angle / pi2) * radix);
  };

    // Returns azimuth angle (theta) in in radix units for twho points: center and destination
  geometry.azimuthOfRadixPoints = function(pc, pd, radix){ return geometry.azimuthOfRadix(pc.x(), pc.y(),  pd.x(), pd.y(), radix); };

    //Returns rectangle from coordinate pairs
  geometry.toRectXY = function(x1,y1, x2,y2) {
    return new geometry.Rectangle( new geometry.Point(x1, y1), new geometry.Point(x2, y2) );
  };

    // Returns rectangle from coordinats and dimensions
  geometry.toRectWH = function(x1,y1, w,h) {
    return new geometry.Rectangle( new geometry.Point(x1, y1), new geometry.Point(x1 + w, y1 + h) );
  };

    // Returns area of overlap between two rectangles
  geometry.overlapAreaRect = function(rect1, rect2) {
    var tl1 = rect1.topLeft();
    var tl2 = rect2.topLeft();
    return geometry.overlapAreaWH(
      tl1.x(), tl1.y(), rect1.width(), rect1.height(),
      tl2.x(), tl2.y(), rect2.width(), rect2.height()
    );
  };

    // Returns area of overlap between two rectangles expressed as top-left/width-height pairs
  geometry.overlapAreaWH = function(x1,y1, w1,h1, x2,y2, w2,h2) {
    var ix;
    var iy;

    if (w2>=w1) {
      if (x1<=x2) ix = (x1+w1) - x2;
      else if ((x1+w1)>=(x2+w2)) ix = (x2+w2)-x1;
      else ix = w1;
    } else {
      if (x2<=x1) ix = (x2+w2) - x1;
      else if ((x2+w2)>=(x1+w1)) ix = (x1+w1)-x2;
      else ix = w2;
    }

    if (h2>=h1) {
      if (y1<=y2) iy = (y1+h1) - y2;
      else if ((y1+h1)>=(y2+h2)) iy = (y2+h2)-y1;
      else iy = h1;
    } else {
      if (y2<=y1) iy = (y2+h2) - y1;
      else if ((y2+h2)>=(y1+h1)) iy = (y1+h1)-y2;
      else iy = h2;
    }

    if (ix<0) ix = 0;
    if (iy<0) iy = 0;

    return ix*iy;
  };

    // Returns area of overlap between two rectangles expressed as top-left/bottom-right pairs
  geometry.overlapAreaXY = function(left1, top1, right1, bott1, left2, top2, right2, bott2) {
    return geometry.overlapAreaWH(left1, top1, right1-left1, bott1-top1, left2, top2, right2-left2, bott2-top2);
  };

    // defines if line has common points with rect
    geometry.lineIntersectsRect = function(rx1, ry1, rx2, ry2, lx1, ly1, lx2, ly2) {
      var a = ly1 - ly2,
          b = lx2 - lx1,
          c = -(a * lx1 + b * ly1);

      var r = a * rx1 + b * ry1 + c;
      var sign = r === 0 ? 0 : (r > 0 ? 1 : -1);

      r = a * rx1 + b * ry2 + c;
      if ((r === 0 ? 0 : (r > 0 ? 1 : -1)) !== sign) return true;

      r = a * rx2 + b * ry1 + c;
      if ((r === 0 ? 0 : (r > 0 ? 1 : -1)) !== sign) return true;

      r = a * rx2 + b * ry2 + c;
      if ((r === 0 ? 0 : (r > 0 ? 1 : -1)) !== sign) return true;

      return false;
    };

    //Returns area of line overlap with rect. Normalizes coordinate direction
    geometry.intersectionLengthRectLineXY = function(rx1, ry1, rx2, ry2, lx1, ly1, lx2, ly2) {
        if (!geometry.lineIntersectsRect(rx1, ry1, rx2, ry2, lx1, ly1, lx2, ly2)) return 0;

        var x1, y1, x2, y2;

        if (lx1 < rx1) x1 = rx1; else if (lx1 > rx2) x1 = rx2; else x1 = lx1;
        if (ly1 < ry1) y1 = ry1; else if (ly1 > ry2) y1 = ry2; else y1 = ly1;
        if (lx2 < rx1) x2 = rx1; else if (lx2 > rx2) x2 = rx2; else x2 = lx2;
        if (ly2 < ry1) y2 = ry1; else if (ly2 > ry2) y2 = ry2; else y2 = ly2;

        return geometry.distance(x1, y1, x2, y2);
    };

    // Modifies an angle by delta value ensuring that resulting angle is always between 0 and 2pi
  geometry.wrapAngle = function(angle, delta) {
    delta = delta % pi2;
    if (delta<0) delta = pi2 + delta;
    var result = angle + delta;
    return result % pi2;
  };

    //Converts map direction to angular coordinate in radians
  geometry.mapDirectionToAngle = function(direction) {
    switch (direction) {
      case geometry.MapDirection.North: return 4/16 * pi2;
      case geometry.MapDirection.South: return 12/16 * pi2;
      case geometry.MapDirection.East:  return 0.0;
      case geometry.MapDirection.West:  return 8/16 * pi2;

      case geometry.MapDirection.NorthEast: return 2/16 * pi2;
      case geometry.MapDirection.NorthWest: return 6/16 * pi2;
      case geometry.MapDirection.SouthEast: return 14/16 * pi2;
      case geometry.MapDirection.SouthWest: return 10/16 * pi2;

      default: return 0.0;
     }
  };

    // Converts a radian angular coordinate into map direction
  geometry.angleToMapDirection = function(angle) {
    angle = geometry.wrapAngle(angle, 0);
    if ((angle >= 0.0) && (angle < pi2 * 1/16)) return geometry.MapDirection.East;
    else if ((angle >= pi2 * 1/16) && (angle < pi2 * 3/16)) return geometry.MapDirection.NorthEast;
    else if ((angle >= pi2 * 3 / 16) && (angle < pi2 * 5 / 16)) return geometry.MapDirection.North;
    else if ((angle >= pi2 * 5 / 16) && (angle < pi2 * 7 / 16)) return geometry.MapDirection.NorthWest;
    else if ((angle >= pi2 * 7 / 16) && (angle < pi2 * 9 / 16)) return geometry.MapDirection.West;
    else if ((angle >= pi2 * 9 / 16) && (angle < pi2 * 11 / 16)) return geometry.MapDirection.SouthWest;
    else if ((angle >= pi2 * 11 / 16) && (angle < pi2 * 13 / 16)) return geometry.MapDirection.South;
    else if ((angle >= pi2 * 13 / 16) && (angle < pi2 * 15 / 16)) return geometry.MapDirection.SouthEast;
    else return geometry.MapDirection.East;
  };

    // Calculates a relative area of an inner rectangle that violates outside perimeter.
    // The area is not 100% geometrically accurate - may be used for relative comparisons only
  geometry.perimeterViolationArea = function(perimeter, inner) {
    var ix = 0;
    var iy = 0;

    if (inner.left()<perimeter.left()) ix = perimeter.left() - inner.left();
    else if (inner.right()>perimeter.right()) ix = inner.right() - perimeter.right();

    if (inner.top() < perimeter.top()) iy = perimeter.top() - inner.top();
    else if (inner.bottom() > perimeter.bottom()) iy = inner.bottom() - perimeter.bottom();

    return (ix * inner.height()) + (iy * inner.width());
  };

  // Returns a point of intersection between a ray cast from the center of a rectangle
  // under certain polar coordinate angle and a rectangle side
  geometry.findRayFromRectangleCenterSideIntersection = function(rect, theta) {
    var center = new geometry.Point(rect.left() + rect.width() / 2, rect.top() + rect.height() / 2);

    //make ray "infinite" in comparison to rect
    var rayLength = rect.width() > rect.height() ? rect.width() : rect.height();

    if (rayLength < 100) rayLength = 100; //safeguard

    var rayEnd = new geometry.PolarPoint(rayLength, theta);//create ray "end" point
    var rayEndPoint = rayEnd.toPoint();

    //get line incline	aka. y = kx
    var k = (rayEndPoint.x() !== 0)? (rayEndPoint.y()) / (rayEndPoint.x()) : 0;

    var x = 0;
    var y = 0;

    var lst = [];

    //north
    x = center.x() + ((k !== 0) ? ((rect.top() - center.y()) / k) : 0);
    y = rect.top();
    if ((x >= rect.left()) && (x <= rect.right())) lst.push(new geometry.Point(x, y));

    //south
    x = center.x() + ((k !== 0) ? ((rect.bottom() - center.y()) / k) : 0);
    y = rect.bottom();
    if ((x >= rect.left()) && (x <= rect.right())) lst.push(new geometry.Point(x, y));

    //east
    x = rect.right();
    y = center.y() + k * (rect.right() - center.x());
    if ((y >= rect.top()) && (y <= rect.bottom())) lst.push(new geometry.Point(x, y));

    //west
    x = rect.left();
    y = center.y() + k * (rect.left() - center.x());
    if ((y >= rect.top()) && (y <= rect.bottom())) lst.push(new geometry.Point(x, y));

    var minPoint = new geometry.Point(1000000, 1000000);
    var minDistance = 1000000;

    var re = rayEnd.toPoint(); //rayEnd is relative to absolute 0,0
    re.offset(center.x(), center.y()); // need to make relative to rectangle center

    //find closest point
    for(var i in lst) {
      var p = lst[i];
      var dst = geometry.distancePoints(p, re);
      if (dst < minDistance) {
        minPoint = p;
        minDistance = dst;
      }
    }
    return minPoint;
  };

  // Calculates coordinates of rectangle given by width/height ('rw'/'rh')
  // which center is rotated by 'angle' (in radians) relatively to point ('cx'/'cy')
  // respecting reatangle size and 'margin'.
  // Returns coordinates of rotated rectangle (geometry.Rectangle object)
  geometry.rotateRectAroundCircle = function (cx, cy, cMargin, rw, rh, angle) {
    var halfRw = rw / 2, halfRh = rh / 2;
    var rcX = cx + Math.cos(angle) * (halfRw + cMargin);
    var rcY = cy + Math.sin(angle) * (halfRh + cMargin);
    return new geometry.toRectXY(rcX - halfRw, rcY - halfRh, rcX + halfRw, rcY + halfRh);
  };

  // Returns 2D bounding box (with  for a set of points (array of {x, y})
  geometry.getBBox = function(points) {
    if (!points) return null;
    var length = points.length;
    if (length === 0) return null;
    var minX = Number.POSITIVE_INFINITY, maxX=Number.NEGATIVE_INFINITY, minY=Number.POSITIVE_INFINITY, maxY=Number.NEGATIVE_INFINITY;
    for(var i=0; i<length; i++) {
      var p = points[i];
      var x = p.x, y = p.y;
      if(x < minX) minX = x;
      if(x > maxX) maxX = x;
      if(y < minY) minY = y;
      if(y > maxY) maxY = y;
    }
    return geometry.toRectXY(minX, minY, maxX, maxY);
  };

    // Point class represents x,y pair on a cartesian plane
  geometry.Point = function(x, y)
    {
    var fX = x;
    this.x = function(val){
            if (typeof(val)!==tUNDEFINED) fX = val;
            return fX;
        };

    var fY = y;
    this.y = function(val){
            if (typeof(val)!==tUNDEFINED) fY = val;
            return fY;
        };

    // Changes point coordinates
    this.offset = function(dx, dy) { fX += dx; fY += dy; };

  }; // Point


    // Returns point as polar point relative to the specified center
  geometry.Point.prototype.toPolarPoint = function(center) {
      var dist = geometry.distancePoints(center, this);
      var angle = Math.atan2(this.y() - center.y(), this.x() - center.x());

      if (angle < 0)	angle = pi2 + angle;

      return new geometry.PolarPoint(dist, angle);
  };


    //Determines whether the two points contain equivalent coordinates
    geometry.Point.prototype.isEqual = function(other){
      return (this.x() === other.x()) && (this.y() === other.y());
    };

    geometry.Point.prototype.toString = function()
    {
      return "(" + this.x().toString() + " , " + this.y().toString() + ")";
    };


    // Polar Point class represents point with polar coordinates
    geometry.PolarPoint = function(radius, theta)
    {
      var fRadius = radius;
      this.radius = function(val){
            if (typeof(val)!==tUNDEFINED) fRadius = val;
            return fRadius;
      };

      var fTheta = checkangle(theta);
      this.theta = function(val){
            if (typeof(val)!==tUNDEFINED) fTheta = checkangle(val);
            return fTheta;
      };

      //Returns polar point as simple x,y point
      this.toPoint = function(){
          var x = fRadius * Math.cos(fTheta);
          var y = fRadius * Math.sin(fTheta);
          return new geometry.Point(x, y);
      };

      function checkangle(angle){
       if ((angle < 0) || (angle > pi2))
            throw "Invalid polar coordinates angle";
       return angle;
      }


    };//PolarPoint

    geometry.PolarPoint.prototype.isEqual = function(other){
        return this.radius() === other.radius() && this.theta() === other.theta();
    };

    geometry.PolarPoint.prototype.toString = function(){
      return "(" + this.radius().toString() + " , " + geometry.radToDeg(this.theta()).toString() + "°)";
    };

    // Represents a rectangle
    geometry.Rectangle = function(corner1, corner2)
    {
        var self = this;

        var fCorner1 = corner1;
        var fCorner2 = corner2;

        this.corner1 = function(val){
            if (typeof(val)!==tUNDEFINED) fCorner1 = val;
            return fCorner1;
        };

        this.corner2 = function(val){
            if (typeof(val)!==tUNDEFINED) fCorner2 = val;
            return fCorner2;
        };

        // Returns top left corner point per natural axis orientation when x increases from left to right, and y increases from top to bottom
        this.topLeft = function(){
            var lx = fCorner1.x();
            var other = fCorner2.x();

            if (other < lx) lx = other;

            var ty = fCorner1.y();
            other = fCorner2.y();

            if (other < ty) ty = other;
            return new geometry.Point(lx, ty);
         };

       // Returns bottom right corner point per natural axis orientation when x increases from left to right, and y increases from top to bottom
       this.bottomRight = function(){
            var rx = fCorner1.x();
            var other = fCorner2.x();

            if (other > rx) rx = other;

            var by = fCorner1.y();
            other = fCorner2.y();

            if (other > by) by = other;

            return new geometry.Point(rx, by);
       };

       // Return rectangle width
       this.width = function(){  return Math.abs(fCorner1.x() - fCorner2.x()); };

       // Return rectangle height
       this.height = function(){  return Math.abs(fCorner1.y() - fCorner2.y()); };

       // Returns left-most edge coordinate
       this.left = function() {
         var lx = fCorner1.x();
         var other = fCorner2.x();

         if (other < lx) lx = other;

         return lx;
       };

       // Returns right-most edge coordinate
       this.right = function(){
         var rx = fCorner1.x();
         var other = fCorner2.x();

         if (other > rx) rx = other;

         return rx;
       };

       // Returns top-most edge coordinate
       this.top = function(){
         var ty = fCorner1.y();
         var other = fCorner2.y();

         if (other < ty) ty = other;

         return ty;
       };

       // Returns bottom-most edge coordinate
       this.bottom = function(){
         var by = fCorner1.y();
         var other = fCorner2.y();

         if (other > by) by = other;

         return by;
       };

       // Returns the center point
       this.centerPoint = function(){
         return new geometry.Point(self.left() + self.width() / 2, self.top() + self.height() / 2);
       };

       // Returns rectangle square
       this.square = function () { return self.width() * self.height(); };

       // Tests if point lies within this reactangle
       this.contains = function (point) {
         var topLeft = self.topLeft(), bottomRight = self.bottomRight();
         var px = point.x(), py = point.y();
         return px >= topLeft.x() && px <= bottomRight.x() && py >= topLeft.y() && py <= bottomRight.y();
       };

    };//Rectangle class

    geometry.Rectangle.prototype.isEqual = function(other){
        return this.left() === other.left() && this.top() === other.top() && this.width() === other.width() && this.height() === other.height();
    };

    geometry.Rectangle.prototype.toString = function(){
      return "(" + this.left().toString() + "," + this.top().toString() + " ; "+ this.width().toString() + "x" + this.height().toString()+")";
    };


    /**
     * Calculates callout balloon vertices suitable for contour curve drawing
     *
     * @param {Rectangle} body Balloon body coordinates
     * @param {Point} target Balloon leg attachment point
     * @param {double} legSweep Length of balloon leg attachment breach at balloon body edge, expressed in radians (arc length). A value such as PI/16 yields good results
     * @returns {array} An array of vertex points
     */
    geometry.vectorizeBalloon = function(body, target, legSweep)
    {
      var result = [];

      var center = new geometry.Point(body.left() + body.width() / 2, body.top() + body.height() / 2);
      var trg = target.toPolarPoint(center);

      var legStart = geometry.findRayFromRectangleCenterSideIntersection(
                                 body,
                                 geometry.wrapAngle(trg.theta(), -legSweep / 2));

      var legEnd = geometry.findRayFromRectangleCenterSideIntersection(
                                 body,
                                 geometry.wrapAngle(trg.theta(), +legSweep / 2));

      //build vertexes
      result.push(new geometry.Point(body.left(), body.top()));

      result.push(new geometry.Point(body.right(), body.top()));

      result.push(new geometry.Point(body.right(), body.bottom()));

      result.push(new geometry.Point(body.left(), body.bottom()));

      result.push(legStart);
      result.push(target);
      result.push(legEnd);

      //reorder points by azimuth so the curve can close and look like a balloon
      result.sort(function(p1, p2)
                      {
                        var pp1 = p1.toPolarPoint(center);
                        var pp2 = p2.toPolarPoint(center);

                        if (pp1.theta() > pp2.theta())
                          return -1;
                        else
                          return +1;
                      }
                 );

      return result;
    };


    published.Geometry = geometry;