#
# Copyright (c) 2003 Art Haas
#
# This file is part of PythonCAD.
#
# PythonCAD is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# PythonCAD is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with PythonCAD; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
#
#
# class stuff for ellipses
#
# Ellipse info:
# http://mathworld.wolfram.com/Ellipse.html
# http://astronomy.swin.edu.au/~pbourke/geometry/ellipsecirc/
#

import math

import baseobject
import tolerance
import point
import graphicobject
import style
import linetype
import color
import util

class Ellipse(graphicobject.GraphicObject):
    """A base class for Ellipses

An ellipse has the following attributes:

center: A _point object
major_axis:
minor_axis:
angle: A float from -90.0 to 90.0 degrees
    """

    messages = {
        'center_changed' : True,
        'major_axis_changed' : True,
        'minor_axis_changed' : True,
        'angle_changed' : True,
        'moved' : True,
        }

    #
    # The default style for the Ellipse class
    #

    __defstyle = style.Style(u'Default Ellipse Style',
                            linetype.Linetype(u'Solid', None),
                            color.Color(0xffffff),
                            1.0)

    def __init__(self, center, major, minor, angle,
                 st=None, lt=None, col=None, t=None, **kw):
        _cp = center
        if not isinstance(_cp, point._Point):
            _cp = point.Point(center)
        _major = major
        if not isinstance(_major, float):
            _major = float(major)
        if not _major > 0.0:
            raise ValueError, "Invalid major axis value: %g" % _major
        _minor = minor
        if not isinstance(_minor, float):
            _minor = float(minor)
        if not _minor > 0.0:
            raise ValueError, "Invalid minor axis value: %g" % _minor
        if _minor > _major:
            raise ValueError, "Minor axis must be less than major axis"
        _angle = util.make_angle(_angle)
        _st = st
        if _st is None:
            _st = Ellipse.__defstyle
        graphicobject.GraphicObject.__init__(self, st, lt, col, t, **kw)
        self.__center = _cp
        self.__major = _major
        self.__minor = _minor
        self.__angle = _angle
        _cp.storeUser(self)
        _cp.connect('moved', self, Ellipse.movePoint)
            
    def __eq__(self, obj):
        """Compare one ellipse to another for equality.
        """
        _val = False
        if isinstance(obj, Ellipse):
            if (self.__center == obj.getCenter() and
                abs(self.__major - obj.getMajorAxis()) < 1e-10 and
                abs(self.__minor - obj.getMinorAxis()) < 1e-10 and
                abs(self.__angle - obj.getAngle()) < 1e-10):
                _val = True
        return _val

    def __ne__(self, obj):
        """Compare one ellipse to another for equality.
        """
        _val = True
        if isinstance(obj, Ellipse):
            if (self.__center == obj.getCenter() and
                abs(self.__major - obj.getMajorAxis()) < 1e-10 and
                abs(self.__minor - obj.getMinorAxis()) < 1e-10 and
                abs(self.__angle - obj.getAngle()) < 1e-10):
                _val = False
        return _val


    def getCenter(self):
        """Return the center _Point of the Ellipse.

getCenter()
        """
        return self.__center

    def setCenter(self, cp):
        """Set the center _Point of the Ellipse.

setCenter(cp)

The argument must be a _Point or a tuple containing
two float values.
        """
        if self.isLocked():
            raise RuntimeError, "Setting center not allowed - object locked."
        if not isinstance(cp, point._Point):
            raise TypeError, "Invalid Point: " + `cp`
        _c = self.__center
        if _c is not cp:
            _c.disconnect(self)
            _c.freeUser(self)
            self.__center = cp
            self.sendMessage('center_changed', _c)
            cp.storeUser(self)
            cp.connect('moved', self, ellipse.movePoint)
            if abs(_c.x - cp.x) > 1e-10 or abs(_c.y - cp.y) > 1e-10:
                _x, _y = _c.getCoords()
                self.sendMessage('moved', _x, _y, self.__major, self.__minor,
                                 self.__angle)
            self.modified()

    center = property(getCenter, setCenter, None, "Ellipse center")

    def getMajorAxis(self):
        """Return the major axis value of the Ellipse.

getMajorAxis()

This method returns a float.
        """
        return self.__major

    def setMajorAxis(self, val):
        """Set the major axis of the Ellipse.

setMajorAxis(val)

Argument 'val' must be a float value greater than 0.
        """
        if self.isLocked():
            raise RuntimeError, "Setting major axis not allowed - object locked."
        _val = val
        if not isinstance(_val, float):
            _val = float(val)
        if not _val > 0.0:
            raise ValueError, "Invalid major axis value: %g" % _val
        if _val < self.__minor:
            raise ValueError, "Major axis must be greater than minor axis."
        _maj = self.__major
        if abs(_val - _maj) > 1e-10:
            self.__major = _val
            self.sendMessage('major_axis_changed', _maj)
            _x, _y = self.__center.getCoords()
            self.sendMessage('moved', _x, _y, _maj, self.__minor, self.__angle)
            self.modified()

    major_axis = property(getMajorAxis, setMajorAxis, None,
                          "Ellipse major axis")

    def getMinorAxis(self):
        """Return the minor axis value of the Ellipse.

getMinorAxis()

This method returns a float.
        """
        return self.__minor

    def setMinorAxis(self, val):
        """Set the minor axis of the Ellipse.

setMinorAxis(val)

Argument 'val' must be a float value greater than 0.
        """
        if self.isModified():
            raise RuntimeError, "Setting minor axis not allowed - object locked."
        _val = val
        if not isinstance(_val, float):
            _val = float(val)
        if not _val > 0.0:
            raise ValueError, "Invalid minor axis value: %g" % _val
        if _val > self.__major:
            raise ValueError, "Minor axis must be less than major axis"
        _min = self.__minor
        if abs(_val - _min) > 1e-10:
            self.__minor = _val
            self.sendMessage('minor_axis_changed', _min)
            _x, _y = self.__center.getCoords()
            self.sendMessage('moved', _x, _y, self.__major, _min, self.__angle)
            self.modified()

    minor_axis = property(getMinorAxis, setMinorAxis, None,
                          "Ellipse minor axis")

    def getAngle(self):
        """Return the Ellipse major axis angle.

getAngle()

This method returns a float value.
        """
        return self.__angle

    def setAngle(self, angle):
        """Set the Ellipse major axis angle.

setAngle(angle)

Argument 'angle' should be a float. The value will be
adjusted so the angle will be defined from 90.0 to -90.0.
        """
        if self.isModified():
            raise RuntimeError, "Setting angle not allowed - object locked."
        _angle = angle
        if not isinstance(_angle, float):
            _angle = float(angle)
        _angle = util.make_angle(_angle)
        _a = self.__angle
        if abs(_a - _angle) > 1e-10:
            self.__angle = _angle
            self.sendMessage('angle_changed', _a)
            _x, _y = self.__center.getCoords()
            self.sendMessage('moved', _x, _y, self.__major, self.__minor,
                             _angle)
            self.modified()

    angle = property(getAngle, setAngle, None, "Ellipse major axis angle")

    def move(self, dx, dy):
        """Move an Ellipse.

move(dx, dy)

Arguments 'dx' and 'dy' should be floats.
        """
        if self.isLocked() or self.__center.isLocked():
            raise RuntimeError, "Moving object not allowed - object locked."
        _dx = dx
        if not isinstance(_dx, float):
            _dx = float(dx)
        _dy = dy
        if not isinstance(_dy, float):
            _dy = float(dy)
        if abs(_dx) > 1e-10 or abs(_dy) > 1e-10:
            _x, _y = self.__center.getCoords()
            self.__center.move(_dx, _dy)
            self.sendMessage('moved', _x, _y, self.__major, self.__minor,
                             self.__angle)
            self.modified()

    def rotate(self, angle):
        """Rotate an Ellipse

rotate(angle)

Argument 'angle' should be a float.
        """
        if self.isLocked():
            raise RuntimeError, "Rotating object not allowed - object locked."
        _angle = angle
        if not isinstance(_angle, float):
            _angle = float(_angle)
        if abs(_angle) < 1e-10:
            return
        _cur = self.__angle
        _new = util.make_angle(_angle + _cur)
        self.__angle = _new
        self.sendMessage('angle_changed', _cur)
        _x, _y = self.__center.getCoords()
        self.sendMessage('moved', _x, _y, self.__major, self.__minor, _cur)
        self.modified()

    def eccentricity(self):
        """Return the eccecntricity of the Ellipse.

eccentricity()

This method returns a float value.
        """
        _major = self.__major
        _minor = self.__minor
        if abs(_major - _minor) < 1e-10: # circular
            _e = 0.0
        else:
            _e = math.sqrt(1.0 - ((_minor * _minor)/(_major * _major)))
        return _e

    def area(self):
        """Return the area of the Ellipse.

area()

This method returns a float value.
        """
        return math.pi * self.__major * self.__minor

    def circumference(self):
        """Return the circumference of an ellipse.

circumference()

This method returns a float.

The algorithm below is taken from
http://astronomy.swin.edu.au/~pbourke/geometry/ellipsecirc/
Ramanujan, Second Approximation
        """
        _a = self.__major
        _b = self.__minor
        _h = math.pow((_a - _b), 2)/math.pow((_a + _b), 2)
        _3h = 3.0 * _h
        return math.pi * (_a + _b) * (1.0 + _3h/(10.0 + math.sqrt(4.0 - _3h)))

    def mapCoords(self, x, y, tol=tolerance.TOL):
        """Return the nearest _Point on the Ellipse to a coordinate pair.

mapCoords(x, y[, tol])

The function has two required arguments:

x: A Float value giving the "x" coordinate
y: A Float value giving the "y" coordinate

There is a single optional argument:

tol: A float value equal or greater than 0.0

This function is used to map a possibly near-by coordinate pair to
an actual _Point on the Ellipse. If the distance between the actual
_Point and the coordinates used as an argument is less than the tolerance,
the actual _Point is returned. Otherwise, this function returns None.
        """
        _x = x
        if not isinstance(_x, float):
            _x = float(x)
        _y = y
        if not isinstance(_y, float):
            _y = float(y)
        _t = tolerance.toltest(tol)
        _cx, _cy = self.__center.getCoords()
        _dist = math.hypot((_x - _cx), (_y - _cy))
        _major = self.__major
        _minor = self.__minor
        _ep = None
        if abs(_major - _minor) < 1e-10: # circular
            _sep = _dist - _major
            if _sep < _t or abs(_sep - _t) < 1e-10:
                _angle = math.atan2((_y - _cy),(_x - _cx))
                _px = _major * math.cos(_angle)
                _py = _major * math.sin(_angle)
                _ep = point._Point((_cx + _px), (_cy + _py))
        else:
            if _dist < _major and _dist > _minor:
                _ecos = math.cos(self.__angle)
                _esin = math.sin(self.__angle)
                # FIXME ...
        return _ep

    def movePoint(self, p, *args):
        _alen = len(args)
        if _alen < 2:
            raise ValueError, "Invalid argument count: %d" % _alen
        _x = args[0]
        if not isinstance(_x, float):
            _x = float(args[0])
        _y = args[1]
        if not isinstance(_y, float):
            _y = float(args[1])
        if p is not self.__center:
            raise ValueError, "Unexpected point in movePoint" + `p`
        self.sendMessage('moved', _x, _y, self.__major, self.__minor,
                         self.__angle)
        self.modified()
    
#
# measure r from focus
#
# x = c + r*cos(theta)
# y = r*sin(theta)
#
# c = sqrt(a^2 - b^2)
#
# r = a*(1-e)/(1 + e*cos(theta))

    def clone(self):
        """Make a copy of an Ellipse.

clone()

This method returns a new Ellipse object
        """
        _cp = self.__center.clone()
        _st = self.getStyle()
        _lt = self.getLinetype()
        _col = self.getColor()
        _th = self.getThickness()
        return Ellipse(_cp, self.__major, self.__minor, self.__angle,
                       _st, _lt, _col, _th)

    def sendsMessage(self, m):
        if m in Ellipse.messages:
            return True
        return graphicobject.GraphicObject.sendsMessage(self, m)
