# tuple3d.py
# Walker M. White (wmw2)
# August 20, 2013
"""Module that provides tuple objects for 3D geometry.

This module provides classes for tuples, points, vectors.  The
tuples are the base class, while points and vectors are subclasses."""
import numpy
import math
import copy

class Tuple3D(object):
    """An instance is a tuple in 3D space.  This serves as the base
    class for both Point and Vector."""
    
    @property
    def x(self):
        """The x coordinate
        
        **Invariant**: Value must be a float. If assigned an int, it
        will be typecast to a float (possibly raising a TypeError)."""
        return self._x
    
    @x.setter
    def x(self, value):
        self._x = float(value)
    
    @x.deleter
    def x(self):
        del self._x 
    
    @property
    def y(self):
        """The y coordinate
        
        **Invariant**: Value must be a float. If assigned an int, it
        will be typecast to a float (possibly raising a TypeError)."""
        return self._y
    
    @y.setter
    def y(self, value):
        self._y = float(value)
    
    @y.deleter
    def y(self):
        del self._y     
    
    @property
    def z(self):
        """The z coordinate
        
        **Invariant**: Value must be a float. If assigned an int, it
        will be typecast to a float (possibly raising a TypeError)."""
        return self._z
    
    @z.setter
    def z(self, value):
        self._z = float(value)
    
    @z.deleter
    def z(self):
        del self._z     
    
    # METHODS
    def __init__(self, x=0, y=0, z=0):
        """**Constructor**: creates a new Tuple3D value (x,y,z).
        
            :param x: initial x value
            **Precondition**: value is an int or float.
        
            :param y: initial y value
            **Precondition**: value is an int or float.
        
            :param z: initial z value
            **Precondition**: value is an int or float.
        
        All values are 0.0 by default.        
        """
        self.x = x
        self.y = y
        self.z = z
    
    def __eq__(self, other):
        """**Returns**: True if self and other are equivalent Tuple3Ds. 
        
        This method uses numpy to test whether the coordinates are 
        "close enough".  It does not require exact equality for floats.
        
            :param other: value to compare against
        """        
        return (type(other) == Tuple3D and numpy.allclose(self.list(),other.list()))
    
    def __ne__(self, other):
        """**Returns**: True if self and other are not equivalent Tuple3Ds. 
        
            :param other: value to compare against
        """
        return not self == other
    
    def __str__(self):
        """**Returns**: Readable String representation of this Tuple3D. """
        return "("+str(self.x)+","+str(self.y)+","+str(self.z)+")"
    
    def __repr__(self):
        """**Returns**: Unambiguous String representation of this Tuple3D. """
        return "%s%s" % (self.__class__,self.__str__())
    
    def list(self):
        """**Returns**: A python list with the contents of this Tuple3D."""
        return [self.x,self.y,self.z]
    
    def abs(self): 
        """Sets each component of this Tuple3D to its absolute value."""
        self.x = abs(self.x)
        self.y = abs(self.y)
        self.z = abs(self.z)
    
    def clamp(self,low,high): 
          """Clamps this tuple to the range [low, high].
          
          Any value in this Vector less than low is set to low.  Any
          value greater than high is set to high."""
          self.x = max(low,min(high,self.x))
          self.y = max(low,min(high,self.y))
          self.z = max(low,min(high,self.z))
    
    def __add__(self, other):
        """**Returns**: the sum of self and other.
        
        The value returned has the same type as self (so it is either
        a Tuple3D or is a subclass of Tuple3D).  The contents of this object
        are not altered.
        
            :param other: tuple value to add
            **Precondition**: value has the same type as self.
        """
        assert (type(other) == type(self)), "value %(value)s is not a of type %(type)s" % {'value': `other`, 'type':`type(self)`}
        result = copy.copy(self)
        result.x += other.x
        result.y += other.y
        result.z += other.z
        return result
    
    def __rmul__(self, scalar):
        """**Returns**: the scalar multiple of self and other.
        
        The value returned is a new Tuple3D.  The contents of this Tuple3D
        are not altered.
        
            :param scalar: scalar to multiply by
            **Precondition**: value is an int or float.
        """
        assert (type(scalar) in [int,float]), "value %s is not a number" % `scalar`
        result = copy.copy(self)
        result.x *= scalar
        result.y *= scalar
        result.z *= scalar
        return result
    
    def interpolate(self, other, alpha):
        """**Returns**: the interpolation of self and other via alpha.

        The value returned has the same type as self (so it is either
        a Tuple3D or is a subclass of Tuple3D).  The contents of this object
        are not altered. The resulting value is 
        
            alpha*self+(1-alpha)*other 
        
        according to Tuple3D addition and scalar multiplication.
        
            :param other: tuple value to interpolate with
            **Precondition**: value has the same type as self.

            :param alpha: scalar to interpolate by
            **Precondition**: value is an int or float.
        """
        assert (type(other) == type(self)), "value %(value)s is not a of type %(type)s" % {'value': `other`, 'type':`type(self)`}
        assert (type(alpha) in [int,float]), "value %s is not a number" % `alpha`
        return alpha*self+(1-alpha)*other


class Point(Tuple3D):
    """An instance is a point in 3D space"""
    
    # METHODS
    def __init__(self, x=0, y=0, z=0):
        """**Constructor**: creates a new Point value (x,y,z).
        
            :param x: initial x value
            **Precondition**: value is an int or float.
        
            :param y: initial y value
            **Precondition**: value is an int or float.
        
            :param z: initial z value
            **Precondition**: value is an int or float.
        
        All values are 0.0 by default.        
        """
        super(Point,self).__init__(x,y,z)
    
    def __eq__(self, other):
        """**Returns**: True if self and other are equivalent Points. 
        
        This method uses numpy to test whether the coordinates are 
        "close enough".  It does not require exact equality for floats.
        
            :param other: value to compare against
        """        
        return (type(other) == Point and numpy.allclose(self.list(),other.list()))
    
    def distanceTo(self, other):
        """**Returns**: the Euclidean distance from this point to other
        
            :param other: value to compare against
            **Precondition**: value is a Tuple3D object.
        """
        return math.sqrt((self.x-other.x)*(self.x-other.x)+
                         (self.y-other.y)*(self.y-other.y)+
                         (self.z-other.z)*(self.z-other.z))
    
    def __sub__(self, tail):
        """**Returns**: the Vector from tail to self.
        
        The value returned is a Vector with this point at its head.
        
            :param tail: the tail value for the new Vector
            **Precondition**: value is a Point object.
        """
        assert (type(tail) == Point), "value %s is not a Point" % `tail`
        return Vector(self.x-tail.x,self.y-tail.y,self.z-tail.z)


class Vector(Tuple3D):
    """An instance is a Vector in 3D space"""
    
    # METHODS
    def __init__(self, x=0, y=0, z=0):
        """**Constructor**: creates a new Vector object (x,y,z).
        
            :param x: initial x value
            **Precondition**: value is an int or float.
        
            :param y: initial y value
            **Precondition**: value is an int or float.
        
            :param z: initial z value
            **Precondition**: value is an int or float.
        
        All values are 0.0 by default.        
        """
        super(Vector,self).__init__(x,y,z)
    
    def __eq__(self, other):
        """**Returns**: True if self and other are equivalent Vectors. 
        
        This method uses numpy to test whether the coordinates are 
        "close enough".  It does not require exact equality for floats.
        
            :param other: value to compare against
        """        
        return (type(other) == Vector and numpy.allclose(self.list(),other.list()))
    
    def __str__(self):
        """**Returns**: Readable String representation of this Vector. """
        return "<"+str(self.x)+","+str(self.y)+","+str(self.z)+">"
    
    def length(self):
        """**Returns**: the length of this Vector."""
        return math.sqrt(self.x*self.x+self.y*self.y+self.z*self.z)
    
    def __sub__(self, other):
        """**Returns**: the difference between this Vector and other.
        
        The value returned is a new Vector.  The contents of this vector are not
        modified.
        
            :param other: the Vector to subtract
            **Precondition**: value is a Vector object.
        """
        assert (type(other) == Vector), "value %s is not a Vector" % `other`
        return Vector(self.x-other.x,self.y-other.y,self.z-other.z)
    
    def angle(self,other):
        """**Returns**: the angle between this vector and other.
        
        The answer provided is in radians. Neither this Vector nor
        other may be the zero vector.
        
            :param other: value to compare against
            **Precondition**: value a nonzero Vector object.
        
        """
        assert (type(other) == Vector), "value %s is not a Vector" % `other`

        na = self.length()
        nb = other.length()
        assert (na != 0), "Vector %s is zero" % `self`
        assert (nb != 0), "Vector %s is zero" % `other`
        
        return math.acos(self.dot(other)/(na*nb))
    
    def cross(self,other):
        """**Returns**: the cross product between self and other.
        
        The result of this method is a new Vector
        
            :param other: value to cross
            **Precondition**: value a Vector object.
        """
        assert (type(other) == Vector), "value %s is not a Vector" % `other`
        return Vector(self.y*other.z-self.z*other.y,
                      self.z*other.x-self.z*other.z,
                      self.x*other.y-self.y*other.x)
    
    def dot(self,other):
        """**Returns**: the dot product between self and other.
        
        The result of this method is a float.

            :param other: value to dot
            **Precondition**: value a Vector object.
        """
        assert (type(other) == Vector), "value %s is not a Vector" % `other`
        return (self.x*other.x+self.y*other.y+self.z*other.z)
    
    def normalize(self):
        """Normalizes this Vector in place.
        
        This method alters the Vector so that it has the same direction, 
        but its length is now 1.
        """
        length = self.length()
        self.x /= length
        self.y /= length
        self.z /= length
