/*
 * Copyright (C) 2018 Duy Tran Le
 *
 * This program 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 3 of the License, or
 * (at your option) any later version.
 *
 * This program 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 this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 */

/* file : CircleArc2D.java
 * 
 * Project : geometry
 *
 * ===========================================
 * 
 * This library is free software; you can redistribute it and/or modify it 
 * under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 2.1 of the License, or (at
 * your option) any later version.
 *
 * This library 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 Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this library. if not, write to :
 * The Free Software Foundation, Inc., 59 Temple Place, Suite 330,
 * Boston, MA 02111-1307, USA.
 * 
 * Created on 29 avr. 2006
 *
 */

package com.duy.ncalc.geom2d.conic;

import android.graphics.Path;

import com.duy.ncalc.geom2d.util.Angle2D;
import com.duy.ncalc.geom2d.GeometricObject2D;
import com.duy.ncalc.geom2d.Point2D;
import com.duy.ncalc.geom2d.util.Shape2D;
import com.duy.ncalc.geom2d.Vector2D;
import com.duy.ncalc.geom2d.curve.AbstractSmoothCurve2D;
import com.duy.ncalc.geom2d.line.LineSegment2D;
import com.duy.ncalc.geom2d.line.LinearShape2D;
import com.duy.ncalc.geom2d.line.Ray2D;
import com.duy.ncalc.geom2d.line.StraightLine2D;
import com.duy.ncalc.geom2d.polygon.LinearCurve2D;
import com.duy.ncalc.geom2d.polygon.Polyline2D;
import com.duy.ncalc.geom2d.util.EqualUtils;

import java.util.Collection;
import java.util.Locale;

import static java.lang.Math.PI;
import static java.lang.Math.abs;
import static java.lang.Math.ceil;
import static java.lang.Math.cos;
import static java.lang.Math.max;
import static java.lang.Math.min;
import static java.lang.Math.sin;


/**
 * A circle arc, defined by the center and the radius of the containing circle,
 * by a starting angle, and by a (signed) angle extent.
 * <p>
 * A circle arc is directed: if angle extent is positive, the arc is counter
 * clockwise. Otherwise, it is clockwise.
 * <p>
 * A circle arc is parameterized using angle from center. The arc contains all
 * points with a parametric equation of t, for each t between 0 and the angle
 * extent.
 *
 * @author dlegland
 */
public class CircleArc2D extends AbstractSmoothCurve2D
        implements  CircularShape2D, Cloneable {

    // ====================================================================
    // static factories

    /**
     * The supporting circle
     */
    protected Circle2D circle;
    /**
     * The starting position on circle, in radians between 0 and +2PI
     */
    protected double startAngle = 0;
    /**
     * The signed angle extent, in radians between -2PI and +2PI.
     */
    protected double angleExtent = PI;

    /**
     * Create a circle arc whose support circle is centered on (0,0) and has a
     * radius equal to 1. Start angle is 0, and angle extent is PI/2.
     */
    public CircleArc2D() {
        this(0, 0, 1, 0, PI / 2);
    }


    // ====================================================================
    // Class variables

    /**
     * create a new circle arc based on an already existing circle.
     */
    public CircleArc2D(Circle2D circle, double startAngle, double angleExtent) {
        this(circle.xc, circle.yc, circle.r, startAngle, angleExtent);
    }

    /**
     * create a new circle arc based on an already existing circle, specifying
     * if arc is direct or not.
     */
    public CircleArc2D(Circle2D circle, double startAngle, double endAngle,
                       boolean direct) {
        this(circle.xc, circle.yc, circle.r, startAngle, endAngle, direct);
    }

    /**
     * Create a new circle arc with specified point center and radius
     */
    public CircleArc2D(Point2D center, double radius, double startAngle,
                       double angleExtent) {
        this(center.x(), center.y(), radius, startAngle, angleExtent);
    }


    // ====================================================================
    // constructors

    /**
     * Create a new circle arc with specified point center and radius, start and
     * end angles, and by specifying whether arc is direct or not.
     */
    public CircleArc2D(Point2D center, double radius, double start, double end,
                       boolean direct) {
        this(center.x(), center.y(), radius, start, end, direct);
    }

    // Constructors based on Circles

    /**
     * Base constructor, for constructing arc from circle parameters, start and
     * end angles, and by specifying whether arc is direct or not.
     */
    public CircleArc2D(double xc, double yc, double r, double startAngle,
                       double endAngle, boolean direct) {
        this.circle = new Circle2D(xc, yc, r);
        this.startAngle = startAngle;
        this.angleExtent = endAngle;
        this.angleExtent = Angle2D.formatAngle(endAngle - startAngle);
        if (!direct)
            this.angleExtent = this.angleExtent - PI * 2;
    }

    /**
     * Base constructor with all parameters specified
     */
    public CircleArc2D(double xc, double yc, double r, double start,
                       double extent) {
        this.circle = new Circle2D(xc, yc, r);
        this.startAngle = start;
        this.angleExtent = extent;
    }

    // Constructors based on points

    /**
     * @deprecated since 0.11.1
     */
    @Deprecated
    public static CircleArc2D create(Circle2D support, double startAngle,
                                     double angleExtent) {
        return new CircleArc2D(support, startAngle, angleExtent);
    }

    /**
     * @deprecated since 0.11.1
     */
    @Deprecated
    public static CircleArc2D create(Circle2D support, double startAngle,
                                     double endAngle, boolean direct) {
        return new CircleArc2D(support, startAngle, endAngle, direct);
    }

    // Constructors based on doubles

    /**
     * @deprecated since 0.11.1
     */
    @Deprecated
    public static CircleArc2D create(Point2D center, double radius,
                                     double startAngle, double angleExtent) {
        return new CircleArc2D(center, radius, startAngle, angleExtent);
    }

    /**
     * @deprecated since 0.11.1
     */
    @Deprecated
    public static CircleArc2D create(Point2D center, double radius,
                                     double startAngle, double endAngle, boolean direct) {
        return new CircleArc2D(center, radius, startAngle, endAngle, direct);
    }

    // ====================================================================
    // methods specific to CircleArc2D

    /**
     * btan computes the length (k) of the control segments at
     * the beginning and end of a cubic Bezier that approximates
     * a segment of an arc with extent less than or equal to
     * 90 degrees.  This length (k) will be used to generate the
     * 2 Bezier control points for such a segment.
     * <p>
     * Assumptions:
     * a) arc is centered on 0,0 with radius of 1.0
     * b) arc extent is less than 90 degrees
     * c) control points should preserve tangent
     * d) control segments should have equal length
     * <p>
     * Initial data:
     * start angle: ang1
     * end angle:   ang2 = ang1 + extent
     * start point: P1 = (x1, y1) = (cos(ang1), sin(ang1))
     * end point:   P4 = (x4, y4) = (cos(ang2), sin(ang2))
     * <p>
     * Control points:
     * P2 = (x2, y2)
     * | x2 = x1 - k * sin(ang1) = cos(ang1) - k * sin(ang1)
     * | y2 = y1 + k * cos(ang1) = sin(ang1) + k * cos(ang1)
     * <p>
     * P3 = (x3, y3)
     * | x3 = x4 + k * sin(ang2) = cos(ang2) + k * sin(ang2)
     * | y3 = y4 - k * cos(ang2) = sin(ang2) - k * cos(ang2)
     * <p>
     * The formula for this length (k) can be found using the
     * following derivations:
     * <p>
     * Midpoints:
     * a) Bezier (t = 1/2)
     * bPm = P1 * (1-t)^3 +
     * 3 * P2 * t * (1-t)^2 +
     * 3 * P3 * t^2 * (1-t) +
     * P4 * t^3 =
     * = (P1 + 3P2 + 3P3 + P4)/8
     * <p>
     * b) arc
     * aPm = (cos((ang1 + ang2)/2), sin((ang1 + ang2)/2))
     * <p>
     * Let angb = (ang2 - ang1)/2; angb is half of the angle
     * between ang1 and ang2.
     * <p>
     * Solve the equation bPm == aPm
     * <p>
     * a) For xm coord:
     * x1 + 3*x2 + 3*x3 + x4 = 8*cos((ang1 + ang2)/2)
     * <p>
     * cos(ang1) + 3*cos(ang1) - 3*k*sin(ang1) +
     * 3*cos(ang2) + 3*k*sin(ang2) + cos(ang2) =
     * = 8*cos((ang1 + ang2)/2)
     * <p>
     * 4*cos(ang1) + 4*cos(ang2) + 3*k*(sin(ang2) - sin(ang1)) =
     * = 8*cos((ang1 + ang2)/2)
     * <p>
     * 8*cos((ang1 + ang2)/2)*cos((ang2 - ang1)/2) +
     * 6*k*sin((ang2 - ang1)/2)*cos((ang1 + ang2)/2) =
     * = 8*cos((ang1 + ang2)/2)
     * <p>
     * 4*cos(angb) + 3*k*sin(angb) = 4
     * <p>
     * k = 4 / 3 * (1 - cos(angb)) / sin(angb)
     * <p>
     * b) For ym coord we derive the same formula.
     * <p>
     * Since this formula can generate "NaN" values for small
     * angles, we will derive a safer form that does not involve
     * dividing by very small values:
     * (1 - cos(angb)) / sin(angb) =
     * = (1 - cos(angb))*(1 + cos(angb)) / sin(angb)*(1 + cos(angb)) =
     * = (1 - cos(angb)^2) / sin(angb)*(1 + cos(angb)) =
     * = sin(angb)^2 / sin(angb)*(1 + cos(angb)) =
     * = sin(angb) / (1 + cos(angb))
     * <p>
     * Function taken from java.awt.geom.ArcIterator.
     */
    private static double btan(double increment) {
        increment /= 2.0;
        return 4.0 / 3.0 * sin(increment) / (1.0 + cos(increment));
    }

    /**
     * Returns true if the circle arc is direct, i.e. if the angle extent is
     * positive.
     */
    public boolean isDirect() {
        return angleExtent >= 0;
    }

    public double getStartAngle() {
        return startAngle;
    }

    public double getAngleExtent() {
        return angleExtent;
    }

    /**
     * @return the area of this CircleArc2D
     */
    public double getArea() {
        // Get the area of the underlying circle
        double c_area = Math.PI * Math.pow(this.circle.radius(), 2.0);
        // What fraction of the underlying circle does this arc represent?
        double c_seg = Math.abs(4 * Math.PI / this.angleExtent);

        return c_area / c_seg;
    }

    /**
     * Gets the area of the chord defined by this arc
     *
     * @return the area of the chord
     */
    public double getChordArea() {
        if (2 * Math.PI == this.angleExtent) {
            return getArea();
        }

        return (circle.r * circle.r * (angleExtent - sin(angleExtent))) / 2;
    }

    public boolean containsAngle(double angle) {
        return Angle2D.containsAngle(
                startAngle, startAngle + angleExtent, angle, angleExtent >= 0);
    }

    /**
     * Returns the angle associated with the given position
     */
    public double getAngle(double position) {
        if (position < 0)
            position = 0;
        if (position > abs(angleExtent))
            position = abs(angleExtent);
        if (angleExtent < 0)
            position = -position;
        return Angle2D.formatAngle(startAngle + position);
    }

    // ===================================================================
    // methods implementing CircularShape2D interface

    /**
     * Converts position on curve to angle with circle center.
     */
    private double positionToAngle(double t) {
        if (t > abs(angleExtent))
            t = abs(angleExtent);
        if (t < 0)
            t = 0;
        if (angleExtent < 0)
            t = -t;
        t = t + startAngle;
        return t;
    }

    // ===================================================================
    // Methods implementing the CirculinearCurve2D interface

	/* (non-Javadoc)
     * @see CirculinearShape2D#buffer(double)
	 */
//	public CirculinearDomain2D buffer(double dist) {
//		BufferCalculator bc = BufferCalculator.getDefaultInstance();
//		return bc.computeBuffer(this, dist);
//	}

    /**
     * Returns the circle that contains the circle arc.
     */
    public Circle2D supportingCircle() {
        return circle;
    }

    /**
     * Returns the circle arc parallel to this circle arc, at the distance
     * dist.
     */
    public CircleArc2D parallel(double dist) {
        double r = circle.radius();
        double r2 = max(angleExtent > 0 ? r + dist : r - dist, 0);
        return new CircleArc2D(circle.center(), r2, startAngle, angleExtent);
    }

    public double length() {
        return circle.radius() * abs(angleExtent);
    }

    /*
     * (non-Javadoc)
     *
     * @see CirculinearCurve2D#length(double)
     */
    public double length(double pos) {
        return pos * circle.radius();
    }

    /*
     * (non-Javadoc)
     *
     * @see CirculinearCurve2D#position(double)
     */
    public double position(double length) {
        return length / circle.radius();
    }


    public double windingAngle(Point2D point) {
        Point2D p1 = firstPoint();
        Point2D p2 = lastPoint();

        // compute angle of point with extreme points
        double angle1 = Angle2D.horizontalAngle(point, p1);
        double angle2 = Angle2D.horizontalAngle(point, p2);

        boolean b1 = (new StraightLine2D(p1, p2)).isInside(point);
        boolean b2 = this.circle.isInside(point);

        if (angleExtent > 0) {
            if (b1 || b2) {
                if (angle2 > angle1)
                    return angle2 - angle1;
                else
                    return 2 * Math.PI - angle1 + angle2;
            } else {
                if (angle2 > angle1)
                    return angle2 - angle1 - 2 * Math.PI;
                else
                    return angle2 - angle1;
            }
        } else {
            if (!b1 || b2) {
                if (angle1 > angle2)
                    return angle2 - angle1;
                else
                    return angle2 - angle1 - 2 * Math.PI;
            } else {
                if (angle1 > angle2)
                    return angle2 - angle1 + 2 * Math.PI;
                else
                    return angle2 - angle1;
            }
        }
    }

    public boolean isInside(Point2D point) {
        return signedDistance(point.x(), point.y()) < 0;
    }

    public double signedDistance(Point2D p) {
        return signedDistance(p.x(), p.y());
    }

    // ====================================================================
    // methods from interface SmoothCurve2D

    public double signedDistance(double x, double y) {
        double dist = distance(x, y);
        Point2D point = new Point2D(x, y);

        boolean direct = angleExtent > 0;
        boolean inCircle = circle.isInside(point);
        if (inCircle)
            return direct ? -dist : dist;

        Point2D p1 = circle.point(startAngle);
        Point2D p2 = circle.point(startAngle + angleExtent);
        boolean onLeft = (new StraightLine2D(p1, p2)).isInside(point);

        if (direct && !onLeft)
            return dist;
        if (!direct && onLeft)
            return -dist;

        Vector2D tangent = circle.tangent(startAngle);
        boolean left1 = (new Ray2D(p1, tangent)).isInside(point);
        if (direct && !left1)
            return dist;
        if (!direct && left1)
            return -dist;

        tangent = circle.tangent(startAngle + angleExtent);
        boolean left2 = (new Ray2D(p2, tangent)).isInside(point);
        if (direct && !left2)
            return dist;
        if (!direct && left2)
            return -dist;

        if (direct)
            return -dist;
        else
            return dist;
    }

    public Vector2D tangent(double t) {
        t = this.positionToAngle(t);

        double r = circle.radius();
        if (angleExtent > 0)
            return new Vector2D(-r * sin(t), r * cos(t));
        else
            return new Vector2D(r * sin(t), -r * cos(t));
    }


    // ===================================================================
    // methods from interface ContinuousCurve2D

    /**
     * Returns curvature of the circle arc. This is the same as the curvature
     * of the parent circle, with a control on the sign that depends on the
     * orientation.
     */
    public double curvature(double t) {
        double kappa = circle.curvature(t);
        return this.isDirect() ? kappa : -kappa;
    }

    /**

    /**
     * Returns false, as a circle arc is never closed by definition.
     */
    public boolean isClosed() {
        return false;
    }

    @Override
    public Vector2D leftTangent(double t) {
        return null;
    }

    @Override
    public Vector2D rightTangent(double t) {
        return null;
    }


    // ====================================================================
    // methods from interface Curve2D

    /* (non-Javadoc)
     * @see ContinuousCurve2D#asPolyline(int)
     */
    public LinearCurve2D asPolyline(int n) {

        // compute increment value
        double dt = Math.abs(this.angleExtent) / n;

        // allocate array of points, and compute each value.
        // Computes also value for last point.
        Point2D[] points = new Point2D[n + 1];
        for (int i = 0; i < n + 1; i++)
            points[i] = this.point(i * dt);

        return new Polyline2D(points);
    }

    /**
     * Returns 0.
     */
    public double t0() {
        return 0;
    }

    /**
     * @deprecated replaced by t0()
     */
    @Deprecated
    public double getT0() {
        return 0;
    }

    /**
     * Returns the last position of the circle are, which is given by the
     * absolute angle of angle extent of this arc.
     */
    public double t1() {
        return abs(this.angleExtent);
    }

    /**
     * @deprecated replaced by t1()
     */
    @Deprecated
    public double getT1() {
        return abs(this.angleExtent);
    }

    /**
     * Returns the position of a point form the curvilinear position.
     */
    public Point2D point(double t) {
        t = this.positionToAngle(t);
        return circle.point(t);
    }

    @Override
    public Point2D firstPoint() {
        return null;
    }

    @Override
    public Point2D lastPoint() {
        return null;
    }

    /**
     * Returns relative position between 0 and the angle extent.
     */
    public double position(Point2D point) {
        double angle = Angle2D.horizontalAngle(circle.center(), point);
        if (containsAngle(angle))
            if (angleExtent > 0)
                return Angle2D.formatAngle(angle - startAngle);
            else
                return Angle2D.formatAngle(startAngle - angle);

        // return either 0 or 1, depending on which extremity is closer.
        return firstPoint().distance(point) <
                lastPoint().distance(point) ? 0 : abs(angleExtent);
    }

    /**
     * Computes intersections of the circle arc with a line. Return an array of
     * Point2D, of size 0, 1 or 2 depending on the distance between circle and
     * line. If there are 2 intersections points, the first one in the array is
     * the first one on the line.
     */
    public Collection<Point2D> intersections(LineSegment2D line) {
        return Circle2D.lineCircleIntersections(line, this);
    }

    public double project(Point2D point) {
        double angle = circle.project(point);

        // Case of an angle contained in the circle arc
        if (Angle2D.containsAngle(startAngle, startAngle + angleExtent, angle,
                angleExtent > 0)) {
            if (angleExtent > 0)
                return Angle2D.formatAngle(angle - startAngle);
            else
                return Angle2D.formatAngle(startAngle - angle);
        }

        Point2D p1 = this.firstPoint();
        Point2D p2 = this.lastPoint();
        if (p1.distance(point) < p2.distance(point))
            return 0;
        else
            return abs(angleExtent);
    }

    @Override
    public Collection<Point2D> intersections(LinearShape2D line) {
        return null;
    }

    /**
     * Returns a new CircleArc2D. Variables t0 and t1 must be comprised between 0
     * and the angle extent of the arc.
     */
    public CircleArc2D subCurve(double t0, double t1) {
        // convert position to angle
        if (angleExtent > 0) {
            t0 = Angle2D.formatAngle(startAngle + t0);
            t1 = Angle2D.formatAngle(startAngle + t1);
        } else {
            t0 = Angle2D.formatAngle(startAngle - t0);
            t1 = Angle2D.formatAngle(startAngle - t1);
        }

        // check bounds of angles
        if (!Angle2D.containsAngle(startAngle, startAngle + angleExtent, t0,
                angleExtent > 0))
            t0 = startAngle;
        if (!Angle2D.containsAngle(startAngle, startAngle + angleExtent, t1,
                angleExtent > 0))
            t1 = Angle2D.formatAngle(startAngle + angleExtent);

        // create new arc
        return new CircleArc2D(circle, t0, t1, angleExtent > 0);
    }

    /**
     * Returns the circle arc which refers to the same parent circle, but
     * with exchanged extremities.
     */
    public CircleArc2D reverse() {
        double newStart = Angle2D.formatAngle(startAngle + angleExtent);
        return new CircleArc2D(this.circle, newStart, -angleExtent);
    }

    /**
     * Returns a collection of curves containing only this circle arc.
     * //
     */
//    public Collection<PolyCurve2D<T>> continuousCurves() {
//    	return AbstractContinuousCurve2D.wrapCurve(this);
//    }


    // ====================================================================
    // methods from interface Shape2D
    public double distance(Point2D p) {
        return distance(p.x(), p.y());
    }

    public double distance(double x, double y) {
        double angle = Angle2D.horizontalAngle(circle.xc, circle.yc, x, y);

        if (containsAngle(angle))
            return Math.abs(Point2D.distance(circle.xc, circle.yc, x, y) - circle.r);
        else
            return Math.min(firstPoint().distance(x, y), lastPoint().distance(x, y));
    }

    /**
     * Returns true, as a circle arc is bounded by definition.
     */
    public boolean isBounded() {
        return true;
    }


    public boolean contains(Point2D p) {
        return contains(p.x(), p.y());
    }

    public boolean contains(double x, double y) {
        // Check if radius is correct
        double r = circle.radius();
        if (Math.abs(Point2D.distance(circle.xc, circle.yc, x, y) - r) > Shape2D.ACCURACY)
            return false;

        // angle from circle center to point
        double angle = Angle2D.horizontalAngle(circle.xc, circle.yc, x, y);

        // check if angle is contained in interval [startAngle-angleExtent]
        return this.containsAngle(angle);
    }

    /**
     * Returns false.
     */
    public boolean isEmpty() {
        return false;
    }


    public Path appendPath(Path path) {
        // number of curves to approximate the arc
        int nSeg = (int) ceil(abs(angleExtent) / (PI / 2));
        nSeg = min(nSeg, 4);

        // angular extent of each curve
        double ext = angleExtent / nSeg;

        // compute coefficient
        double k = btan(abs(ext));

        for (int i = 0; i < nSeg; i++) {
            // position of the two extremities
            double ti0 = abs(i * ext);
            double ti1 = abs((i + 1) * ext);

            // extremity points
            Point2D p1 = this.point(ti0);
            Point2D p2 = this.point(ti1);

            // tangent vectors, multiplied by appropriate coefficient
            Vector2D v1 = this.tangent(ti0).times(k);
            Vector2D v2 = this.tangent(ti1).times(k);

            // append a cubic curve to the path
            path.rCubicTo(
                    (float) (p1.x() + v1.x()), (float) (p1.y() + v1.y()),
                    (float) (p2.x() - v2.x()), (float) (p2.y() - v2.y()),
                    (float) (p2.x()), (float) (p2.y()));
        }
        return path;
    }

    public Path getGeneralPath() {
        // create new path
        Path path = new Path();

        // move to the first point
        Point2D point = this.firstPoint();
        path.moveTo((float) point.x(), (float) point.y());

        // append the curve
        path = this.appendPath(path);

        // return the final path
        return path;
    }

    // ===================================================================
    // methods implementing GeometricObject2D interface

    /* (non-Javadoc)
     * @see GeometricObject2D#almostEquals(GeometricObject2D, double)
     */
    public boolean almostEquals(GeometricObject2D obj, double eps) {
        if (this == obj)
            return true;

        if (!(obj instanceof CircleArc2D))
            return super.equals(obj);

        CircleArc2D arc = (CircleArc2D) obj;
        // test whether supporting ellipses have same support
        if (Math.abs(circle.xc - arc.circle.xc) > eps)
            return false;
        if (Math.abs(circle.yc - arc.circle.yc) > eps)
            return false;
        if (Math.abs(circle.r - arc.circle.r) > eps)
            return false;
        if (Math.abs(circle.theta - arc.circle.theta) > eps)
            return false;

        // test is angles are the same
        if (Math.abs(Angle2D.formatAngle(startAngle)
                - Angle2D.formatAngle(arc.startAngle)) > eps)
            return false;
        if (Math.abs(Angle2D.formatAngle(angleExtent)
                - Angle2D.formatAngle(arc.angleExtent)) > eps)
            return false;

        // if no difference, this is the same
        return true;
    }

    // ===================================================================
    // methods implementing Object interface

    public String toString() {
        Point2D center = circle.center();
        return String.format(Locale.US,
                "CircleArc2D(%7.2f,%7.2f,%7.2f,%7.5f,%7.5f)",
                center.x(), center.y(), circle.radius(),
                getStartAngle(), getAngleExtent());
    }

    /**
     * Two circle arc are equal if the have same center, same radius, same
     * starting and ending angles, and same orientation.
     */
    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;

        if (!(obj instanceof CircleArc2D))
            return false;
        CircleArc2D that = (CircleArc2D) obj;

        // test whether supporting circles have same support
        if (!this.circle.equals(that.circle))
            return false;

        // test if angles are the same
        if (!EqualUtils.areEqual(startAngle, that.startAngle))
            return false;
        if (!EqualUtils.areEqual(angleExtent, that.angleExtent))
            return false;

        // if no difference, this is the same
        return true;
    }


    @Override
    public StraightLine2D supportingLine() {
        return null;
    }

    @Override
    public double horizontalAngle() {
        return 0;
    }

    @Override
    public Point2D origin() {
        return null;
    }

    @Override
    public Vector2D direction() {
        return null;
    }


    @Override
    public boolean containsProjection(Point2D point) {
        return false;
    }
}