# Filename: vec2d_jdm.py
# Author: James D. Miller; Gustavus Adolphus College.

# This Vec2D class is based on the vec2d class obtained here:
# http://karpathy.ca/phyces/tutorial1.php

import math

def almostEqual(a,b):
    eps = 10*-6
    return abs(a - b) <= eps

class Vec2D:
    # Components of the vector can be input as individual arguments or as
    # a tuple pair in one argument.
    def __init__(self, x_or_pair, y = None, int_flag = "not_int"):
        if y == None:
            self.x = x_or_pair[0]
            self.y = x_or_pair[1]
        else:
            self.x = x_or_pair
            self.y = y

        if int_flag == "int":
            self.x = int(round(self.x))
            self.y = int(round(self.y))
        else:
            self.x = float(self.x)
            self.y = float(self.y)
        
    # This establishes the format of the output string if you print a vector (an object instance).
    def __str__(self):
        return 'Vec2D(%s, %s)' % (self.x, self.y)

    #============================================================================
    
    # This section defines methods for vector arithmetic and comparison. 
    # Operator overloading is established here.
    
    # Addition
    def add_vector(self, vec_B):
        return Vec2D(self.x + vec_B.x, self.y + vec_B.y)
    def __add__(self, vec_B):
        return Vec2D(self.x + vec_B.x, self.y + vec_B.y)
    
    # Subtraction
    def sub_vector(self, vec_B):
        return Vec2D(self.x - vec_B.x, self.y - vec_B.y)
    def __sub__(self, vec_B):
        return Vec2D(self.x - vec_B.x, self.y - vec_B.y)

    def __eq__(self,other):
        if not isinstance(other,Vec2D): return False
        return almostEqual(self.x,other.x) and almostEqual(self.y,other.y)
    
    # Scaling operations. Note that the operator overloading cases
    # must have the vector preceding the scaling factor!
    # e.g.  vector * 1.2   or   vector / 3.2,  but not    1.2 * vector
    def scale_vector(self, scale_factor):
        return Vec2D( self.x * scale_factor, self.y * scale_factor)
    def __mul__(self, other):
        if isinstance(other,Vec2D):
            result = Vec2D(1,0)
            result = result.set_magnitude(self.length() * other.length())
            result = result.set_angle(self.get_angle() + other.get_angle())
            return result
        else: return self.scale_vector(other)
    def __truediv__(self, scale_factor):
        return Vec2D( self.x / scale_factor, self.y / scale_factor)
    def __repr__(self):
        return "(%d,%d)" % (self.x,self.y)
    
    # Comparisons.
    def equal(self, vec_B):
        return (self.x == vec_B.x) and (self.y == vec_B.y)
    def not_equal(self, vec_B):
        return (self.x != vec_B.x) or (self.y != vec_B.y)
        
    #============================================================================
  
    # Determine the scaler length of the vector. This uses the
    # squareroot operation.
    def length(self):
        return (self.x*self.x + self.y*self.y)**0.5
    
    # Length squared can sometimes be used in substitution for the scaler length
    # of a vector. Length squared is much faster to calculate since there is
    # no squareroot operation.
    def length_squared(self):
        return (self.x*self.x + self.y*self.y)
    
    # Returns a vector in the same direction as the
    # original but with a length of 1 (unit length).
    def normal(self):
        # I modified this so it doesn't give errors...
        if self.length_squared() == 0: return self
        else: return self / self.length()
    
    # Returns a vector in the same direction as the
    # original but with a length equal to the target magnitude.
    def set_magnitude(self, target_magnitude):
        if self.length_squared() != 0:
            return self.normal() * target_magnitude
        else: return Vec2D(0,target_magnitude)
    
    # Dot product with another vector.
    def dot(self, vec_B):
        return (self.x * vec_B.x) + (self.y * vec_B.y)
    
    # The vector component of the self vector along the direction of the B vector.
    # (Refer to the drawings in the PDF.)
    def projection_onto(self, vec_B):
        vB_dot_vB = vec_B.dot(vec_B)
        if (vB_dot_vB > 0):
            return vec_B * ( self.dot(vec_B) / vB_dot_vB )        
        else:
            # If vec_B has zero magnitude, return a zero magnitude vector. In this 
            # case, the dot product of the two vectors will be zero. This leaves the 
            # projection undetermined; vec_B * (0/0). It would be appropriate to 
            # return a None value here! But the zero magnitude vector is handy for 
            # dealing with the spring-anchored pucks (because the separation distance 
            # between the puck and the pinning point eventually goes to zero as the 
            # disturbed puck loses energy). So again, it would be better to return a 
            # None here and catch this in the code following the projection. 
            return self * 0
    
    # Rotate 90 degrees counterclockwise.
    def rotate90(self):
        return Vec2D(-self.y, self.x)
    
    # Flip it around (reverse the direction of the vector).
    def rotate180(self):
        return Vec2D(-self.x, -self.y)
    
    # Rotate (change the direction of) the original vector by a specified number of degrees.
    # (Original vector rotated by angle_degrees.)
    def rotated(self, angle_degrees, sameVector=False):
        angle_radians = math.radians(angle_degrees)
        cos = math.cos(angle_radians)
        sin = math.sin(angle_radians)
        # The rotation transformation.
        x = self.x * cos - self.y * sin
        y = self.x * sin + self.y * cos
        if sameVector:
            self.x = x
            self.y = y
        else:
            return Vec2D(x, y)
        
    # Set the direction of the vector to a specific angle.
    def set_angle(self, angle_degrees):
        self.x = self.length()
        self.y = 0
        return self.rotated(angle_degrees)
    
    # Determine the angle that this vector makes with the x axis. Measure
    # counterclockwise from the x axis.
    def get_angle(self):
        if (self.length_squared() == 0):
            return 0
        return math.degrees(math.atan2(self.y, self.x))
    
    # Determine the angle between two vectors.
    def get_angle_between(self, vec_B):
        cross = self.x*vec_B.y - self.y*vec_B.x    #= ABsin(theta)
        dot   = self.x*vec_B.x + self.y*vec_B.y    #= ABcos(theta)       
        # Use the two parameter input (y,x) form of the arctan. This is safer than
        # taking the arctan of the cross/dot which can be zero in the denominator.
        return math.degrees(math.atan2(cross, dot))  
        
    # Return a tuple containing the two components of the vector.
    def tuple(self):
        return (self.x, self.y)

    # Return a reflection with the other as the reflecting surface
    # By Matt Zhao
    def reflect(self,other):
        para = self.projection_onto(other)
        perp = self - para
        return para - perp
    