View Javadoc

1   /* file : EllipseArc2D.java
2    * 
3    * Project : geometry
4    *
5    * ===========================================
6    * 
7    * This library is free software; you can redistribute it and/or modify it 
8    * under the terms of the GNU Lesser General Public License as published by
9    * the Free Software Foundation, either version 2.1 of the License, or (at
10   * your option) any later version.
11   *
12   * This library is distributed in the hope that it will be useful, but 
13   * WITHOUT ANY WARRANTY, without even the implied warranty of MERCHANTABILITY
14   * or FITNESS FOR A PARTICULAR PURPOSE.
15   *
16   * See the GNU Lesser General Public License for more details.
17   *
18   * You should have received a copy of the GNU Lesser General Public License
19   * along with this library. if not, write to :
20   * The Free Software Foundation, Inc., 59 Temple Place, Suite 330,
21   * Boston, MA 02111-1307, USA.
22   * 
23   * Created on 24 avr. 2006
24   *
25   */
26  
27  package math.geom2d.conic;
28  
29  import java.awt.Graphics2D;
30  import java.util.ArrayList;
31  import java.util.Collection;
32  import java.util.Locale;
33  
34  import math.geom2d.AffineTransform2D;
35  import math.geom2d.Angle2D;
36  import math.geom2d.Box2D;
37  import math.geom2d.Point2D;
38  import math.geom2d.Shape2D;
39  import math.geom2d.Vector2D;
40  import math.geom2d.curve.AbstractSmoothCurve2D;
41  import math.geom2d.curve.Curve2D;
42  import math.geom2d.curve.Curve2DUtils;
43  import math.geom2d.curve.CurveArray2D;
44  import math.geom2d.curve.CurveSet2D;
45  import math.geom2d.curve.SmoothCurve2D;
46  import math.geom2d.domain.SmoothOrientedCurve2D;
47  import math.geom2d.line.LinearShape2D;
48  import math.geom2d.line.Ray2D;
49  import math.geom2d.line.StraightLine2D;
50  
51  /**
52   * An arc of ellipse. It is defined by a supporting ellipse, a starting angle,
53   * and a signed angle extent, both in radians. The ellipse arc is oriented
54   * counter-clockwise if angle extent is positive, and clockwise otherwise.
55   * 
56   * @author dlegland
57   */
58  public class EllipseArc2D extends AbstractSmoothCurve2D
59  implements SmoothOrientedCurve2D, Cloneable {
60  
61      /** The supporting ellipse */
62      protected Ellipse2D ellipse;
63  
64      /** The starting position on ellipse, in radians between 0 and +2PI */
65      protected double    startAngle  = 0;
66  
67      /** The signed angle extent, in radians between -2PI and +2PI. */
68      protected double    angleExtent = Math.PI;
69  
70      // ====================================================================
71      // Constructors
72  
73      /**
74       * Construct a default Ellipse arc, centered on (0,0), with radii equal to 1
75       * and 1, orientation equal to 0, start angle equal to 0, and angle extent
76       * equal to PI/2.
77       */
78      public EllipseArc2D() {
79          this(0, 0, 1, 1, 0, 0, Math.PI/2);
80      }
81  
82      /**
83       * Specify supporting ellipse, start angle and angle extent.
84       * 
85       * @param ell the supporting ellipse
86       * @param start the starting angle (angle between 0 and 2*PI)
87       * @param extent the angle extent (signed angle)
88       */
89      public EllipseArc2D(Ellipse2D ell, double start, double extent) {
90      	this(ell.xc, ell.yc, ell.r1, ell.r2, ell.theta, start, extent);
91      }
92  
93      /**
94       * Specify supporting ellipse, start angle and end angle, and a flag
95       * indicating whether the arc is directed or not.
96       * 
97       * @param ell the supporting ellipse
98       * @param start the starting angle
99       * @param end the ending angle
100      * @param direct flag indicating if the arc is direct
101      */
102     public EllipseArc2D(Ellipse2D ell, double start, double end, boolean direct) {
103         this(ell.xc, ell.yc, ell.r1, ell.r2, ell.theta, start, end, direct);
104     }
105 
106     /**
107      * Specify parameters of supporting ellipse, start angle, and angle extent.
108      */
109     public EllipseArc2D(double xc, double yc, double a, double b, double theta,
110             double start, double extent) {
111         this.ellipse = new Ellipse2D(xc, yc, a, b, theta);
112         this.startAngle = start;
113         this.angleExtent = extent;
114     }
115 
116     /**
117      * Specify parameters of supporting ellipse, bounding angles and flag for
118      * direct ellipse.
119      */
120     public EllipseArc2D(double xc, double yc, double a, double b, double theta,
121             double start, double end, boolean direct) {
122         this.ellipse = new Ellipse2D(xc, yc, a, b, theta);
123         this.startAngle = start;
124         this.angleExtent = Angle2D.formatAngle(end-start);
125         if (!direct)
126             this.angleExtent = this.angleExtent-Math.PI*2;
127     }
128 
129     // ====================================================================
130     // methods specific to EllipseArc2D
131 
132     /**
133      * Specify supporting ellipse, start angle and end angle, and a flag
134      * indicating whether the arc is directed or not.
135      * 
136      * @param ell the supporting ellipse
137      * @param start the starting angle
138      * @param end the ending angle
139      * @param direct flag indicating if the arc is direct
140      */
141     public static EllipseArc2D create(Ellipse2D ell, double start, 
142     		double extent) {
143         return new EllipseArc2D(ell.xc, ell.yc, ell.r1, ell.r2, ell.theta,
144         		start, extent);
145     }
146 
147     /**
148      * Specify supporting ellipse, start angle and end angle, and a flag
149      * indicating whether the arc is directed or not.
150      * 
151      * @param ell the supporting ellipse
152      * @param start the starting angle
153      * @param end the ending angle
154      * @param direct flag indicating if the arc is direct
155      */
156     public static EllipseArc2D create(Ellipse2D ell, double start, double end,
157     		boolean direct) {
158     	return new EllipseArc2D(ell.xc, ell.yc, ell.r1, ell.r2, ell.theta,
159     			start, end, direct);
160     }
161 
162     // ====================================================================
163     // methods specific to EllipseArc2D
164 
165     public Ellipse2D getSupportingEllipse() {
166     	return ellipse;
167     }
168     
169     public double getStartAngle() {
170     	return startAngle;
171     }
172     
173     public double getAngleExtent() {
174     	return angleExtent;
175     }
176     
177     /**
178      * Returns true if the ellipse arc is direct, i.e. if the angle extent is
179      * positive.
180      */
181     public boolean isDirect() {
182     	return angleExtent>0;
183     }
184     
185     public boolean containsAngle(double angle) {
186         return Angle2D.containsAngle(
187         		startAngle, startAngle+angleExtent, angle, angleExtent>0);
188     }
189 
190     /** Returns the angle associated with the given position */
191     public double getAngle(double position) {
192         if (position<0)
193             position = 0;
194         if (position>Math.abs(angleExtent))
195             position = Math.abs(angleExtent);
196         if (angleExtent<0)
197             position = -position;
198         return Angle2D.formatAngle(startAngle+position);
199     }
200 
201     // ====================================================================
202     // methods from interface OrientedCurve2D
203 
204     /*
205      * (non-Javadoc)
206      * 
207      * @see math.geom2d.OrientedCurve2D#getViewAngle(math.geom2d.Point2D)
208      */
209     public double getWindingAngle(java.awt.geom.Point2D point) {
210         Point2D p1 = getPoint(0);
211         Point2D p2 = getPoint(Math.abs(angleExtent));
212 
213         // compute angle of point with extreme points
214         double angle1 = Angle2D.getHorizontalAngle(point, p1);
215         double angle2 = Angle2D.getHorizontalAngle(point, p2);
216 
217         // test on which 'side' of the arc the point lie
218         boolean b1 = (new StraightLine2D(p1, p2)).isInside(point);
219         boolean b2 = ellipse.isInside(point);
220 
221         if (angleExtent>0) {
222             if (b1||b2) { // inside of ellipse arc
223                 if (angle2>angle1)
224                     return angle2-angle1;
225                 else
226                     return 2*Math.PI-angle1+angle2;
227             } else { // outside of ellipse arc
228                 if (angle2>angle1)
229                     return angle2-angle1-2*Math.PI;
230                 else
231                     return angle2-angle1;
232             }
233         } else {
234             if (!b1||b2) {
235                 if (angle1>angle2)
236                     return angle2-angle1;
237                 else
238                     return angle2-angle1-2*Math.PI;
239             } else {
240                 if (angle1>angle2)
241                     return angle2-angle1+2*Math.PI;
242                 else
243                     return angle2-angle1;
244             }
245             // if(b1 || b2){
246             // if(angle1>angle2) return angle1 - angle2;
247             // else return 2*Math.PI - angle2 + angle1;
248             // }else{
249             // if(angle1>angle2) return angle1 - angle2 - 2*Math.PI;
250             // else return angle1 - angle2;
251             // }
252         }
253     }
254 
255     public boolean isInside(java.awt.geom.Point2D p) {
256         return getSignedDistance(p.getX(), p.getY())<0;
257     }
258 
259     public double getSignedDistance(java.awt.geom.Point2D p) {
260         return getSignedDistance(p.getX(), p.getY());
261     }
262 
263     /*
264      * (non-Javadoc)
265      * 
266      * @see math.geom2d.Shape2D#getSignedDistance(math.geom2d.Point2D)
267      */
268     public double getSignedDistance(double x, double y) {
269         boolean direct = angleExtent>0;
270 
271         double dist = getDistance(x, y);
272         Point2D point = new Point2D(x, y);
273 
274         boolean inside = ellipse.isInside(point);
275         if (inside)
276             return angleExtent>0 ? -dist : dist;
277 
278         Point2D p1 = getPoint(startAngle);
279         Point2D p2 = getPoint(startAngle+angleExtent);
280         boolean onLeft = (new StraightLine2D(p1, p2)).isInside(point);
281 
282         if (direct&&!onLeft)
283             return dist;
284         if (!direct&&onLeft)
285             return -dist;
286 
287         boolean left1 = (new Ray2D(p1, -Math.sin(startAngle), Math
288                 .cos(startAngle))).isInside(point);
289         if (direct&&!left1)
290             return dist;
291         if (!direct&&left1)
292             return -dist;
293 
294         boolean left2 = (new Ray2D(p2, -Math.sin(startAngle+angleExtent), Math
295                 .cos(startAngle+angleExtent))).isInside(point);
296         if (direct&&!left2)
297             return dist;
298         if (!direct&&left2)
299             return -dist;
300 
301         if (direct)
302             return -dist;
303         else
304             return dist;
305     }
306 
307     // ====================================================================
308     // methods from interface SmoothCurve2D
309 
310     public Vector2D getTangent(double t) {
311     	// format between min and max admissible values
312     	t = Math.min(Math.max(0, t), Math.abs(angleExtent));
313     	
314     	// compute tangent vector depending on position
315         if (angleExtent<0) {
316         	// need to invert vector for indirect arcs
317             return ellipse.getTangent(startAngle-t).times(-1);
318         } else {
319             return ellipse.getTangent(startAngle+t);
320         }
321     }
322 
323     /**
324      * returns the curvature of the ellipse arc.
325      */
326     public double getCurvature(double t) {
327         // convert position to angle
328         if (angleExtent<0)
329             t = startAngle-t;
330         else
331             t = startAngle+t;
332         double kappa = ellipse.getCurvature(t);
333         return this.isDirect() ? kappa : -kappa;
334     }
335 
336     // ====================================================================
337     // methods from interface ContinuousCurve2D
338 
339     /** Returns false, as an ellipse arc is never closed. */
340     public boolean isClosed() {
341         return false;
342     }
343 
344     // ====================================================================
345     // methods from interface Curve2D
346 
347     /** Always returns 0 */
348     public double getT0() {
349         return 0;
350     }
351 
352     /** Always returns the absolute value of the angle extent */
353     public double getT1() {
354         return Math.abs(angleExtent);
355     }
356 
357     /*
358      * (non-Javadoc)
359      * 
360      * @see math.geom2d.Curve2D#getPoint(double, math.geom2d.Point2D)
361      */
362     public Point2D getPoint(double t) {
363         // check bounds
364         t = Math.max(t, 0);
365         t = Math.min(t, Math.abs(angleExtent));
366 
367         // convert position to angle
368         if (angleExtent<0)
369             t = startAngle-t;
370         else
371             t = startAngle+t;
372 
373         // return corresponding point
374         return ellipse.getPoint(t);
375     }
376 
377     /*
378      * (non-Javadoc)
379      * 
380      * @see math.geom2d.Curve2D#getPosition(math.geom2d.Point2D)
381      */
382     public double getPosition(java.awt.geom.Point2D point) {
383         double angle = Angle2D.getHorizontalAngle(ellipse.getCenter(), point);
384         if (this.containsAngle(angle))
385             if (angleExtent>0)
386                 return Angle2D.formatAngle(angle-startAngle);
387             else
388                 return Angle2D.formatAngle(startAngle-angle);
389 
390         // If the point is not contained in the arc, return NaN.
391         return Double.NaN;
392     }
393 
394     public double project(java.awt.geom.Point2D point) {
395         double angle = ellipse.project(point);
396 
397         // Case of an angle contained in the ellipse arc
398         if (this.containsAngle(angle)) {
399             if (angleExtent>0)
400                 return Angle2D.formatAngle(angle-startAngle);
401             else
402                 return Angle2D.formatAngle(startAngle-angle);
403         }
404 
405         // return either 0 or T1, depending on which extremity is closer.
406         double d1 = this.getFirstPoint().distance(point);
407         double d2 = this.getLastPoint().distance(point);
408         return d1<d2 ? 0 : Math.abs(angleExtent);
409     }
410     
411     /*
412      * (non-Javadoc)
413      * 
414      * @see math.geom2d.Curve2D#getIntersections(math.geom2d.LinearShape2D)
415      */
416     public Collection<Point2D> getIntersections(LinearShape2D line) {
417 
418         // check point contained in it
419         ArrayList<Point2D> array = new ArrayList<Point2D>();
420         for (Point2D point : ellipse.getIntersections(line))
421             if (contains(point))
422                 array.add(point);
423 
424         return array;
425     }
426 
427     /**
428      * Returns the ellipse arc which refers to the reversed parent ellipse, with
429      * same start angle, and with opposite angle extent.
430      */
431     public EllipseArc2D getReverseCurve() {
432         return new EllipseArc2D(ellipse, Angle2D.formatAngle(startAngle
433                 +angleExtent), -angleExtent);
434     }
435 
436 	@Override
437     public Collection<? extends EllipseArc2D> getContinuousCurves() {
438     	return wrapCurve(this);
439     }
440 
441     /**
442      * Returns a new EllipseArc2D.
443      */
444     public EllipseArc2D getSubCurve(double t0, double t1) {
445         // convert position to angle
446         t0 = Angle2D.formatAngle(startAngle+t0);
447         t1 = Angle2D.formatAngle(startAngle+t1);
448 
449         // check bounds of angles
450         if (!Angle2D.containsAngle(startAngle, startAngle+angleExtent, t0,
451                 angleExtent>0))
452             t0 = startAngle;
453         if (!Angle2D.containsAngle(startAngle, startAngle+angleExtent, t1,
454                 angleExtent>0))
455             t1 = angleExtent;
456 
457         // create new arc
458         return new EllipseArc2D(ellipse, t0, t1, angleExtent>0);
459     }
460 
461     // ====================================================================
462     // methods from interface Shape2D
463 
464     /*
465      * (non-Javadoc)
466      * 
467      * @see math.geom2d.Shape2D#getDistance(math.geom2d.Point2D)
468      */
469     public double getDistance(java.awt.geom.Point2D point) {
470         return getDistance(point.getX(), point.getY());
471     }
472 
473     /*
474      * (non-Javadoc)
475      * 
476      * @see math.geom2d.Shape2D#getDistance(double, double)
477      */
478     public double getDistance(double x, double y) {
479         Point2D p = getPoint(project(new Point2D(x, y)));
480         return p.getDistance(x, y);
481     }
482 
483     /** Always return true: an ellipse arc is bounded by definition */
484     public boolean isBounded() {
485         return true;
486     }
487 
488     public boolean isEmpty() {
489         return false;
490     }
491 
492     /**
493      * Clip the ellipse arc by a box. The result is an instance of CurveSet2D<EllipseArc2D>,
494      * which contains only instances of EllipseArc2D. If the ellipse arc is not
495      * clipped, the result is an instance of CurveSet2D<EllipseArc2D> which
496      * contains 0 curves.
497      */
498     public CurveSet2D<? extends EllipseArc2D> clip(Box2D box) {
499         // Clip the curve
500         CurveSet2D<SmoothCurve2D> set = Curve2DUtils.clipSmoothCurve(this, box);
501 
502         // Stores the result in appropriate structure
503         CurveArray2D<EllipseArc2D> result = 
504         	new CurveArray2D<EllipseArc2D>(set.getCurveNumber());
505 
506         // convert the result
507         for (Curve2D curve : set.getCurves()) {
508             if (curve instanceof EllipseArc2D)
509                 result.addCurve((EllipseArc2D) curve);
510         }
511         return result;
512     }
513 
514     public Box2D getBoundingBox() {
515 
516         // first get ending points
517         Point2D p0 = getFirstPoint();
518         Point2D p1 = getLastPoint();
519 
520         // get coordinate of ending points
521         double x0 = p0.getX();
522         double y0 = p0.getY();
523         double x1 = p1.getX();
524         double y1 = p1.getY();
525 
526         // intialize min and max coords
527         double xmin = Math.min(x0, x1);
528         double xmax = Math.max(x0, x1);
529         double ymin = Math.min(y0, y1);
530         double ymax = Math.max(y0, y1);
531 
532         // check cases arc contains one maximum
533         Point2D center = ellipse.getCenter();
534         double xc = center.getX();
535         double yc = center.getY();
536         if (Angle2D.containsAngle(startAngle, startAngle+angleExtent, Math.PI/2
537                 +ellipse.theta, angleExtent>=0))
538             ymax = Math.max(ymax, yc+ellipse.r1);
539         if (Angle2D.containsAngle(startAngle, startAngle+angleExtent, 3*Math.PI
540                 /2+ellipse.theta, angleExtent>=0))
541             ymin = Math.min(ymin, yc-ellipse.r1);
542         if (Angle2D.containsAngle(startAngle, startAngle+angleExtent,
543                 ellipse.theta, angleExtent>=0))
544             xmax = Math.max(xmax, xc+ellipse.r2);
545         if (Angle2D.containsAngle(startAngle, startAngle+angleExtent, Math.PI
546                 +ellipse.theta, angleExtent>=0))
547             xmin = Math.min(xmin, xc-ellipse.r2);
548 
549         // return a bounding with computed limits
550         return new Box2D(xmin, xmax, ymin, ymax);
551     }
552 
553     /*
554      * (non-Javadoc)
555      * 
556      * @see math.geom2d.Shape2D#transform(math.geom2d.AffineTransform2D)
557      */
558     public EllipseArc2D transform(AffineTransform2D trans) {
559         // transform supporting ellipse
560         Ellipse2D ell = ellipse.transform(trans);
561 
562         // ensure ellipse is direct
563         if (!ell.isDirect())
564             ell = ell.getReverseCurve();
565 
566         // Compute position of end points on the transformed ellipse
567         double startPos = ell.project(this.getFirstPoint().transform(trans));
568         double endPos = ell.project(this.getLastPoint().transform(trans));
569 
570         // Compute the new arc
571         boolean direct = !(angleExtent>0^trans.isDirect());
572         return new EllipseArc2D(ell, startPos, endPos, direct);
573     }
574 
575     /*
576      * (non-Javadoc)
577      * 
578      * @see java.awt.Shape#contains(double, double)
579      */
580     public boolean contains(double x, double y) {
581         return getDistance(x, y)>Shape2D.ACCURACY;
582     }
583 
584     /*
585      * (non-Javadoc)
586      * 
587      * @see java.awt.Shape#contains(java.awt.geom.Point2D)
588      */
589     public boolean contains(java.awt.geom.Point2D point) {
590         return contains(point.getX(), point.getY());
591     }
592 
593     public java.awt.geom.GeneralPath appendPath(java.awt.geom.GeneralPath path) {
594     	// number of curves to approximate the arc
595     	int nSeg = (int) Math.ceil(Math.abs(angleExtent)/(Math.PI/2));
596     	nSeg = Math.min(nSeg, 4);
597     	
598     	// angular extent of each curve
599     	double ext = angleExtent/nSeg;
600     	
601     	// compute coefficient 
602     	double k = btan(Math.abs(ext));
603     	
604     	for(int i=0; i<nSeg; i++) {
605     		// position of the two extremities
606     		double ti0 = Math.abs(i*ext);
607     		double ti1 = Math.abs((i+1)*ext);
608     		
609     		// extremity points
610     		Point2D p1 = this.getPoint(ti0);
611     		Point2D p2 = this.getPoint(ti1);
612     		
613     		// tangent vectors, multiplied by appropriate coefficient
614     		Vector2D v1 = this.getTangent(ti0).times(k);
615     		Vector2D v2 = this.getTangent(ti1).times(k);
616     		
617     		// append a cubic curve to the path
618     		path.curveTo(
619     				p1.getX()+v1.getX(), p1.getY()+v1.getY(), 
620     				p2.getX()-v2.getX(), p2.getY()-v2.getY(), 
621     				p2.getX(), p2.getY());
622     	}
623 		return path;    		
624     }
625 
626     public java.awt.geom.GeneralPath getGeneralPath() {
627         // create new path
628         java.awt.geom.GeneralPath path = new java.awt.geom.GeneralPath();
629 
630         // move to the first point
631         Point2D point = this.getFirstPoint();
632         path.moveTo((float) point.getX(), (float) point.getY());
633 
634         // append the curve
635         path = this.appendPath(path);
636 
637         // return the final path
638         return path;
639     }
640 
641 	@Override
642     public void draw(Graphics2D g2) {
643         g2.draw(this.getGeneralPath());
644     }
645 
646     /**
647      * 
648      * btan computes the length (k) of the control segments at
649      * the beginning and end of a cubic Bezier that approximates
650      * a segment of an arc with extent less than or equal to
651      * 90 degrees.  This length (k) will be used to generate the
652      * 2 Bezier control points for such a segment.
653      *
654      *   Assumptions:
655      *     a) arc is centered on 0,0 with radius of 1.0
656      *     b) arc extent is less than 90 degrees
657      *     c) control points should preserve tangent
658      *     d) control segments should have equal length
659      *
660      *   Initial data:
661      *     start angle: ang1
662      *     end angle:   ang2 = ang1 + extent
663      *     start point: P1 = (x1, y1) = (cos(ang1), sin(ang1))
664      *     end point:   P4 = (x4, y4) = (cos(ang2), sin(ang2))
665      *
666      *   Control points:
667      *     P2 = (x2, y2)
668      *     | x2 = x1 - k * sin(ang1) = cos(ang1) - k * sin(ang1)
669      *     | y2 = y1 + k * cos(ang1) = sin(ang1) + k * cos(ang1)
670      *
671      *     P3 = (x3, y3)
672      *     | x3 = x4 + k * sin(ang2) = cos(ang2) + k * sin(ang2)
673      *     | y3 = y4 - k * cos(ang2) = sin(ang2) - k * cos(ang2)
674      *
675      * The formula for this length (k) can be found using the
676      * following derivations:
677      *
678      *   Midpoints:
679      *     a) Bezier (t = 1/2)
680      *        bPm = P1 * (1-t)^3 +
681      *              3 * P2 * t * (1-t)^2 +
682      *              3 * P3 * t^2 * (1-t) +
683      *              P4 * t^3 =
684      *            = (P1 + 3P2 + 3P3 + P4)/8
685      *
686      *     b) arc
687      *        aPm = (cos((ang1 + ang2)/2), sin((ang1 + ang2)/2))
688      *
689      *   Let angb = (ang2 - ang1)/2; angb is half of the angle
690      *   between ang1 and ang2.
691      *
692      *   Solve the equation bPm == aPm
693      *
694      *     a) For xm coord:
695      *        x1 + 3*x2 + 3*x3 + x4 = 8*cos((ang1 + ang2)/2)
696      *
697      *        cos(ang1) + 3*cos(ang1) - 3*k*sin(ang1) +
698      *        3*cos(ang2) + 3*k*sin(ang2) + cos(ang2) =
699      *        = 8*cos((ang1 + ang2)/2)
700      *
701      *        4*cos(ang1) + 4*cos(ang2) + 3*k*(sin(ang2) - sin(ang1)) =
702      *        = 8*cos((ang1 + ang2)/2)
703      *
704      *        8*cos((ang1 + ang2)/2)*cos((ang2 - ang1)/2) +
705      *        6*k*sin((ang2 - ang1)/2)*cos((ang1 + ang2)/2) =
706      *        = 8*cos((ang1 + ang2)/2)
707      *
708      *        4*cos(angb) + 3*k*sin(angb) = 4
709      *
710      *        k = 4 / 3 * (1 - cos(angb)) / sin(angb)
711      *
712      *     b) For ym coord we derive the same formula.
713      *
714      * Since this formula can generate "NaN" values for small
715      * angles, we will derive a safer form that does not involve
716      * dividing by very small values:
717      *     (1 - cos(angb)) / sin(angb) =
718      *     = (1 - cos(angb))*(1 + cos(angb)) / sin(angb)*(1 + cos(angb)) =
719      *     = (1 - cos(angb)^2) / sin(angb)*(1 + cos(angb)) =
720      *     = sin(angb)^2 / sin(angb)*(1 + cos(angb)) =
721      *     = sin(angb) / (1 + cos(angb))
722      *
723      * Function taken from java.awt.geom.ArcIterator.
724      */
725     private static double btan(double increment) {
726         increment /= 2.0;
727         return 4.0 / 3.0 * Math.sin(increment) / (1.0 + Math.cos(increment));
728     }
729 
730     // ====================================================================
731     // methods from interface Object
732 
733     @Override
734     public String toString() {
735     	Point2D center = ellipse.getCenter();
736         return String.format(Locale.US, 
737                 "EllipseArc2D(%7.2f,%7.2f,%7.2f,%7.2f,%7.5f,%7.5f,%7.5f)", 
738                 center.getX(), center.getY(), 
739                 ellipse.r1, ellipse.r2, ellipse.theta,
740                 startAngle, angleExtent);
741     }
742 
743     @Override
744     public boolean equals(Object obj) {
745         if (!(obj instanceof EllipseArc2D))
746             return false;
747         EllipseArc2D arc = (EllipseArc2D) obj;
748 
749         // test whether supporting ellipses have same support
750         if (Math.abs(ellipse.xc-arc.ellipse.xc)>Shape2D.ACCURACY)
751             return false;
752         if (Math.abs(ellipse.yc-arc.ellipse.yc)>Shape2D.ACCURACY)
753             return false;
754         if (Math.abs(ellipse.r1-arc.ellipse.r1)>Shape2D.ACCURACY)
755             return false;
756         if (Math.abs(ellipse.r2-arc.ellipse.r2)>Shape2D.ACCURACY)
757             return false;
758         if (Math.abs(ellipse.theta-arc.ellipse.theta)>Shape2D.ACCURACY)
759             return false;
760 
761         // test if angles are the same
762         if (!Angle2D.equals(startAngle, arc.startAngle))
763             return false;
764         if (!Angle2D.equals(angleExtent, arc.angleExtent))
765             return false;
766 
767         return true;
768     }
769     
770     @Override
771     public EllipseArc2D clone() {
772         return new EllipseArc2D(ellipse, startAngle, angleExtent);
773     }
774 }