1   /* Copyright 2002-2022 CS GROUP
2    * Licensed to CS GROUP (CS) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * CS licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *   http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.orekit.bodies;
18  
19  import java.io.Serializable;
20  
21  import org.hipparchus.CalculusFieldElement;
22  import org.hipparchus.Field;
23  import org.hipparchus.analysis.differentiation.DerivativeStructure;
24  import org.hipparchus.geometry.euclidean.threed.FieldLine;
25  import org.hipparchus.geometry.euclidean.threed.FieldVector3D;
26  import org.hipparchus.geometry.euclidean.threed.Line;
27  import org.hipparchus.geometry.euclidean.threed.Vector3D;
28  import org.hipparchus.geometry.euclidean.twod.Vector2D;
29  import org.hipparchus.util.FastMath;
30  import org.hipparchus.util.FieldSinCos;
31  import org.hipparchus.util.MathArrays;
32  import org.hipparchus.util.MathUtils;
33  import org.hipparchus.util.SinCos;
34  import org.orekit.frames.FieldTransform;
35  import org.orekit.frames.Frame;
36  import org.orekit.frames.StaticTransform;
37  import org.orekit.frames.Transform;
38  import org.orekit.time.AbsoluteDate;
39  import org.orekit.time.FieldAbsoluteDate;
40  import org.orekit.utils.PVCoordinates;
41  import org.orekit.utils.TimeStampedPVCoordinates;
42  
43  
44  /** Modeling of a one-axis ellipsoid.
45  
46   * <p>One-axis ellipsoids is a good approximate model for most planet-size
47   * and larger natural bodies. It is the equilibrium shape reached by
48   * a fluid body under its own gravity field when it rotates. The symmetry
49   * axis is the rotation or polar axis.</p>
50  
51   * @author Luc Maisonobe
52   * @author Guylaine Prat
53   */
54  public class OneAxisEllipsoid extends Ellipsoid implements BodyShape {
55  
56      /** Serializable UID. */
57      private static final long serialVersionUID = 20130518L;
58  
59      /** Threshold for polar and equatorial points detection. */
60      private static final double ANGULAR_THRESHOLD = 1.0e-4;
61  
62      /** Body frame related to body shape. */
63      private final Frame bodyFrame;
64  
65      /** Equatorial radius power 2. */
66      private final double ae2;
67  
68      /** Polar radius power 2. */
69      private final double ap2;
70  
71      /** Flattening. */
72      private final double f;
73  
74      /** Eccentricity. */
75      private final double e;
76  
77      /** Eccentricity squared. */
78      private final double e2;
79  
80      /** 1 minus flatness. */
81      private final double g;
82  
83      /** g squared. */
84      private final double g2;
85  
86      /** Convergence limit. */
87      private double angularThreshold;
88  
89      /** Simple constructor.
90       * <p>Standard values for Earth models can be found in the {@link org.orekit.utils.Constants Constants} class:</p>
91       * <table border="1" style="background-color:#f5f5dc;">
92       * <caption>Ellipsoid Models</caption>
93       * <tr style="background-color:#c9d5c9;"><th>model</th><th>a<sub>e</sub> (m)</th> <th>f</th></tr>
94       * <tr><td style="background-color:#c9d5c9; padding:5px">GRS 80</td>
95       *     <td>{@link org.orekit.utils.Constants#GRS80_EARTH_EQUATORIAL_RADIUS Constants.GRS80_EARTH_EQUATORIAL_RADIUS}</td>
96       *     <td>{@link org.orekit.utils.Constants#GRS80_EARTH_FLATTENING Constants.GRS80_EARTH_FLATTENING}</td></tr>
97       * <tr><td style="background-color:#c9d5c9; padding:5px">WGS84</td>
98       *     <td>{@link org.orekit.utils.Constants#WGS84_EARTH_EQUATORIAL_RADIUS Constants.WGS84_EARTH_EQUATORIAL_RADIUS}</td>
99       *     <td>{@link org.orekit.utils.Constants#WGS84_EARTH_FLATTENING Constants.WGS84_EARTH_FLATTENING}</td></tr>
100      * <tr><td style="background-color:#c9d5c9; padding:5px">IERS96</td>
101      *     <td>{@link org.orekit.utils.Constants#IERS96_EARTH_EQUATORIAL_RADIUS Constants.IERS96_EARTH_EQUATORIAL_RADIUS}</td>
102      *     <td>{@link org.orekit.utils.Constants#IERS96_EARTH_FLATTENING Constants.IERS96_EARTH_FLATTENING}</td></tr>
103      * <tr><td style="background-color:#c9d5c9; padding:5px">IERS2003</td>
104      *     <td>{@link org.orekit.utils.Constants#IERS2003_EARTH_EQUATORIAL_RADIUS Constants.IERS2003_EARTH_EQUATORIAL_RADIUS}</td>
105      *     <td>{@link org.orekit.utils.Constants#IERS2003_EARTH_FLATTENING Constants.IERS2003_EARTH_FLATTENING}</td></tr>
106      * <tr><td style="background-color:#c9d5c9; padding:5px">IERS2010</td>
107      *     <td>{@link org.orekit.utils.Constants#IERS2010_EARTH_EQUATORIAL_RADIUS Constants.IERS2010_EARTH_EQUATORIAL_RADIUS}</td>
108      *     <td>{@link org.orekit.utils.Constants#IERS2010_EARTH_FLATTENING Constants.IERS2010_EARTH_FLATTENING}</td></tr>
109      * </table>
110      * @param ae equatorial radius
111      * @param f the flattening (f = (a-b)/a)
112      * @param bodyFrame body frame related to body shape
113      * @see org.orekit.frames.FramesFactory#getITRF(org.orekit.utils.IERSConventions, boolean)
114      */
115     public OneAxisEllipsoid(final double ae, final double f,
116                             final Frame bodyFrame) {
117         super(bodyFrame, ae, ae, ae * (1.0 - f));
118         this.f    = f;
119         this.ae2  = ae * ae;
120         this.e2   = f * (2.0 - f);
121         this.e    = FastMath.sqrt(e2);
122         this.g    = 1.0 - f;
123         this.g2   = g * g;
124         this.ap2  = ae2 * g2;
125         setAngularThreshold(1.0e-12);
126         this.bodyFrame = bodyFrame;
127     }
128 
129     /** Set the angular convergence threshold.
130      * <p>The angular threshold is used both to identify points close to
131      * the ellipse axes and as the convergence threshold used to
132      * stop the iterations in the {@link #transform(Vector3D, Frame,
133      * AbsoluteDate)} method.</p>
134      * <p>If this method is not called, the default value is set to
135      * 10<sup>-12</sup>.</p>
136      * @param angularThreshold angular convergence threshold (rad)
137      */
138     public void setAngularThreshold(final double angularThreshold) {
139         this.angularThreshold = angularThreshold;
140     }
141 
142     /** Get the equatorial radius of the body.
143      * @return equatorial radius of the body (m)
144      */
145     public double getEquatorialRadius() {
146         return getA();
147     }
148 
149     /** Get the flattening of the body: f = (a-b)/a.
150      * @return the flattening
151      */
152     public double getFlattening() {
153         return f;
154     }
155 
156     /** Get the first eccentricity squared of the ellipsoid: e^2 = f * (2.0 - f).
157      * @return the eccentricity squared
158      */
159     public double getEccentricitySquared() {
160         return e2;
161     }
162 
163     /** Get the first eccentricity of the ellipsoid: e = sqrt(f * (2.0 - f)).
164      * @return the eccentricity
165      */
166     public double getEccentricity() {
167         return e;
168     }
169 
170     /** {@inheritDoc} */
171     public Frame getBodyFrame() {
172         return bodyFrame;
173     }
174 
175     /** Get the intersection point of a line with the surface of the body.
176      * <p>A line may have several intersection points with a closed
177      * surface (we consider the one point case as a degenerated two
178      * points case). The close parameter is used to select which of
179      * these points should be returned. The selected point is the one
180      * that is closest to the close point.</p>
181      * @param line test line (may intersect the body or not)
182      * @param close point used for intersections selection
183      * @param frame frame in which line is expressed
184      * @param date date of the line in given frame
185      * @return intersection point at altitude zero or null if the line does
186      * not intersect the surface
187      * @since 9.3
188      */
189     public Vector3D getCartesianIntersectionPoint(final Line line, final Vector3D close,
190                                                   final Frame frame, final AbsoluteDate date) {
191 
192         // transform line and close to body frame
193         final StaticTransform frameToBodyFrame =
194                 frame.getStaticTransformTo(bodyFrame, date);
195         final Line lineInBodyFrame = frameToBodyFrame.transformLine(line);
196 
197         // compute some miscellaneous variables
198         final Vector3D point    = lineInBodyFrame.getOrigin();
199         final double x          = point.getX();
200         final double y          = point.getY();
201         final double z          = point.getZ();
202         final double z2         = z * z;
203         final double r2         = x * x + y * y;
204 
205         final Vector3D direction = lineInBodyFrame.getDirection();
206         final double dx         = direction.getX();
207         final double dy         = direction.getY();
208         final double dz         = direction.getZ();
209         final double cz2        = dx * dx + dy * dy;
210 
211         // abscissa of the intersection as a root of a 2nd degree polynomial :
212         // a k^2 - 2 b k + c = 0
213         final double a  = 1.0 - e2 * cz2;
214         final double b  = -(g2 * (x * dx + y * dy) + z * dz);
215         final double c  = g2 * (r2 - ae2) + z2;
216         final double b2 = b * b;
217         final double ac = a * c;
218         if (b2 < ac) {
219             return null;
220         }
221         final double s  = FastMath.sqrt(b2 - ac);
222         final double k1 = (b < 0) ? (b - s) / a : c / (b + s);
223         final double k2 = c / (a * k1);
224 
225         // select the right point
226         final Vector3D closeInBodyFrame = frameToBodyFrame.transformPosition(close);
227         final double   closeAbscissa    = lineInBodyFrame.getAbscissa(closeInBodyFrame);
228         final double k =
229             (FastMath.abs(k1 - closeAbscissa) < FastMath.abs(k2 - closeAbscissa)) ? k1 : k2;
230         return lineInBodyFrame.pointAt(k);
231 
232     }
233 
234     /** {@inheritDoc} */
235     public GeodeticPoint getIntersectionPoint(final Line line, final Vector3D close,
236                                               final Frame frame, final AbsoluteDate date) {
237 
238         final Vector3D intersection = getCartesianIntersectionPoint(line, close, frame, date);
239         if (intersection == null) {
240             return null;
241         }
242         final double ix = intersection.getX();
243         final double iy = intersection.getY();
244         final double iz = intersection.getZ();
245 
246         final double lambda = FastMath.atan2(iy, ix);
247         final double phi    = FastMath.atan2(iz, g2 * FastMath.sqrt(ix * ix + iy * iy));
248         return new GeodeticPoint(phi, lambda, 0.0);
249 
250     }
251 
252     /** Get the intersection point of a line with the surface of the body.
253      * <p>A line may have several intersection points with a closed
254      * surface (we consider the one point case as a degenerated two
255      * points case). The close parameter is used to select which of
256      * these points should be returned. The selected point is the one
257      * that is closest to the close point.</p>
258      * @param line test line (may intersect the body or not)
259      * @param close point used for intersections selection
260      * @param frame frame in which line is expressed
261      * @param date date of the line in given frame
262      * @param <T> type of the field elements
263      * @return intersection point at altitude zero or null if the line does
264      * not intersect the surface
265      * @since 9.3
266      */
267     public <T extends CalculusFieldElement<T>> FieldVector3D<T> getCartesianIntersectionPoint(final FieldLine<T> line,
268                                                                                           final FieldVector3D<T> close,
269                                                                                           final Frame frame,
270                                                                                           final FieldAbsoluteDate<T> date) {
271 
272         // transform line and close to body frame
273         final FieldTransform<T> frameToBodyFrame = frame.getTransformTo(bodyFrame, date);
274         final FieldLine<T>      lineInBodyFrame  = frameToBodyFrame.transformLine(line);
275 
276         // compute some miscellaneous variables
277         final FieldVector3D<T> point = lineInBodyFrame.getOrigin();
278         final T x  = point.getX();
279         final T y  = point.getY();
280         final T z  = point.getZ();
281         final T z2 = z.multiply(z);
282         final T r2 = x.multiply(x).add(y.multiply(y));
283 
284         final FieldVector3D<T> direction = lineInBodyFrame.getDirection();
285         final T dx  = direction.getX();
286         final T dy  = direction.getY();
287         final T dz  = direction.getZ();
288         final T cz2 = dx.multiply(dx).add(dy.multiply(dy));
289 
290         // abscissa of the intersection as a root of a 2nd degree polynomial :
291         // a k^2 - 2 b k + c = 0
292         final T a  = cz2.multiply(e2).subtract(1.0).negate();
293         final T b  = x.multiply(dx).add(y.multiply(dy)).multiply(g2).add(z.multiply(dz)).negate();
294         final T c  = r2.subtract(ae2).multiply(g2).add(z2);
295         final T b2 = b.multiply(b);
296         final T ac = a.multiply(c);
297         if (b2.getReal() < ac.getReal()) {
298             return null;
299         }
300         final T s  = b2.subtract(ac).sqrt();
301         final T k1 = (b.getReal() < 0) ? b.subtract(s).divide(a) : c.divide(b.add(s));
302         final T k2 = c.divide(a.multiply(k1));
303 
304         // select the right point
305         final FieldVector3D<T>  closeInBodyFrame = frameToBodyFrame.transformPosition(close);
306         final T                 closeAbscissa    = lineInBodyFrame.getAbscissa(closeInBodyFrame);
307         final T k = (FastMath.abs(k1.getReal() - closeAbscissa.getReal()) < FastMath.abs(k2.getReal() - closeAbscissa.getReal())) ?
308                     k1 : k2;
309         return lineInBodyFrame.pointAt(k);
310     }
311 
312     /** {@inheritDoc} */
313     public <T extends CalculusFieldElement<T>> FieldGeodeticPoint<T> getIntersectionPoint(final FieldLine<T> line,
314                                                                                       final FieldVector3D<T> close,
315                                                                                       final Frame frame,
316                                                                                       final FieldAbsoluteDate<T> date) {
317 
318         final FieldVector3D<T> intersection = getCartesianIntersectionPoint(line, close, frame, date);
319         if (intersection == null) {
320             return null;
321         }
322         final T ix = intersection.getX();
323         final T iy = intersection.getY();
324         final T iz = intersection.getZ();
325 
326         final T lambda = iy.atan2(ix);
327         final T phi    = iz.atan2(ix.multiply(ix).add(iy.multiply(iy)).sqrt().multiply(g2));
328         return new FieldGeodeticPoint<>(phi, lambda, phi.getField().getZero());
329 
330     }
331 
332     /** {@inheritDoc} */
333     public Vector3D transform(final GeodeticPoint point) {
334         final double longitude = point.getLongitude();
335         final SinCos scLambda  = FastMath.sinCos(longitude);
336         final double latitude  = point.getLatitude();
337         final SinCos scPhi     = FastMath.sinCos(latitude);
338         final double h         = point.getAltitude();
339         final double n         = getA() / FastMath.sqrt(1.0 - e2 * scPhi.sin() * scPhi.sin());
340         final double r         = (n + h) * scPhi.cos();
341         return new Vector3D(r * scLambda.cos(), r * scLambda.sin(), (g2 * n + h) * scPhi.sin());
342     }
343 
344     /** {@inheritDoc} */
345     public <T extends CalculusFieldElement<T>> FieldVector3D<T> transform(final FieldGeodeticPoint<T> point) {
346 
347         final T latitude  = point.getLatitude();
348         final T longitude = point.getLongitude();
349         final T altitude  = point.getAltitude();
350 
351         final FieldSinCos<T> scLambda = FastMath.sinCos(longitude);
352         final FieldSinCos<T> scPhi    = FastMath.sinCos(latitude);
353         final T cLambda = scLambda.cos();
354         final T sLambda = scLambda.sin();
355         final T cPhi    = scPhi.cos();
356         final T sPhi    = scPhi.sin();
357         final T n       = sPhi.multiply(sPhi).multiply(e2).subtract(1.0).negate().sqrt().reciprocal().multiply(getA());
358         final T r       = n.add(altitude).multiply(cPhi);
359 
360         return new FieldVector3D<>(r.multiply(cLambda),
361                                    r.multiply(sLambda),
362                                    sPhi.multiply(altitude.add(n.multiply(g2))));
363     }
364 
365     /** {@inheritDoc} */
366     public Vector3D projectToGround(final Vector3D point, final AbsoluteDate date, final Frame frame) {
367 
368         // transform point to body frame
369         final StaticTransform toBody = frame.getStaticTransformTo(bodyFrame, date);
370         final Vector3D   p         = toBody.transformPosition(point);
371         final double     z         = p.getZ();
372         final double     r         = FastMath.hypot(p.getX(), p.getY());
373 
374         // set up the 2D meridian ellipse
375         final Ellipse meridian = new Ellipse(Vector3D.ZERO,
376                                              r == 0 ? Vector3D.PLUS_I : new Vector3D(p.getX() / r, p.getY() / r, 0),
377                                              Vector3D.PLUS_K,
378                                              getA(), getC(), bodyFrame);
379 
380         // find the closest point in the meridian plane
381         final Vector3D groundPoint = meridian.toSpace(meridian.projectToEllipse(new Vector2D(r, z)));
382 
383         // transform point back to initial frame
384         return toBody.getInverse().transformPosition(groundPoint);
385 
386     }
387 
388     /** {@inheritDoc} */
389     public TimeStampedPVCoordinates projectToGround(final TimeStampedPVCoordinates pv, final Frame frame) {
390 
391         // transform point to body frame
392         final Transform                toBody        = frame.getTransformTo(bodyFrame, pv.getDate());
393         final TimeStampedPVCoordinates pvInBodyFrame = toBody.transformPVCoordinates(pv);
394         final Vector3D                 p             = pvInBodyFrame.getPosition();
395         final double                   r             = FastMath.hypot(p.getX(), p.getY());
396 
397         // set up the 2D ellipse corresponding to first principal curvature along meridian
398         final Vector3D meridian = r == 0 ? Vector3D.PLUS_I : new Vector3D(p.getX() / r, p.getY() / r, 0);
399         final Ellipse firstPrincipalCurvature =
400                 new Ellipse(Vector3D.ZERO, meridian, Vector3D.PLUS_K, getA(), getC(), bodyFrame);
401 
402         // project coordinates in the meridian plane
403         final TimeStampedPVCoordinates gpFirst = firstPrincipalCurvature.projectToEllipse(pvInBodyFrame);
404         final Vector3D                 gpP     = gpFirst.getPosition();
405         final double                   gr      = MathArrays.linearCombination(gpP.getX(), meridian.getX(),
406                                                                               gpP.getY(), meridian.getY());
407         final double                   gz      = gpP.getZ();
408 
409         // topocentric frame
410         final Vector3D east   = new Vector3D(-meridian.getY(), meridian.getX(), 0);
411         final Vector3D zenith = new Vector3D(gr * getC() / getA(), meridian, gz * getA() / getC(), Vector3D.PLUS_K).normalize();
412         final Vector3D north  = Vector3D.crossProduct(zenith, east);
413 
414         // set up the ellipse corresponding to second principal curvature in the zenith/east plane
415         final Ellipse secondPrincipalCurvature  = getPlaneSection(gpP, north);
416         final TimeStampedPVCoordinates gpSecond = secondPrincipalCurvature.projectToEllipse(pvInBodyFrame);
417 
418         final Vector3D gpV = gpFirst.getVelocity().add(gpSecond.getVelocity());
419         final Vector3D gpA = gpFirst.getAcceleration().add(gpSecond.getAcceleration());
420 
421         // moving projected point
422         final TimeStampedPVCoordinates groundPV =
423                 new TimeStampedPVCoordinates(pv.getDate(), gpP, gpV, gpA);
424 
425         // transform moving projected point back to initial frame
426         return toBody.getInverse().transformPVCoordinates(groundPV);
427 
428     }
429 
430     /** {@inheritDoc}
431      * <p>
432      * This method is based on Toshio Fukushima's algorithm which uses Halley's method.
433      * <a href="https://www.researchgate.net/publication/227215135_Transformation_from_Cartesian_to_Geodetic_Coordinates_Accelerated_by_Halley's_Method">
434      * transformation from Cartesian to Geodetic Coordinates Accelerated by Halley's Method</a>,
435      * Toshio Fukushima, Journal of Geodesy 9(12):689-693, February 2006
436      * </p>
437      * <p>
438      * Some changes have been added to the original method:
439      * <ul>
440      *   <li>in order to handle more accurately corner cases near the pole</li>
441      *   <li>in order to handle properly corner cases near the equatorial plane, even far inside the ellipsoid</li>
442      *   <li>in order to handle very flat ellipsoids</li>
443      * </ul>
444      */
445     public GeodeticPoint transform(final Vector3D point, final Frame frame, final AbsoluteDate date) {
446 
447         // transform point to body frame
448         final Vector3D pointInBodyFrame = frame.getStaticTransformTo(bodyFrame, date)
449                 .transformPosition(point);
450         final double   r2               = pointInBodyFrame.getX() * pointInBodyFrame.getX() +
451                                           pointInBodyFrame.getY() * pointInBodyFrame.getY();
452         final double   r                = FastMath.sqrt(r2);
453         final double   z                = pointInBodyFrame.getZ();
454 
455         final double   lambda           = FastMath.atan2(pointInBodyFrame.getY(), pointInBodyFrame.getX());
456 
457         double h;
458         double phi;
459         if (r <= ANGULAR_THRESHOLD * FastMath.abs(z)) {
460             // the point is almost on the polar axis, approximate the ellipsoid with
461             // the osculating sphere whose center is at evolute cusp along polar axis
462             final double osculatingRadius = ae2 / getC();
463             final double evoluteCuspZ     = FastMath.copySign(getA() * e2 / g, -z);
464             final double deltaZ           = z - evoluteCuspZ;
465             // we use π/2 - atan(r/Δz) instead of atan(Δz/r) for accuracy purposes, as r is much smaller than Δz
466             phi = FastMath.copySign(0.5 * FastMath.PI - FastMath.atan(r / FastMath.abs(deltaZ)), deltaZ);
467             h   = FastMath.hypot(deltaZ, r) - osculatingRadius;
468         } else if (FastMath.abs(z) <= ANGULAR_THRESHOLD * r) {
469             // the point is almost on the major axis
470 
471             final double osculatingRadius = ap2 / getA();
472             final double evoluteCuspR     = getA() * e2;
473             final double deltaR           = r - evoluteCuspR;
474             if (deltaR >= 0) {
475                 // the point is outside of the ellipse evolute, approximate the ellipse
476                 // with the osculating circle whose center is at evolute cusp along major axis
477                 phi = (deltaR == 0) ? 0.0 : FastMath.atan(z / deltaR);
478                 h   = FastMath.hypot(deltaR, z) - osculatingRadius;
479             } else {
480                 // the point is on the part of the major axis within ellipse evolute
481                 // we can compute the closest ellipse point analytically, and it is NOT near the equator
482                 final double rClose = r / e2;
483                 final double zClose = FastMath.copySign(g * FastMath.sqrt(ae2 - rClose * rClose), z);
484                 phi = FastMath.atan((zClose - z) / (rClose - r));
485                 h   = -FastMath.hypot(r - rClose, z - zClose);
486             }
487 
488         } else {
489             // use Toshio Fukushima method, with several iterations
490             final double epsPhi = 1.0e-15;
491             final double epsH   = 1.0e-14 * FastMath.max(getA(), FastMath.sqrt(r2 + z * z));
492             final double c     = getA() * e2;
493             final double absZ  = FastMath.abs(z);
494             final double zc    = g * absZ;
495             double sn  = absZ;
496             double sn2 = sn * sn;
497             double cn  = g * r;
498             double cn2 = cn * cn;
499             double an2 = cn2 + sn2;
500             double an  = FastMath.sqrt(an2);
501             double bn  = 0;
502             phi = Double.POSITIVE_INFINITY;
503             h   = Double.POSITIVE_INFINITY;
504             for (int i = 0; i < 10; ++i) { // this usually converges in 2 iterations
505                 final double oldSn  = sn;
506                 final double oldCn  = cn;
507                 final double oldPhi = phi;
508                 final double oldH   = h;
509                 final double an3    = an2 * an;
510                 final double csncn  = c * sn * cn;
511                 bn    = 1.5 * csncn * ((r * sn - zc * cn) * an - csncn);
512                 sn    = (zc * an3 + c * sn2 * sn) * an3 - bn * sn;
513                 cn    = (r  * an3 - c * cn2 * cn) * an3 - bn * cn;
514                 if (sn * oldSn < 0 || cn < 0) {
515                     // the Halley iteration went too far, we restrict it and iterate again
516                     while (sn * oldSn < 0 || cn < 0) {
517                         sn = (sn + oldSn) / 2;
518                         cn = (cn + oldCn) / 2;
519                     }
520                 } else {
521 
522                     // rescale components to avoid overflow when several iterations are used
523                     final int exp = (FastMath.getExponent(sn) + FastMath.getExponent(cn)) / 2;
524                     sn = FastMath.scalb(sn, -exp);
525                     cn = FastMath.scalb(cn, -exp);
526 
527                     sn2 = sn * sn;
528                     cn2 = cn * cn;
529                     an2 = cn2 + sn2;
530                     an  = FastMath.sqrt(an2);
531 
532                     final double cc = g * cn;
533                     h = (r * cc + absZ * sn - getA() * g * an) / FastMath.sqrt(an2 - e2 * cn2);
534                     if (FastMath.abs(oldH   - h)   < epsH) {
535                         phi = FastMath.copySign(FastMath.atan(sn / cc), z);
536                         if (FastMath.abs(oldPhi - phi) < epsPhi) {
537                             break;
538                         }
539                     }
540 
541                 }
542 
543             }
544         }
545 
546         return new GeodeticPoint(phi, lambda, h);
547 
548     }
549 
550     /** {@inheritDoc}
551      * <p>
552      * This method is based on Toshio Fukushima's algorithm which uses Halley's method.
553      * <a href="https://www.researchgate.net/publication/227215135_Transformation_from_Cartesian_to_Geodetic_Coordinates_Accelerated_by_Halley's_Method">
554      * transformation from Cartesian to Geodetic Coordinates Accelerated by Halley's Method</a>,
555      * Toshio Fukushima, Journal of Geodesy 9(12):689-693, February 2006
556      * </p>
557      * <p>
558      * Some changes have been added to the original method:
559      * <ul>
560      *   <li>in order to handle more accurately corner cases near the pole</li>
561      *   <li>in order to handle properly corner cases near the equatorial plane, even far inside the ellipsoid</li>
562      *   <li>in order to handle very flat ellipsoids</li>
563      * </ul>
564      */
565     public <T extends CalculusFieldElement<T>> FieldGeodeticPoint<T> transform(final FieldVector3D<T> point,
566                                                                            final Frame frame,
567                                                                            final FieldAbsoluteDate<T> date) {
568 
569         // transform point to body frame
570         final FieldVector3D<T> pointInBodyFrame = frame.getTransformTo(bodyFrame, date).transformPosition(point);
571         final T   r2                            = pointInBodyFrame.getX().multiply(pointInBodyFrame.getX()).
572                                               add(pointInBodyFrame.getY().multiply(pointInBodyFrame.getY()));
573         final T   r                             = r2.sqrt();
574         final T   z                             = pointInBodyFrame.getZ();
575 
576         final T   lambda                        = pointInBodyFrame.getY().atan2(pointInBodyFrame.getX());
577 
578         T h;
579         T phi;
580         if (r.getReal() <= ANGULAR_THRESHOLD * FastMath.abs(z.getReal())) {
581             // the point is almost on the polar axis, approximate the ellipsoid with
582             // the osculating sphere whose center is at evolute cusp along polar axis
583             final double osculatingRadius = ae2 / getC();
584             final double evoluteCuspZ     = FastMath.copySign(getA() * e2 / g, -z.getReal());
585             final T      deltaZ           = z.subtract(evoluteCuspZ);
586             // we use π/2 - atan(r/Δz) instead of atan(Δz/r) for accuracy purposes, as r is much smaller than Δz
587             phi = r.divide(deltaZ.abs()).atan().negate().add(r.getPi().multiply(0.5)).copySign(deltaZ);
588             h   = deltaZ.hypot(r).subtract(osculatingRadius);
589         } else if (FastMath.abs(z.getReal()) <= ANGULAR_THRESHOLD * r.getReal()) {
590             // the point is almost on the major axis
591 
592             final double osculatingRadius = ap2 / getA();
593             final double evoluteCuspR     = getA() * e2;
594             final T      deltaR           = r.subtract(evoluteCuspR);
595             if (deltaR.getReal() >= 0) {
596                 // the point is outside of the ellipse evolute, approximate the ellipse
597                 // with the osculating circle whose center is at evolute cusp along major axis
598                 phi = (deltaR.getReal() == 0) ? z.getField().getZero() : z.divide(deltaR).atan();
599                 h   = deltaR.hypot(z).subtract(osculatingRadius);
600             } else {
601                 // the point is on the part of the major axis within ellipse evolute
602                 // we can compute the closest ellipse point analytically, and it is NOT near the equator
603                 final T rClose = r.divide(e2);
604                 final T zClose = rClose.multiply(rClose).negate().add(ae2).sqrt().multiply(g).copySign(z);
605                 phi = zClose.subtract(z).divide(rClose.subtract(r)).atan();
606                 h   = r.subtract(rClose).hypot(z.subtract(zClose)).negate();
607             }
608 
609         } else {
610             // use Toshio Fukushima method, with several iterations
611             final double epsPhi = 1.0e-15;
612             final double epsH   = 1.0e-14 * getA();
613             final double c      = getA() * e2;
614             final T      absZ   = z.abs();
615             final T      zc     = absZ.multiply(g);
616             T            sn     = absZ;
617             T            sn2    = sn.multiply(sn);
618             T            cn     = r.multiply(g);
619             T            cn2    = cn.multiply(cn);
620             T            an2    = cn2.add(sn2);
621             T            an     = an2.sqrt();
622             T            bn     = an.getField().getZero();
623             phi = an.getField().getZero().add(Double.POSITIVE_INFINITY);
624             h   = an.getField().getZero().add(Double.POSITIVE_INFINITY);
625             for (int i = 0; i < 10; ++i) { // this usually converges in 2 iterations
626                 final T oldSn  = sn;
627                 final T oldCn  = cn;
628                 final T oldPhi = phi;
629                 final T oldH   = h;
630                 final T an3    = an2.multiply(an);
631                 final T csncn  = sn.multiply(cn).multiply(c);
632                 bn    = csncn.multiply(1.5).multiply((r.multiply(sn).subtract(zc.multiply(cn))).multiply(an).subtract(csncn));
633                 sn    = zc.multiply(an3).add(sn2.multiply(sn).multiply(c)).multiply(an3).subtract(bn.multiply(sn));
634                 cn    = r.multiply(an3).subtract(cn2.multiply(cn).multiply(c)).multiply(an3).subtract(bn.multiply(cn));
635                 if (sn.getReal() * oldSn.getReal() < 0 || cn.getReal() < 0) {
636                     // the Halley iteration went too far, we restrict it and iterate again
637                     while (sn.getReal() * oldSn.getReal() < 0 || cn.getReal() < 0) {
638                         sn = sn.add(oldSn).multiply(0.5);
639                         cn = cn.add(oldCn).multiply(0.5);
640                     }
641                 } else {
642 
643                     // rescale components to avoid overflow when several iterations are used
644                     final int exp = (FastMath.getExponent(sn.getReal()) + FastMath.getExponent(cn.getReal())) / 2;
645                     sn = sn.scalb(-exp);
646                     cn = cn.scalb(-exp);
647 
648                     sn2 = sn.multiply(sn);
649                     cn2 = cn.multiply(cn);
650                     an2 = cn2.add(sn2);
651                     an  = an2.sqrt();
652 
653                     final T cc = cn.multiply(g);
654                     h = r.multiply(cc).add(absZ.multiply(sn)).subtract(an.multiply(getA() * g)).divide(an2.subtract(cn2.multiply(e2)).sqrt());
655                     if (FastMath.abs(oldH.getReal()  - h.getReal())   < epsH) {
656                         phi = sn.divide(cc).atan().copySign(z);
657                         if (FastMath.abs(oldPhi.getReal() - phi.getReal()) < epsPhi) {
658                             break;
659                         }
660                     }
661 
662                 }
663 
664             }
665         }
666 
667         return new FieldGeodeticPoint<>(phi, lambda, h);
668 
669     }
670 
671     /** Transform a Cartesian point to a surface-relative point.
672      * @param point Cartesian point
673      * @param frame frame in which Cartesian point is expressed
674      * @param date date of the computation (used for frames conversions)
675      * @return point at the same location but as a surface-relative point,
676      * using time as the single derivation parameter
677      */
678     public FieldGeodeticPoint<DerivativeStructure> transform(final PVCoordinates point,
679                                                              final Frame frame, final AbsoluteDate date) {
680 
681         // transform point to body frame
682         final Transform toBody = frame.getTransformTo(bodyFrame, date);
683         final PVCoordinates pointInBodyFrame = toBody.transformPVCoordinates(point);
684         final FieldVector3D<DerivativeStructure> p = pointInBodyFrame.toDerivativeStructureVector(2);
685         final DerivativeStructure   pr2 = p.getX().multiply(p.getX()).add(p.getY().multiply(p.getY()));
686         final DerivativeStructure   pr  = pr2.sqrt();
687         final DerivativeStructure   pz  = p.getZ();
688 
689         // project point on the ellipsoid surface
690         final TimeStampedPVCoordinates groundPoint = projectToGround(new TimeStampedPVCoordinates(date, pointInBodyFrame),
691                                                                      bodyFrame);
692         final FieldVector3D<DerivativeStructure> gp = groundPoint.toDerivativeStructureVector(2);
693         final DerivativeStructure   gpr2 = gp.getX().multiply(gp.getX()).add(gp.getY().multiply(gp.getY()));
694         final DerivativeStructure   gpr  = gpr2.sqrt();
695         final DerivativeStructure   gpz  = gp.getZ();
696 
697         // relative position of test point with respect to its ellipse sub-point
698         final DerivativeStructure dr  = pr.subtract(gpr);
699         final DerivativeStructure dz  = pz.subtract(gpz);
700         final double insideIfNegative = g2 * (pr2.getReal() - ae2) + pz.getReal() * pz.getReal();
701 
702         return new FieldGeodeticPoint<>(DerivativeStructure.atan2(gpz, gpr.multiply(g2)),
703                                                                   DerivativeStructure.atan2(p.getY(), p.getX()),
704                                                                   DerivativeStructure.hypot(dr, dz).copySign(insideIfNegative));
705     }
706 
707     /** Compute the azimuth angle from local north between the two points.
708      *
709      * The angle is calculated clockwise from local north at the origin point
710      * and follows the rhumb line to the destination point.
711      *
712      * @param origin the origin point, at which the azimuth angle will be computed (non-{@code null})
713      * @param destination the destination point, to which the angle is defined (non-{@code null})
714      * @return the resulting azimuth angle (radians, {@code [0-2pi)})
715      * @since 11.3
716      */
717     public double azimuthBetweenPoints(final GeodeticPoint origin, final GeodeticPoint destination) {
718         final double dLon = MathUtils.normalizeAngle(destination.getLongitude(), origin.getLongitude()) - origin.getLongitude();
719         final double originIsoLat = geodeticToIsometricLatitude(origin.getLatitude());
720         final double destIsoLat = geodeticToIsometricLatitude(destination.getLatitude());
721 
722         final double az = FastMath.atan2(dLon, destIsoLat - originIsoLat);
723         if (az < 0.) {
724             return az + MathUtils.TWO_PI;
725         }
726         return az;
727     }
728 
729     /** Compute the azimuth angle from local north between the two points.
730      *
731      * The angle is calculated clockwise from local north at the origin point
732      * and follows the rhumb line to the destination point.
733      *
734      * @param origin the origin point, at which the azimuth angle will be computed (non-{@code null})
735      * @param destination the destination point, to which the angle is defined (non-{@code null})
736      * @param <T> the type of field elements
737      * @return the resulting azimuth angle (radians, {@code [0-2pi)})
738      * @since 11.3
739      */
740     public <T extends CalculusFieldElement<T>> T azimuthBetweenPoints(final FieldGeodeticPoint<T> origin, final FieldGeodeticPoint<T> destination) {
741         final T dLon = MathUtils.normalizeAngle(destination.getLongitude().subtract(origin.getLongitude()), origin.getLongitude().getField().getZero());
742         final T originIsoLat = geodeticToIsometricLatitude(origin.getLatitude());
743         final T destIsoLat = geodeticToIsometricLatitude(destination.getLatitude());
744 
745         final T az = FastMath.atan2(dLon, destIsoLat.subtract(originIsoLat));
746         if (az.getReal() < 0.) {
747             return az.add(az.getPi().multiply(2));
748         }
749         return az;
750     }
751 
752     /** Compute the <a href="https://mathworld.wolfram.com/IsometricLatitude.html">isometric latitude</a>
753      *  corresponding to the provided latitude.
754      *
755      * @param geodeticLatitude the latitude (radians, within interval {@code [-pi/2, +pi/2]})
756      * @return the isometric latitude (radians)
757      * @since 11.3
758      */
759     public double geodeticToIsometricLatitude(final double geodeticLatitude) {
760         if (FastMath.abs(geodeticLatitude) <= angularThreshold) {
761             return 0.;
762         }
763 
764         final double eSinLat = e * FastMath.sin(geodeticLatitude);
765 
766         // first term: ln(tan(pi/4 + lat/2))
767         final double a = FastMath.log(FastMath.tan(FastMath.PI / 4. + geodeticLatitude / 2.));
768         // second term: (ecc / 2) * ln((1 - ecc*sin(lat)) / (1 + ecc * sin(lat)))
769         final double b = (e / 2.) * FastMath.log((1. - eSinLat) / (1. + eSinLat));
770 
771         return a + b;
772     }
773 
774     /** Compute the <a href="https://mathworld.wolfram.com/IsometricLatitude.html">isometric latitude</a>
775      *  corresponding to the provided latitude.
776      *
777      * @param geodeticLatitude the latitude (radians, within interval {@code [-pi/2, +pi/2]})
778      * @param <T> the type of field elements
779      * @return the isometric latitude (radians)
780      * @since 11.3
781      */
782     public <T extends CalculusFieldElement<T>> T geodeticToIsometricLatitude(final T geodeticLatitude) {
783         if (geodeticLatitude.abs().getReal() <= angularThreshold) {
784             return geodeticLatitude.getField().getZero();
785         }
786         final Field<T> field = geodeticLatitude.getField();
787         final T ecc = geodeticLatitude.newInstance(e);
788         final T eSinLat = ecc.multiply(geodeticLatitude.sin());
789 
790         // first term: ln(tan(pi/4 + lat/2))
791         final T a = FastMath.log(FastMath.tan(geodeticLatitude.getPi().divide(4.).add(geodeticLatitude.divide(2.))));
792         // second term: (ecc / 2) * ln((1 - ecc*sin(lat)) / (1 + ecc * sin(lat)))
793         final T b = ecc.divide(2.).multiply(FastMath.log(field.getOne().subtract(eSinLat).divide(field.getOne().add(eSinLat))));
794 
795         return a.add(b);
796     }
797 
798     /** Replace the instance with a data transfer object for serialization.
799      * <p>
800      * This intermediate class serializes the files supported names, the
801      * ephemeris type and the body name.
802      * </p>
803      * @return data transfer object that will be serialized
804      */
805     private Object writeReplace() {
806         return new DataTransferObject(getA(), f, bodyFrame, angularThreshold);
807     }
808 
809     /** Internal class used only for serialization. */
810     private static class DataTransferObject implements Serializable {
811 
812         /** Serializable UID. */
813         private static final long serialVersionUID = 20130518L;
814 
815         /** Equatorial radius. */
816         private final double ae;
817 
818         /** Flattening. */
819         private final double f;
820 
821         /** Body frame related to body shape. */
822         private final Frame bodyFrame;
823 
824         /** Convergence limit. */
825         private final double angularThreshold;
826 
827         /** Simple constructor.
828          * @param ae equatorial radius
829          * @param f the flattening (f = (a-b)/a)
830          * @param bodyFrame body frame related to body shape
831          * @param angularThreshold convergence limit
832          */
833         DataTransferObject(final double ae, final double f,
834                                   final Frame bodyFrame, final double angularThreshold) {
835             this.ae               = ae;
836             this.f                = f;
837             this.bodyFrame        = bodyFrame;
838             this.angularThreshold = angularThreshold;
839         }
840 
841         /** Replace the deserialized data transfer object with a
842          * {@link JPLCelestialBody}.
843          * @return replacement {@link JPLCelestialBody}
844          */
845         private Object readResolve() {
846             final OneAxisEllipsoid ellipsoid = new OneAxisEllipsoid(ae, f, bodyFrame);
847             ellipsoid.setAngularThreshold(angularThreshold);
848             return ellipsoid;
849         }
850 
851     }
852 
853 }