OneAxisEllipsoid.java

  1. /* Copyright 2002-2024 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. import java.io.Serializable;
  19. import java.util.function.DoubleFunction;

  20. import org.hipparchus.CalculusFieldElement;
  21. import org.hipparchus.Field;
  22. import org.hipparchus.analysis.differentiation.DerivativeStructure;
  23. import org.hipparchus.analysis.solvers.BracketingNthOrderBrentSolver;
  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.FieldStaticTransform;
  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. /** Modeling of a one-axis ellipsoid.

  43.  * <p>One-axis ellipsoids is a good approximate model for most planet-size
  44.  * and larger natural bodies. It is the equilibrium shape reached by
  45.  * a fluid body under its own gravity field when it rotates. The symmetry
  46.  * axis is the rotation or polar axis.</p>

  47.  * @author Luc Maisonobe
  48.  * @author Guylaine Prat
  49.  */
  50. public class OneAxisEllipsoid extends Ellipsoid implements BodyShape {

  51.     /** Serializable UID. */
  52.     private static final long serialVersionUID = 20130518L;

  53.     /** Threshold for polar and equatorial points detection. */
  54.     private static final double ANGULAR_THRESHOLD = 1.0e-4;

  55.     /** Body frame related to body shape. */
  56.     private final Frame bodyFrame;

  57.     /** Equatorial radius power 2. */
  58.     private final double ae2;

  59.     /** Polar radius power 2. */
  60.     private final double ap2;

  61.     /** Flattening. */
  62.     private final double f;

  63.     /** Eccentricity. */
  64.     private final double e;

  65.     /** Eccentricity squared. */
  66.     private final double e2;

  67.     /** 1 minus flatness. */
  68.     private final double g;

  69.     /** g squared. */
  70.     private final double g2;

  71.     /** Convergence limit. */
  72.     private double angularThreshold;

  73.     /** Simple constructor.
  74.      * <p>Standard values for Earth models can be found in the {@link org.orekit.utils.Constants Constants} class:</p>
  75.      * <table border="1" style="background-color:#f5f5dc;">
  76.      * <caption>Ellipsoid Models</caption>
  77.      * <tr style="background-color:#c9d5c9;"><th>model</th><th>a<sub>e</sub> (m)</th> <th>f</th></tr>
  78.      * <tr><td style="background-color:#c9d5c9; padding:5px">GRS 80</td>
  79.      *     <td>{@link org.orekit.utils.Constants#GRS80_EARTH_EQUATORIAL_RADIUS Constants.GRS80_EARTH_EQUATORIAL_RADIUS}</td>
  80.      *     <td>{@link org.orekit.utils.Constants#GRS80_EARTH_FLATTENING Constants.GRS80_EARTH_FLATTENING}</td></tr>
  81.      * <tr><td style="background-color:#c9d5c9; padding:5px">WGS84</td>
  82.      *     <td>{@link org.orekit.utils.Constants#WGS84_EARTH_EQUATORIAL_RADIUS Constants.WGS84_EARTH_EQUATORIAL_RADIUS}</td>
  83.      *     <td>{@link org.orekit.utils.Constants#WGS84_EARTH_FLATTENING Constants.WGS84_EARTH_FLATTENING}</td></tr>
  84.      * <tr><td style="background-color:#c9d5c9; padding:5px">IERS96</td>
  85.      *     <td>{@link org.orekit.utils.Constants#IERS96_EARTH_EQUATORIAL_RADIUS Constants.IERS96_EARTH_EQUATORIAL_RADIUS}</td>
  86.      *     <td>{@link org.orekit.utils.Constants#IERS96_EARTH_FLATTENING Constants.IERS96_EARTH_FLATTENING}</td></tr>
  87.      * <tr><td style="background-color:#c9d5c9; padding:5px">IERS2003</td>
  88.      *     <td>{@link org.orekit.utils.Constants#IERS2003_EARTH_EQUATORIAL_RADIUS Constants.IERS2003_EARTH_EQUATORIAL_RADIUS}</td>
  89.      *     <td>{@link org.orekit.utils.Constants#IERS2003_EARTH_FLATTENING Constants.IERS2003_EARTH_FLATTENING}</td></tr>
  90.      * <tr><td style="background-color:#c9d5c9; padding:5px">IERS2010</td>
  91.      *     <td>{@link org.orekit.utils.Constants#IERS2010_EARTH_EQUATORIAL_RADIUS Constants.IERS2010_EARTH_EQUATORIAL_RADIUS}</td>
  92.      *     <td>{@link org.orekit.utils.Constants#IERS2010_EARTH_FLATTENING Constants.IERS2010_EARTH_FLATTENING}</td></tr>
  93.      * </table>
  94.      * @param ae equatorial radius
  95.      * @param f the flattening (f = (a-b)/a)
  96.      * @param bodyFrame body frame related to body shape
  97.      * @see org.orekit.frames.FramesFactory#getITRF(org.orekit.utils.IERSConventions, boolean)
  98.      */
  99.     public OneAxisEllipsoid(final double ae, final double f,
  100.                             final Frame bodyFrame) {
  101.         super(bodyFrame, ae, ae, ae * (1.0 - f));
  102.         this.f    = f;
  103.         this.ae2  = ae * ae;
  104.         this.e2   = f * (2.0 - f);
  105.         this.e    = FastMath.sqrt(e2);
  106.         this.g    = 1.0 - f;
  107.         this.g2   = g * g;
  108.         this.ap2  = ae2 * g2;
  109.         setAngularThreshold(1.0e-12);
  110.         this.bodyFrame = bodyFrame;
  111.     }

  112.     /** Set the angular convergence threshold.
  113.      * <p>The angular threshold is used both to identify points close to
  114.      * the ellipse axes and as the convergence threshold used to
  115.      * stop the iterations in the {@link #transform(Vector3D, Frame,
  116.      * AbsoluteDate)} method.</p>
  117.      * <p>If this method is not called, the default value is set to
  118.      * 10<sup>-12</sup>.</p>
  119.      * @param angularThreshold angular convergence threshold (rad)
  120.      */
  121.     public void setAngularThreshold(final double angularThreshold) {
  122.         this.angularThreshold = angularThreshold;
  123.     }

  124.     /** Get the equatorial radius of the body.
  125.      * @return equatorial radius of the body (m)
  126.      */
  127.     public double getEquatorialRadius() {
  128.         return getA();
  129.     }

  130.     /** Get the flattening of the body: f = (a-b)/a.
  131.      * @return the flattening
  132.      */
  133.     public double getFlattening() {
  134.         return f;
  135.     }

  136.     /** Get the first eccentricity squared of the ellipsoid: e^2 = f * (2.0 - f).
  137.      * @return the eccentricity squared
  138.      */
  139.     public double getEccentricitySquared() {
  140.         return e2;
  141.     }

  142.     /** Get the first eccentricity of the ellipsoid: e = sqrt(f * (2.0 - f)).
  143.      * @return the eccentricity
  144.      */
  145.     public double getEccentricity() {
  146.         return e;
  147.     }

  148.     /** {@inheritDoc} */
  149.     public Frame getBodyFrame() {
  150.         return bodyFrame;
  151.     }

  152.     /** Get the intersection point of a line with the surface of the body.
  153.      * <p>A line may have several intersection points with a closed
  154.      * surface (we consider the one point case as a degenerated two
  155.      * points case). The close parameter is used to select which of
  156.      * these points should be returned. The selected point is the one
  157.      * that is closest to the close point.</p>
  158.      * @param line test line (may intersect the body or not)
  159.      * @param close point used for intersections selection
  160.      * @param frame frame in which line is expressed
  161.      * @param date date of the line in given frame
  162.      * @return intersection point at altitude zero or null if the line does
  163.      * not intersect the surface
  164.      * @since 9.3
  165.      */
  166.     public Vector3D getCartesianIntersectionPoint(final Line line, final Vector3D close,
  167.                                                   final Frame frame, final AbsoluteDate date) {

  168.         // transform line and close to body frame
  169.         final StaticTransform frameToBodyFrame =
  170.                 frame.getStaticTransformTo(bodyFrame, date);
  171.         final Line lineInBodyFrame = frameToBodyFrame.transformLine(line);

  172.         // compute some miscellaneous variables
  173.         final Vector3D point    = lineInBodyFrame.getOrigin();
  174.         final double x          = point.getX();
  175.         final double y          = point.getY();
  176.         final double z          = point.getZ();
  177.         final double z2         = z * z;
  178.         final double r2         = x * x + y * y;

  179.         final Vector3D direction = lineInBodyFrame.getDirection();
  180.         final double dx         = direction.getX();
  181.         final double dy         = direction.getY();
  182.         final double dz         = direction.getZ();
  183.         final double cz2        = dx * dx + dy * dy;

  184.         // abscissa of the intersection as a root of a 2nd degree polynomial :
  185.         // a k^2 - 2 b k + c = 0
  186.         final double a  = 1.0 - e2 * cz2;
  187.         final double b  = -(g2 * (x * dx + y * dy) + z * dz);
  188.         final double c  = g2 * (r2 - ae2) + z2;
  189.         final double b2 = b * b;
  190.         final double ac = a * c;
  191.         if (b2 < ac) {
  192.             return null;
  193.         }
  194.         final double s  = FastMath.sqrt(b2 - ac);
  195.         final double k1 = (b < 0) ? (b - s) / a : c / (b + s);
  196.         final double k2 = c / (a * k1);

  197.         // select the right point
  198.         final Vector3D closeInBodyFrame = frameToBodyFrame.transformPosition(close);
  199.         final double   closeAbscissa    = lineInBodyFrame.getAbscissa(closeInBodyFrame);
  200.         final double k =
  201.             (FastMath.abs(k1 - closeAbscissa) < FastMath.abs(k2 - closeAbscissa)) ? k1 : k2;
  202.         return lineInBodyFrame.pointAt(k);

  203.     }

  204.     /** {@inheritDoc} */
  205.     public GeodeticPoint getIntersectionPoint(final Line line, final Vector3D close,
  206.                                               final Frame frame, final AbsoluteDate date) {

  207.         final Vector3D intersection = getCartesianIntersectionPoint(line, close, frame, date);
  208.         if (intersection == null) {
  209.             return null;
  210.         }
  211.         final double ix = intersection.getX();
  212.         final double iy = intersection.getY();
  213.         final double iz = intersection.getZ();

  214.         final double lambda = FastMath.atan2(iy, ix);
  215.         final double phi    = FastMath.atan2(iz, g2 * FastMath.sqrt(ix * ix + iy * iy));
  216.         return new GeodeticPoint(phi, lambda, 0.0);

  217.     }

  218.     /** Get the intersection point of a line with the surface of the body.
  219.      * <p>A line may have several intersection points with a closed
  220.      * surface (we consider the one point case as a degenerated two
  221.      * points case). The close parameter is used to select which of
  222.      * these points should be returned. The selected point is the one
  223.      * that is closest to the close point.</p>
  224.      * @param line test line (may intersect the body or not)
  225.      * @param close point used for intersections selection
  226.      * @param frame frame in which line is expressed
  227.      * @param date date of the line in given frame
  228.      * @param <T> type of the field elements
  229.      * @return intersection point at altitude zero or null if the line does
  230.      * not intersect the surface
  231.      * @since 9.3
  232.      */
  233.     public <T extends CalculusFieldElement<T>> FieldVector3D<T> getCartesianIntersectionPoint(final FieldLine<T> line,
  234.                                                                                           final FieldVector3D<T> close,
  235.                                                                                           final Frame frame,
  236.                                                                                           final FieldAbsoluteDate<T> date) {

  237.         // transform line and close to body frame
  238.         final FieldStaticTransform<T> frameToBodyFrame = frame.getStaticTransformTo(bodyFrame, date);
  239.         final FieldLine<T>            lineInBodyFrame  = frameToBodyFrame.transformLine(line);

  240.         // compute some miscellaneous variables
  241.         final FieldVector3D<T> point = lineInBodyFrame.getOrigin();
  242.         final T x  = point.getX();
  243.         final T y  = point.getY();
  244.         final T z  = point.getZ();
  245.         final T z2 = z.square();
  246.         final T r2 = x.square().add(y.square());

  247.         final FieldVector3D<T> direction = lineInBodyFrame.getDirection();
  248.         final T dx  = direction.getX();
  249.         final T dy  = direction.getY();
  250.         final T dz  = direction.getZ();
  251.         final T cz2 = dx.square().add(dy.square());

  252.         // abscissa of the intersection as a root of a 2nd degree polynomial :
  253.         // a k^2 - 2 b k + c = 0
  254.         final T a  = cz2.multiply(e2).subtract(1.0).negate();
  255.         final T b  = x.multiply(dx).add(y.multiply(dy)).multiply(g2).add(z.multiply(dz)).negate();
  256.         final T c  = r2.subtract(ae2).multiply(g2).add(z2);
  257.         final T b2 = b.square();
  258.         final T ac = a.multiply(c);
  259.         if (b2.getReal() < ac.getReal()) {
  260.             return null;
  261.         }
  262.         final T s  = b2.subtract(ac).sqrt();
  263.         final T k1 = (b.getReal() < 0) ? b.subtract(s).divide(a) : c.divide(b.add(s));
  264.         final T k2 = c.divide(a.multiply(k1));

  265.         // select the right point
  266.         final FieldVector3D<T>  closeInBodyFrame = frameToBodyFrame.transformPosition(close);
  267.         final T                 closeAbscissa    = lineInBodyFrame.getAbscissa(closeInBodyFrame);
  268.         final T k = (FastMath.abs(k1.getReal() - closeAbscissa.getReal()) < FastMath.abs(k2.getReal() - closeAbscissa.getReal())) ?
  269.                     k1 : k2;
  270.         return lineInBodyFrame.pointAt(k);
  271.     }

  272.     /** {@inheritDoc} */
  273.     public <T extends CalculusFieldElement<T>> FieldGeodeticPoint<T> getIntersectionPoint(final FieldLine<T> line,
  274.                                                                                       final FieldVector3D<T> close,
  275.                                                                                       final Frame frame,
  276.                                                                                       final FieldAbsoluteDate<T> date) {

  277.         final FieldVector3D<T> intersection = getCartesianIntersectionPoint(line, close, frame, date);
  278.         if (intersection == null) {
  279.             return null;
  280.         }
  281.         final T ix = intersection.getX();
  282.         final T iy = intersection.getY();
  283.         final T iz = intersection.getZ();

  284.         final T lambda = iy.atan2(ix);
  285.         final T phi    = iz.atan2(ix.multiply(ix).add(iy.multiply(iy)).sqrt().multiply(g2));
  286.         return new FieldGeodeticPoint<>(phi, lambda, phi.getField().getZero());

  287.     }

  288.     /** {@inheritDoc} */
  289.     public Vector3D transform(final GeodeticPoint point) {
  290.         final double longitude = point.getLongitude();
  291.         final SinCos scLambda  = FastMath.sinCos(longitude);
  292.         final double latitude  = point.getLatitude();
  293.         final SinCos scPhi     = FastMath.sinCos(latitude);
  294.         final double h         = point.getAltitude();
  295.         final double n         = getA() / FastMath.sqrt(1.0 - e2 * scPhi.sin() * scPhi.sin());
  296.         final double r         = (n + h) * scPhi.cos();
  297.         return new Vector3D(r * scLambda.cos(), r * scLambda.sin(), (g2 * n + h) * scPhi.sin());
  298.     }

  299.     /** {@inheritDoc} */
  300.     public <T extends CalculusFieldElement<T>> FieldVector3D<T> transform(final FieldGeodeticPoint<T> point) {

  301.         final T latitude  = point.getLatitude();
  302.         final T longitude = point.getLongitude();
  303.         final T altitude  = point.getAltitude();

  304.         final FieldSinCos<T> scLambda = FastMath.sinCos(longitude);
  305.         final FieldSinCos<T> scPhi    = FastMath.sinCos(latitude);
  306.         final T cLambda = scLambda.cos();
  307.         final T sLambda = scLambda.sin();
  308.         final T cPhi    = scPhi.cos();
  309.         final T sPhi    = scPhi.sin();
  310.         final T n       = sPhi.multiply(sPhi).multiply(e2).subtract(1.0).negate().sqrt().reciprocal().multiply(getA());
  311.         final T r       = n.add(altitude).multiply(cPhi);

  312.         return new FieldVector3D<>(r.multiply(cLambda),
  313.                                    r.multiply(sLambda),
  314.                                    sPhi.multiply(altitude.add(n.multiply(g2))));
  315.     }

  316.     /** {@inheritDoc} */
  317.     public Vector3D projectToGround(final Vector3D point, final AbsoluteDate date, final Frame frame) {

  318.         // transform point to body frame
  319.         final StaticTransform toBody = frame.getStaticTransformTo(bodyFrame, date);
  320.         final Vector3D   p         = toBody.transformPosition(point);
  321.         final double     z         = p.getZ();
  322.         final double     r         = FastMath.hypot(p.getX(), p.getY());

  323.         // set up the 2D meridian ellipse
  324.         final Ellipse meridian = new Ellipse(Vector3D.ZERO,
  325.                                              r == 0 ? Vector3D.PLUS_I : new Vector3D(p.getX() / r, p.getY() / r, 0),
  326.                                              Vector3D.PLUS_K,
  327.                                              getA(), getC(), bodyFrame);

  328.         // find the closest point in the meridian plane
  329.         final Vector3D groundPoint = meridian.toSpace(meridian.projectToEllipse(new Vector2D(r, z)));

  330.         // transform point back to initial frame
  331.         return toBody.getInverse().transformPosition(groundPoint);

  332.     }

  333.     /** {@inheritDoc} */
  334.     public TimeStampedPVCoordinates projectToGround(final TimeStampedPVCoordinates pv, final Frame frame) {

  335.         // transform point to body frame
  336.         final Transform                toBody        = frame.getTransformTo(bodyFrame, pv.getDate());
  337.         final TimeStampedPVCoordinates pvInBodyFrame = toBody.transformPVCoordinates(pv);
  338.         final Vector3D                 p             = pvInBodyFrame.getPosition();
  339.         final double                   r             = FastMath.hypot(p.getX(), p.getY());

  340.         // set up the 2D ellipse corresponding to first principal curvature along meridian
  341.         final Vector3D meridian = r == 0 ? Vector3D.PLUS_I : new Vector3D(p.getX() / r, p.getY() / r, 0);
  342.         final Ellipse firstPrincipalCurvature =
  343.                 new Ellipse(Vector3D.ZERO, meridian, Vector3D.PLUS_K, getA(), getC(), bodyFrame);

  344.         // project coordinates in the meridian plane
  345.         final TimeStampedPVCoordinates gpFirst = firstPrincipalCurvature.projectToEllipse(pvInBodyFrame);
  346.         final Vector3D                 gpP     = gpFirst.getPosition();
  347.         final double                   gr      = MathArrays.linearCombination(gpP.getX(), meridian.getX(),
  348.                                                                               gpP.getY(), meridian.getY());
  349.         final double                   gz      = gpP.getZ();

  350.         // topocentric frame
  351.         final Vector3D east   = new Vector3D(-meridian.getY(), meridian.getX(), 0);
  352.         final Vector3D zenith = new Vector3D(gr * getC() / getA(), meridian, gz * getA() / getC(), Vector3D.PLUS_K).normalize();
  353.         final Vector3D north  = Vector3D.crossProduct(zenith, east);

  354.         // set up the ellipse corresponding to second principal curvature in the zenith/east plane
  355.         final Ellipse secondPrincipalCurvature  = getPlaneSection(gpP, north);
  356.         final TimeStampedPVCoordinates gpSecond = secondPrincipalCurvature.projectToEllipse(pvInBodyFrame);

  357.         final Vector3D gpV = gpFirst.getVelocity().add(gpSecond.getVelocity());
  358.         final Vector3D gpA = gpFirst.getAcceleration().add(gpSecond.getAcceleration());

  359.         // moving projected point
  360.         final TimeStampedPVCoordinates groundPV =
  361.                 new TimeStampedPVCoordinates(pv.getDate(), gpP, gpV, gpA);

  362.         // transform moving projected point back to initial frame
  363.         return toBody.getInverse().transformPVCoordinates(groundPV);

  364.     }

  365.     /** {@inheritDoc}
  366.      * <p>
  367.      * This method is based on Toshio Fukushima's algorithm which uses Halley's method.
  368.      * <a href="https://www.researchgate.net/publication/227215135_Transformation_from_Cartesian_to_Geodetic_Coordinates_Accelerated_by_Halley's_Method">
  369.      * transformation from Cartesian to Geodetic Coordinates Accelerated by Halley's Method</a>,
  370.      * Toshio Fukushima, Journal of Geodesy 9(12):689-693, February 2006
  371.      * </p>
  372.      * <p>
  373.      * Some changes have been added to the original method:
  374.      * </p>
  375.      * <ul>
  376.      *   <li>in order to handle more accurately corner cases near the pole</li>
  377.      *   <li>in order to handle properly corner cases near the equatorial plane, even far inside the ellipsoid</li>
  378.      *   <li>in order to handle very flat ellipsoids</li>
  379.      * </ul>
  380.      * <p>
  381.      * In some rare cases (for example very flat ellipsoid, or points close to ellipsoid center), the loop
  382.      * may fail to converge. As this seems to happen only in degenerate cases, a design choice was to return
  383.      * an approximate point corresponding to last iteration. This point may be incorrect and fail to give the
  384.      * initial point back if doing roundtrip by calling {@link #transform(GeodeticPoint)}. This design choice
  385.      * was made to avoid NaNs appearing for example in inter-satellites visibility checks when two satellites
  386.      * are almost on opposite sides of Earth. The intermediate points far within the Earth should not prevent
  387.      * the detection algorithm to find visibility start/end.
  388.      * </p>
  389.      */
  390.     public GeodeticPoint transform(final Vector3D point, final Frame frame, final AbsoluteDate date) {

  391.         // transform point to body frame
  392.         final Vector3D pointInBodyFrame = frame.getStaticTransformTo(bodyFrame, date)
  393.                 .transformPosition(point);
  394.         final double   r2               = pointInBodyFrame.getX() * pointInBodyFrame.getX() +
  395.                                           pointInBodyFrame.getY() * pointInBodyFrame.getY();
  396.         final double   r                = FastMath.sqrt(r2);
  397.         final double   z                = pointInBodyFrame.getZ();

  398.         final double   lambda           = FastMath.atan2(pointInBodyFrame.getY(), pointInBodyFrame.getX());

  399.         double h;
  400.         double phi;
  401.         if (r <= ANGULAR_THRESHOLD * FastMath.abs(z)) {
  402.             // the point is almost on the polar axis, approximate the ellipsoid with
  403.             // the osculating sphere whose center is at evolute cusp along polar axis
  404.             final double osculatingRadius = ae2 / getC();
  405.             final double evoluteCuspZ     = FastMath.copySign(getA() * e2 / g, -z);
  406.             final double deltaZ           = z - evoluteCuspZ;
  407.             // we use Ï€/2 - atan(r/Δz) instead of atan(Δz/r) for accuracy purposes, as r is much smaller than Δz
  408.             phi = FastMath.copySign(0.5 * FastMath.PI - FastMath.atan(r / FastMath.abs(deltaZ)), deltaZ);
  409.             h   = FastMath.hypot(deltaZ, r) - osculatingRadius;
  410.         } else if (FastMath.abs(z) <= ANGULAR_THRESHOLD * r) {
  411.             // the point is almost on the major axis

  412.             final double osculatingRadius = ap2 / getA();
  413.             final double evoluteCuspR     = getA() * e2;
  414.             final double deltaR           = r - evoluteCuspR;
  415.             if (deltaR >= 0) {
  416.                 // the point is outside of the ellipse evolute, approximate the ellipse
  417.                 // with the osculating circle whose center is at evolute cusp along major axis
  418.                 phi = (deltaR == 0) ? 0.0 : FastMath.atan(z / deltaR);
  419.                 h   = FastMath.hypot(deltaR, z) - osculatingRadius;
  420.             } else {
  421.                 // the point is on the part of the major axis within ellipse evolute
  422.                 // we can compute the closest ellipse point analytically, and it is NOT near the equator
  423.                 final double rClose = r / e2;
  424.                 final double zClose = FastMath.copySign(g * FastMath.sqrt(ae2 - rClose * rClose), z);
  425.                 phi = FastMath.atan((zClose - z) / (rClose - r));
  426.                 h   = -FastMath.hypot(r - rClose, z - zClose);
  427.             }

  428.         } else {
  429.             // use Toshio Fukushima method, with several iterations
  430.             final double epsPhi = 1.0e-15;
  431.             final double epsH   = 1.0e-14 * FastMath.max(getA(), FastMath.sqrt(r2 + z * z));
  432.             final double c     = getA() * e2;
  433.             final double absZ  = FastMath.abs(z);
  434.             final double zc    = g * absZ;
  435.             double sn  = absZ;
  436.             double sn2 = sn * sn;
  437.             double cn  = g * r;
  438.             double cn2 = cn * cn;
  439.             double an2 = cn2 + sn2;
  440.             double an  = FastMath.sqrt(an2);
  441.             double bn  = 0;
  442.             phi = Double.POSITIVE_INFINITY;
  443.             h   = Double.POSITIVE_INFINITY;
  444.             for (int i = 0; i < 1000; ++i) {
  445.                 // this usually converges in 2 iterations, but in rare cases it can take much more
  446.                 // see https://gitlab.orekit.org/orekit/orekit/-/issues/1224 for examples
  447.                 // with points near Earth center which need 137 iterations for the first example
  448.                 // and 1150 iterations for the second example
  449.                 final double oldSn  = sn;
  450.                 final double oldCn  = cn;
  451.                 final double oldPhi = phi;
  452.                 final double oldH   = h;
  453.                 final double an3    = an2 * an;
  454.                 final double csncn  = c * sn * cn;
  455.                 bn    = 1.5 * csncn * ((r * sn - zc * cn) * an - csncn);
  456.                 sn    = (zc * an3 + c * sn2 * sn) * an3 - bn * sn;
  457.                 cn    = (r  * an3 - c * cn2 * cn) * an3 - bn * cn;
  458.                 if (sn * oldSn < 0 || cn < 0) {
  459.                     // the Halley iteration went too far, we restrict it and iterate again
  460.                     while (sn * oldSn < 0 || cn < 0) {
  461.                         sn = (sn + oldSn) / 2;
  462.                         cn = (cn + oldCn) / 2;
  463.                     }
  464.                 } else {

  465.                     // rescale components to avoid overflow when several iterations are used
  466.                     final int exp = (FastMath.getExponent(sn) + FastMath.getExponent(cn)) / 2;
  467.                     sn = FastMath.scalb(sn, -exp);
  468.                     cn = FastMath.scalb(cn, -exp);

  469.                     sn2 = sn * sn;
  470.                     cn2 = cn * cn;
  471.                     an2 = cn2 + sn2;
  472.                     an  = FastMath.sqrt(an2);

  473.                     final double cc = g * cn;
  474.                     h = (r * cc + absZ * sn - getA() * g * an) / FastMath.sqrt(an2 - e2 * cn2);
  475.                     if (FastMath.abs(oldH   - h)   < epsH) {
  476.                         phi = FastMath.copySign(FastMath.atan(sn / cc), z);
  477.                         if (FastMath.abs(oldPhi - phi) < epsPhi) {
  478.                             break;
  479.                         }
  480.                     }

  481.                 }

  482.             }

  483.             if (Double.isInfinite(phi)) {
  484.                 // we did not converge, the point is probably within the ellipsoid
  485.                 // we just compute the "best" phi we can to avoid NaN
  486.                 phi = FastMath.copySign(FastMath.atan(sn / (g * cn)), z);
  487.             }

  488.         }

  489.         return new GeodeticPoint(phi, lambda, h);

  490.     }

  491.     /** {@inheritDoc}
  492.      * <p>
  493.      * This method is based on Toshio Fukushima's algorithm which uses Halley's method.
  494.      * <a href="https://www.researchgate.net/publication/227215135_Transformation_from_Cartesian_to_Geodetic_Coordinates_Accelerated_by_Halley's_Method">
  495.      * transformation from Cartesian to Geodetic Coordinates Accelerated by Halley's Method</a>,
  496.      * Toshio Fukushima, Journal of Geodesy 9(12):689-693, February 2006
  497.      * </p>
  498.      * <p>
  499.      * Some changes have been added to the original method:
  500.      * <ul>
  501.      *   <li>in order to handle more accurately corner cases near the pole</li>
  502.      *   <li>in order to handle properly corner cases near the equatorial plane, even far inside the ellipsoid</li>
  503.      *   <li>in order to handle very flat ellipsoids</li>
  504.      * </ul>
  505.      * <p>
  506.      * In some rare cases (for example very flat ellipsoid, or points close to ellipsoid center), the loop
  507.      * may fail to converge. As this seems to happen only in degenerate cases, a design choice was to return
  508.      * an approximate point corresponding to last iteration. This point may be incorrect and fail to give the
  509.      * initial point back if doing roundtrip by calling {@link #transform(GeodeticPoint)}. This design choice
  510.      * was made to avoid NaNs appearing for example in inter-satellites visibility checks when two satellites
  511.      * are almost on opposite sides of Earth. The intermediate points far within the Earth should not prevent
  512.      * the detection algorithm to find visibility start/end.
  513.      * </p>
  514.      */
  515.     public <T extends CalculusFieldElement<T>> FieldGeodeticPoint<T> transform(final FieldVector3D<T> point,
  516.                                                                            final Frame frame,
  517.                                                                            final FieldAbsoluteDate<T> date) {

  518.         // transform point to body frame
  519.         final FieldVector3D<T> pointInBodyFrame = (frame == bodyFrame) ?
  520.                                                   point :
  521.                                                   frame.getStaticTransformTo(bodyFrame, date).transformPosition(point);
  522.         final T   r2                            = pointInBodyFrame.getX().multiply(pointInBodyFrame.getX()).
  523.                                                   add(pointInBodyFrame.getY().multiply(pointInBodyFrame.getY()));
  524.         final T   r                             = r2.sqrt();
  525.         final T   z                             = pointInBodyFrame.getZ();

  526.         final T   lambda                        = pointInBodyFrame.getY().atan2(pointInBodyFrame.getX());

  527.         T h;
  528.         T phi;
  529.         if (r.getReal() <= ANGULAR_THRESHOLD * FastMath.abs(z.getReal())) {
  530.             // the point is almost on the polar axis, approximate the ellipsoid with
  531.             // the osculating sphere whose center is at evolute cusp along polar axis
  532.             final double osculatingRadius = ae2 / getC();
  533.             final double evoluteCuspZ     = FastMath.copySign(getA() * e2 / g, -z.getReal());
  534.             final T      deltaZ           = z.subtract(evoluteCuspZ);
  535.             // we use Ï€/2 - atan(r/Δz) instead of atan(Δz/r) for accuracy purposes, as r is much smaller than Δz
  536.             phi = r.divide(deltaZ.abs()).atan().negate().add(r.getPi().multiply(0.5)).copySign(deltaZ);
  537.             h   = deltaZ.hypot(r).subtract(osculatingRadius);
  538.         } else if (FastMath.abs(z.getReal()) <= ANGULAR_THRESHOLD * r.getReal()) {
  539.             // the point is almost on the major axis

  540.             final double osculatingRadius = ap2 / getA();
  541.             final double evoluteCuspR     = getA() * e2;
  542.             final T      deltaR           = r.subtract(evoluteCuspR);
  543.             if (deltaR.getReal() >= 0) {
  544.                 // the point is outside of the ellipse evolute, approximate the ellipse
  545.                 // with the osculating circle whose center is at evolute cusp along major axis
  546.                 phi = (deltaR.getReal() == 0) ? z.getField().getZero() : z.divide(deltaR).atan();
  547.                 h   = deltaR.hypot(z).subtract(osculatingRadius);
  548.             } else {
  549.                 // the point is on the part of the major axis within ellipse evolute
  550.                 // we can compute the closest ellipse point analytically, and it is NOT near the equator
  551.                 final T rClose = r.divide(e2);
  552.                 final T zClose = rClose.multiply(rClose).negate().add(ae2).sqrt().multiply(g).copySign(z);
  553.                 phi = zClose.subtract(z).divide(rClose.subtract(r)).atan();
  554.                 h   = r.subtract(rClose).hypot(z.subtract(zClose)).negate();
  555.             }

  556.         } else {
  557.             // use Toshio Fukushima method, with several iterations
  558.             final double epsPhi = 1.0e-15;
  559.             final double epsH   = 1.0e-14 * getA();
  560.             final double c      = getA() * e2;
  561.             final T      absZ   = z.abs();
  562.             final T      zc     = absZ.multiply(g);
  563.             T            sn     = absZ;
  564.             T            sn2    = sn.multiply(sn);
  565.             T            cn     = r.multiply(g);
  566.             T            cn2    = cn.multiply(cn);
  567.             T            an2    = cn2.add(sn2);
  568.             T            an     = an2.sqrt();
  569.             T            bn     = an.getField().getZero();
  570.             phi = an.getField().getZero().add(Double.POSITIVE_INFINITY);
  571.             h   = an.getField().getZero().add(Double.POSITIVE_INFINITY);
  572.             for (int i = 0; i < 1000; ++i) {
  573.                 // this usually converges in 2 iterations, but in rare cases it can take much more
  574.                 // see https://gitlab.orekit.org/orekit/orekit/-/issues/1224 for examples
  575.                 // with points near Earth center which need 137 iterations for the first example
  576.                 // and 1150 iterations for the second example
  577.                 final T oldSn  = sn;
  578.                 final T oldCn  = cn;
  579.                 final T oldPhi = phi;
  580.                 final T oldH   = h;
  581.                 final T an3    = an2.multiply(an);
  582.                 final T csncn  = sn.multiply(cn).multiply(c);
  583.                 bn    = csncn.multiply(1.5).multiply((r.multiply(sn).subtract(zc.multiply(cn))).multiply(an).subtract(csncn));
  584.                 sn    = zc.multiply(an3).add(sn2.multiply(sn).multiply(c)).multiply(an3).subtract(bn.multiply(sn));
  585.                 cn    = r.multiply(an3).subtract(cn2.multiply(cn).multiply(c)).multiply(an3).subtract(bn.multiply(cn));
  586.                 if (sn.getReal() * oldSn.getReal() < 0 || cn.getReal() < 0) {
  587.                     // the Halley iteration went too far, we restrict it and iterate again
  588.                     while (sn.getReal() * oldSn.getReal() < 0 || cn.getReal() < 0) {
  589.                         sn = sn.add(oldSn).multiply(0.5);
  590.                         cn = cn.add(oldCn).multiply(0.5);
  591.                     }
  592.                 } else {

  593.                     // rescale components to avoid overflow when several iterations are used
  594.                     final int exp = (FastMath.getExponent(sn.getReal()) + FastMath.getExponent(cn.getReal())) / 2;
  595.                     sn = sn.scalb(-exp);
  596.                     cn = cn.scalb(-exp);

  597.                     sn2 = sn.square();
  598.                     cn2 = cn.square();
  599.                     an2 = cn2.add(sn2);
  600.                     an  = an2.sqrt();

  601.                     final T cc = cn.multiply(g);
  602.                     h = r.multiply(cc).add(absZ.multiply(sn)).subtract(an.multiply(getA() * g)).divide(an2.subtract(cn2.multiply(e2)).sqrt());
  603.                     if (FastMath.abs(oldH.getReal()  - h.getReal())   < epsH) {
  604.                         phi = sn.divide(cc).atan().copySign(z);
  605.                         if (FastMath.abs(oldPhi.getReal() - phi.getReal()) < epsPhi) {
  606.                             break;
  607.                         }
  608.                     }

  609.                 }

  610.             }

  611.             if (Double.isInfinite(phi.getReal())) {
  612.                 // we did not converge, the point is probably within the ellipsoid
  613.                 // we just compute the "best" phi we can to avoid NaN
  614.                 phi = sn.divide(cn.multiply(g)).atan().copySign(z);
  615.             }

  616.         }

  617.         return new FieldGeodeticPoint<>(phi, lambda, h);

  618.     }

  619.     /** Transform a Cartesian point to a surface-relative point.
  620.      * @param point Cartesian point
  621.      * @param frame frame in which Cartesian point is expressed
  622.      * @param date date of the computation (used for frames conversions)
  623.      * @return point at the same location but as a surface-relative point,
  624.      * using time as the single derivation parameter
  625.      */
  626.     public FieldGeodeticPoint<DerivativeStructure> transform(final PVCoordinates point,
  627.                                                              final Frame frame, final AbsoluteDate date) {

  628.         // transform point to body frame
  629.         final Transform toBody = frame.getTransformTo(bodyFrame, date);
  630.         final PVCoordinates pointInBodyFrame = toBody.transformPVCoordinates(point);
  631.         final FieldVector3D<DerivativeStructure> p = pointInBodyFrame.toDerivativeStructureVector(2);
  632.         final DerivativeStructure   pr2 = p.getX().square().add(p.getY().square());
  633.         final DerivativeStructure   pr  = pr2.sqrt();
  634.         final DerivativeStructure   pz  = p.getZ();

  635.         // project point on the ellipsoid surface
  636.         final TimeStampedPVCoordinates groundPoint = projectToGround(new TimeStampedPVCoordinates(date, pointInBodyFrame),
  637.                                                                      bodyFrame);
  638.         final FieldVector3D<DerivativeStructure> gp = groundPoint.toDerivativeStructureVector(2);
  639.         final DerivativeStructure   gpr2 = gp.getX().square().add(gp.getY().square());
  640.         final DerivativeStructure   gpr  = gpr2.sqrt();
  641.         final DerivativeStructure   gpz  = gp.getZ();

  642.         // relative position of test point with respect to its ellipse sub-point
  643.         final DerivativeStructure dr  = pr.subtract(gpr);
  644.         final DerivativeStructure dz  = pz.subtract(gpz);
  645.         final double insideIfNegative = g2 * (pr2.getReal() - ae2) + pz.getReal() * pz.getReal();

  646.         return new FieldGeodeticPoint<>(DerivativeStructure.atan2(gpz, gpr.multiply(g2)),
  647.                                                                   DerivativeStructure.atan2(p.getY(), p.getX()),
  648.                                                                   DerivativeStructure.hypot(dr, dz).copySign(insideIfNegative));
  649.     }

  650.     /** Compute the azimuth angle from local north between the two points.
  651.      *
  652.      * The angle is calculated clockwise from local north at the origin point
  653.      * and follows the rhumb line to the destination point.
  654.      *
  655.      * @param origin the origin point, at which the azimuth angle will be computed (non-{@code null})
  656.      * @param destination the destination point, to which the angle is defined (non-{@code null})
  657.      * @return the resulting azimuth angle (radians, {@code [0-2pi)})
  658.      * @since 11.3
  659.      */
  660.     public double azimuthBetweenPoints(final GeodeticPoint origin, final GeodeticPoint destination) {
  661.         final double dLon = MathUtils.normalizeAngle(destination.getLongitude(), origin.getLongitude()) - origin.getLongitude();
  662.         final double originIsoLat = geodeticToIsometricLatitude(origin.getLatitude());
  663.         final double destIsoLat = geodeticToIsometricLatitude(destination.getLatitude());

  664.         final double az = FastMath.atan2(dLon, destIsoLat - originIsoLat);
  665.         if (az < 0.) {
  666.             return az + MathUtils.TWO_PI;
  667.         }
  668.         return az;
  669.     }

  670.     /** Compute the azimuth angle from local north between the two points.
  671.      *
  672.      * The angle is calculated clockwise from local north at the origin point
  673.      * and follows the rhumb line to the destination point.
  674.      *
  675.      * @param origin the origin point, at which the azimuth angle will be computed (non-{@code null})
  676.      * @param destination the destination point, to which the angle is defined (non-{@code null})
  677.      * @param <T> the type of field elements
  678.      * @return the resulting azimuth angle (radians, {@code [0-2pi)})
  679.      * @since 11.3
  680.      */
  681.     public <T extends CalculusFieldElement<T>> T azimuthBetweenPoints(final FieldGeodeticPoint<T> origin, final FieldGeodeticPoint<T> destination) {
  682.         final T dLon = MathUtils.normalizeAngle(destination.getLongitude().subtract(origin.getLongitude()), origin.getLongitude().getField().getZero());
  683.         final T originIsoLat = geodeticToIsometricLatitude(origin.getLatitude());
  684.         final T destIsoLat = geodeticToIsometricLatitude(destination.getLatitude());

  685.         final T az = FastMath.atan2(dLon, destIsoLat.subtract(originIsoLat));
  686.         if (az.getReal() < 0.) {
  687.             return az.add(az.getPi().multiply(2));
  688.         }
  689.         return az;
  690.     }

  691.     /** Compute the <a href="https://mathworld.wolfram.com/IsometricLatitude.html">isometric latitude</a>
  692.      *  corresponding to the provided latitude.
  693.      *
  694.      * @param geodeticLatitude the latitude (radians, within interval {@code [-pi/2, +pi/2]})
  695.      * @return the isometric latitude (radians)
  696.      * @since 11.3
  697.      */
  698.     public double geodeticToIsometricLatitude(final double geodeticLatitude) {
  699.         if (FastMath.abs(geodeticLatitude) <= angularThreshold) {
  700.             return 0.;
  701.         }

  702.         final double eSinLat = e * FastMath.sin(geodeticLatitude);

  703.         // first term: ln(tan(pi/4 + lat/2))
  704.         final double a = FastMath.log(FastMath.tan(FastMath.PI / 4. + geodeticLatitude / 2.));
  705.         // second term: (ecc / 2) * ln((1 - ecc*sin(lat)) / (1 + ecc * sin(lat)))
  706.         final double b = (e / 2.) * FastMath.log((1. - eSinLat) / (1. + eSinLat));

  707.         return a + b;
  708.     }

  709.     /** Compute the <a href="https://mathworld.wolfram.com/IsometricLatitude.html">isometric latitude</a>
  710.      *  corresponding to the provided latitude.
  711.      *
  712.      * @param geodeticLatitude the latitude (radians, within interval {@code [-pi/2, +pi/2]})
  713.      * @param <T> the type of field elements
  714.      * @return the isometric latitude (radians)
  715.      * @since 11.3
  716.      */
  717.     public <T extends CalculusFieldElement<T>> T geodeticToIsometricLatitude(final T geodeticLatitude) {
  718.         if (geodeticLatitude.abs().getReal() <= angularThreshold) {
  719.             return geodeticLatitude.getField().getZero();
  720.         }
  721.         final Field<T> field = geodeticLatitude.getField();
  722.         final T ecc = geodeticLatitude.newInstance(e);
  723.         final T eSinLat = ecc.multiply(geodeticLatitude.sin());

  724.         // first term: ln(tan(pi/4 + lat/2))
  725.         final T a = FastMath.log(FastMath.tan(geodeticLatitude.getPi().divide(4.).add(geodeticLatitude.divide(2.))));
  726.         // second term: (ecc / 2) * ln((1 - ecc*sin(lat)) / (1 + ecc * sin(lat)))
  727.         final T b = ecc.divide(2.).multiply(FastMath.log(field.getOne().subtract(eSinLat).divide(field.getOne().add(eSinLat))));

  728.         return a.add(b);
  729.     }

  730.     /** Find intermediate point of lowest altitude along a line between two endpoints.
  731.      * @param endpoint1 first endpoint, in body frame
  732.      * @param endpoint2 second endpoint, in body frame
  733.      * @return point with lowest altitude between {@code endpoint1} and {@code endpoint2}.
  734.      * @since 12.0
  735.      */
  736.     public GeodeticPoint lowestAltitudeIntermediate(final Vector3D endpoint1, final Vector3D endpoint2) {

  737.         final Vector3D delta = endpoint2.subtract(endpoint1);

  738.         // function computing intermediate point above ellipsoid (lambda varying between 0 and 1)
  739.         final DoubleFunction<GeodeticPoint> intermediate =
  740.                         lambda -> transform(new Vector3D(1 - lambda, endpoint1, lambda, endpoint2),
  741.                                             bodyFrame, null);

  742.         // first endpoint
  743.         final GeodeticPoint gp1 = intermediate.apply(0.0);

  744.         if (Vector3D.dotProduct(delta, gp1.getZenith()) >= 0) {
  745.             // the line from first endpoint to second endpoint is going away from central body
  746.             // the minimum altitude is reached at first endpoint
  747.             return gp1;
  748.         } else {
  749.             // the line from first endpoint to second endpoint is closing the central body

  750.             // second endpoint
  751.             final GeodeticPoint gp2 = intermediate.apply(1.0);

  752.             if (Vector3D.dotProduct(delta, gp2.getZenith()) <= 0) {
  753.                 // the line from first endpoint to second endpoint is still decreasing when reaching second endpoint,
  754.                 // the minimum altitude is reached at second endpoint
  755.                 return gp2;
  756.             } else {
  757.                 // the line from first endpoint to second endpoint reaches a minimum between first and second endpoints
  758.                 final double lambdaMin = new BracketingNthOrderBrentSolver(1.0e-14, 5).
  759.                                          solve(1000,
  760.                                                lambda -> Vector3D.dotProduct(delta, intermediate.apply(lambda).getZenith()),
  761.                                                0.0, 1.0);
  762.                 return intermediate.apply(lambdaMin);
  763.             }
  764.         }

  765.     }

  766.     /** Find intermediate point of lowest altitude along a line between two endpoints.
  767.      * @param endpoint1 first endpoint, in body frame
  768.      * @param endpoint2 second endpoint, in body frame
  769.      * @param <T> type of the field elements
  770.      * @return point with lowest altitude between {@code endpoint1} and {@code endpoint2}.
  771.      * @since 12.0
  772.      */
  773.     public <T extends CalculusFieldElement<T>> FieldGeodeticPoint<T> lowestAltitudeIntermediate(final FieldVector3D<T> endpoint1,
  774.                                                                                                 final FieldVector3D<T> endpoint2) {

  775.         final FieldVector3D<T> delta = endpoint2.subtract(endpoint1);

  776.         // function computing intermediate point above ellipsoid (lambda varying between 0 and 1)
  777.         final DoubleFunction<FieldGeodeticPoint<T>> intermediate =
  778.                         lambda -> transform(new FieldVector3D<>(1 - lambda, endpoint1, lambda, endpoint2),
  779.                                             bodyFrame, null);

  780.         // first endpoint
  781.         final FieldGeodeticPoint<T> gp1 = intermediate.apply(0.0);

  782.         if (FieldVector3D.dotProduct(delta, gp1.getZenith()).getReal() >= 0) {
  783.             // the line from first endpoint to second endpoint is going away from central body
  784.             // the minimum altitude is reached at first endpoint
  785.             return gp1;
  786.         } else {
  787.             // the line from first endpoint to second endpoint is closing the central body

  788.             // second endpoint
  789.             final FieldGeodeticPoint<T> gp2 = intermediate.apply(1.0);

  790.             if (FieldVector3D.dotProduct(delta, gp2.getZenith()).getReal() <= 0) {
  791.                 // the line from first endpoint to second endpoint is still decreasing when reaching second endpoint,
  792.                 // the minimum altitude is reached at second endpoint
  793.                 return gp2;
  794.             } else {
  795.                 // the line from first endpoint to second endpoint reaches a minimum between first and second endpoints
  796.                 final double lambdaMin = new BracketingNthOrderBrentSolver(1.0e-14, 5).
  797.                                          solve(1000,
  798.                                                lambda -> FieldVector3D.dotProduct(delta, intermediate.apply(lambda).getZenith()).getReal(),
  799.                                                0.0, 1.0);
  800.                 return intermediate.apply(lambdaMin);
  801.             }
  802.         }

  803.     }

  804.     /** Replace the instance with a data transfer object for serialization.
  805.      * <p>
  806.      * This intermediate class serializes the files supported names, the
  807.      * ephemeris type and the body name.
  808.      * </p>
  809.      * @return data transfer object that will be serialized
  810.      */
  811.     private Object writeReplace() {
  812.         return new DataTransferObject(getA(), f, bodyFrame, angularThreshold);
  813.     }

  814.     /** Internal class used only for serialization. */
  815.     private static class DataTransferObject implements Serializable {

  816.         /** Serializable UID. */
  817.         private static final long serialVersionUID = 20130518L;

  818.         /** Equatorial radius. */
  819.         private final double ae;

  820.         /** Flattening. */
  821.         private final double f;

  822.         /** Body frame related to body shape. */
  823.         private final Frame bodyFrame;

  824.         /** Convergence limit. */
  825.         private final double angularThreshold;

  826.         /** Simple constructor.
  827.          * @param ae equatorial radius
  828.          * @param f the flattening (f = (a-b)/a)
  829.          * @param bodyFrame body frame related to body shape
  830.          * @param angularThreshold convergence limit
  831.          */
  832.         DataTransferObject(final double ae, final double f,
  833.                                   final Frame bodyFrame, final double angularThreshold) {
  834.             this.ae               = ae;
  835.             this.f                = f;
  836.             this.bodyFrame        = bodyFrame;
  837.             this.angularThreshold = angularThreshold;
  838.         }

  839.         /** Replace the deserialized data transfer object with a
  840.          * {@link JPLCelestialBody}.
  841.          * @return replacement {@link JPLCelestialBody}
  842.          */
  843.         private Object readResolve() {
  844.             final OneAxisEllipsoid ellipsoid = new OneAxisEllipsoid(ae, f, bodyFrame);
  845.             ellipsoid.setAngularThreshold(angularThreshold);
  846.             return ellipsoid;
  847.         }

  848.     }

  849. }