Ellipsoid.java

  1. /* Copyright 2002-2025 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 org.hipparchus.CalculusFieldElement;
  19. import org.hipparchus.exception.MathRuntimeException;
  20. import org.hipparchus.geometry.euclidean.threed.FieldVector3D;
  21. import org.hipparchus.geometry.euclidean.threed.Vector3D;
  22. import org.hipparchus.geometry.euclidean.twod.FieldVector2D;
  23. import org.hipparchus.geometry.euclidean.twod.Vector2D;
  24. import org.hipparchus.util.FastMath;
  25. import org.hipparchus.util.MathArrays;
  26. import org.hipparchus.util.Precision;
  27. import org.orekit.errors.OrekitException;
  28. import org.orekit.errors.OrekitMessages;
  29. import org.orekit.frames.Frame;

  30. /**
  31.  * Modeling of a general three-axes ellipsoid.
  32.  * @since 7.0
  33.  * @author Luc Maisonobe
  34.  */
  35. public class Ellipsoid {

  36.     /** Frame at the ellipsoid center, aligned with principal axes. */
  37.     private final Frame frame;

  38.     /** First semi-axis length. */
  39.     private final double a;

  40.     /** Second semi-axis length. */
  41.     private final double b;

  42.     /** Third semi-axis length. */
  43.     private final double c;

  44.     /** Simple constructor.
  45.      * @param frame at the ellipsoid center, aligned with principal axes
  46.      * @param a first semi-axis length
  47.      * @param b second semi-axis length
  48.      * @param c third semi-axis length
  49.      */
  50.     public Ellipsoid(final Frame frame, final double a, final double b, final double c) {
  51.         this.frame = frame;
  52.         this.a     = a;
  53.         this.b     = b;
  54.         this.c     = c;
  55.     }

  56.     /** Get the length of the first semi-axis.
  57.      * @return length of the first semi-axis (m)
  58.      */
  59.     public double getA() {
  60.         return a;
  61.     }

  62.     /** Get the length of the second semi-axis.
  63.      * @return length of the second semi-axis (m)
  64.      */
  65.     public double getB() {
  66.         return b;
  67.     }

  68.     /** Get the length of the third semi-axis.
  69.      * @return length of the third semi-axis (m)
  70.      */
  71.     public double getC() {
  72.         return c;
  73.     }

  74.     /** Get the ellipsoid central frame.
  75.      * @return ellipsoid central frame
  76.      */
  77.     public Frame getFrame() {
  78.         return frame;
  79.     }

  80.     /** Check if a point is inside the ellipsoid.
  81.      * @param point point to check, in the ellipsoid frame
  82.      * @return true if the point is inside the ellipsoid
  83.      * (or exactly on ellipsoid surface)
  84.      * @since 7.1
  85.      */
  86.     public boolean isInside(final Vector3D point) {
  87.         final double scaledX = point.getX() / a;
  88.         final double scaledY = point.getY() / b;
  89.         final double scaledZ = point.getZ() / c;
  90.         return scaledX * scaledX + scaledY * scaledY + scaledZ * scaledZ <= 1.0;
  91.     }

  92.     /** Check if a point is inside the ellipsoid.
  93.      * @param point point to check, in the ellipsoid frame
  94.      * @return true if the point is inside the ellipsoid
  95.      * (or exactly on ellipsoid surface)
  96.      * @param <T> the type of the field elements
  97.      * @since 12.0
  98.      */
  99.     public <T extends CalculusFieldElement<T>> boolean isInside(final FieldVector3D<T> point) {
  100.         final T scaledX = point.getX().divide(a);
  101.         final T scaledY = point.getY().divide(b);
  102.         final T scaledZ = point.getZ().divide(c);
  103.         final T d2      = scaledX.multiply(scaledX).add(scaledY.multiply(scaledY)).add(scaledZ.multiply(scaledZ));
  104.         return d2.subtract(1.0).getReal() <= 0.0;
  105.     }

  106.     /** Compute the 2D ellipse at the intersection of the 3D ellipsoid and a plane.
  107.      * @param planePoint point belonging to the plane, in the ellipsoid frame
  108.      * @param planeNormal normal of the plane, in the ellipsoid frame
  109.      * @return plane section or null if there are no intersections
  110.      * @exception MathRuntimeException if the norm of planeNormal is null
  111.      */
  112.     public Ellipse getPlaneSection(final Vector3D planePoint, final Vector3D planeNormal)
  113.         throws MathRuntimeException {

  114.         // we define the points Q in the plane using two free variables τ and υ as:
  115.         // Q = P + τ u + υ v
  116.         // where u and v are two unit vectors belonging to the plane
  117.         // Q belongs to the 3D ellipsoid so:
  118.         // (xQ / a)² + (yQ / b)² + (zQ / c)² = 1
  119.         // combining both equations, we get:
  120.         //   (xP² + 2 xP (τ xU + υ xV) + (τ xU + υ xV)²) / a²
  121.         // + (yP² + 2 yP (τ yU + υ yV) + (τ yU + υ yV)²) / b²
  122.         // + (zP² + 2 zP (τ zU + υ zV) + (τ zU + υ zV)²) / c²
  123.         // = 1
  124.         // which can be rewritten:
  125.         // α τ² + β υ² + 2 γ τυ + 2 δ τ + 2 ε υ + ζ = 0
  126.         // with
  127.         // α =  xU²  / a² +  yU²  / b² +  zU²  / c² > 0
  128.         // β =  xV²  / a² +  yV²  / b² +  zV²  / c² > 0
  129.         // γ = xU xV / a² + yU yV / b² + zU zV / c²
  130.         // δ = xP xU / a² + yP yU / b² + zP zU / c²
  131.         // ε = xP xV / a² + yP yV / b² + zP zV / c²
  132.         // ζ =  xP²  / a² +  yP²  / b² +  zP²  / c² - 1
  133.         // this is the equation of a conic (here an ellipse)
  134.         // Of course, we note that if the point P belongs to the ellipsoid
  135.         // then ζ = 0 and the equation holds at point P since τ = 0 and υ = 0
  136.         final Vector3D u     = planeNormal.orthogonal();
  137.         final Vector3D v     = Vector3D.crossProduct(planeNormal, u).normalize();
  138.         final double xUOa    = u.getX() / a;
  139.         final double yUOb    = u.getY() / b;
  140.         final double zUOc    = u.getZ() / c;
  141.         final double xVOa    = v.getX() / a;
  142.         final double yVOb    = v.getY() / b;
  143.         final double zVOc    = v.getZ() / c;
  144.         final double xPOa    = planePoint.getX() / a;
  145.         final double yPOb    = planePoint.getY() / b;
  146.         final double zPOc    = planePoint.getZ() / c;
  147.         final double alpha   = xUOa * xUOa + yUOb * yUOb + zUOc * zUOc;
  148.         final double beta    = xVOa * xVOa + yVOb * yVOb + zVOc * zVOc;
  149.         final double gamma   = MathArrays.linearCombination(xUOa, xVOa, yUOb, yVOb, zUOc, zVOc);
  150.         final double delta   = MathArrays.linearCombination(xPOa, xUOa, yPOb, yUOb, zPOc, zUOc);
  151.         final double epsilon = MathArrays.linearCombination(xPOa, xVOa, yPOb, yVOb, zPOc, zVOc);
  152.         final double zeta    = MathArrays.linearCombination(xPOa, xPOa, yPOb, yPOb, zPOc, zPOc, 1, -1);

  153.         // reduce the general equation α τ² + β υ² + 2 γ τυ + 2 δ τ + 2 ε υ + ζ = 0
  154.         // to canonical form (λ/l)² + (μ/m)² = 1
  155.         // using a coordinates change
  156.         //       τ = τC + λ cosθ - μ sinθ
  157.         //       υ = υC + λ sinθ + μ cosθ
  158.         // or equivalently
  159.         //       λ =   (τ - τC) cosθ + (υ - υC) sinθ
  160.         //       μ = - (τ - τC) sinθ + (υ - υC) cosθ
  161.         // τC and υC are the coordinates of the 2D ellipse center with respect to P
  162.         // 2l and 2m and are the axes lengths (major or minor depending on which one is greatest)
  163.         // θ is the angle of the 2D ellipse axis corresponding to axis with length 2l

  164.         // choose θ in order to cancel the coupling term in λμ
  165.         // expanding the general equation, we get: A λ² + B μ² + C λμ + D λ + E μ + F = 0
  166.         // with C = 2[(β - α) cosθ sinθ + γ (cos²θ - sin²θ)]
  167.         // hence the term is cancelled when θ = arctan(t), with γ t² + (α - β) t - γ = 0
  168.         // As the solutions of the quadratic equation obey t₁t₂ = -1, they correspond to
  169.         // angles θ in quadrature to each other. Selecting one solution or the other simply
  170.         // exchanges the principal axes. As we don't care about which axis we want as the
  171.         // first one, we select an arbitrary solution
  172.         final double tanTheta;
  173.         if (FastMath.abs(gamma) < Precision.SAFE_MIN) {
  174.             tanTheta = 0.0;
  175.         } else {
  176.             final double bMA = beta - alpha;
  177.             tanTheta = (bMA >= 0) ?
  178.                        (-2 * gamma / (bMA + FastMath.sqrt(bMA * bMA + 4 * gamma * gamma))) :
  179.                        (-2 * gamma / (bMA - FastMath.sqrt(bMA * bMA + 4 * gamma * gamma)));
  180.         }
  181.         final double tan2   = tanTheta * tanTheta;
  182.         final double cos2   = 1 / (1 + tan2);
  183.         final double sin2   = tan2 * cos2;
  184.         final double cosSin = tanTheta * cos2;
  185.         final double cos    = FastMath.sqrt(cos2);
  186.         final double sin    = tanTheta * cos;

  187.         // choose τC and υC in order to cancel the linear terms in λ and μ
  188.         // expanding the general equation, we get: A λ² + B μ² + C λμ + D λ + E μ + F = 0
  189.         // with D = 2[ (α τC + γ υC + δ) cosθ + (γ τC + β υC + ε) sinθ]
  190.         //      E = 2[-(α τC + γ υC + δ) sinθ + (γ τC + β υC + ε) cosθ]
  191.         // θ can be eliminated by combining the equations
  192.         // D cosθ - E sinθ = 2[α τC + γ υC + δ]
  193.         // E cosθ + D sinθ = 2[γ τC + β υC + ε]
  194.         // hence the terms D and E are both cancelled (regardless of θ) when
  195.         //     τC = (β δ - γ ε) / (γ² - α β)
  196.         //     υC = (α ε - γ δ) / (γ² - α β)
  197.         final double denom = MathArrays.linearCombination(gamma, gamma,   -alpha, beta);
  198.         final double tauC  = MathArrays.linearCombination(beta,  delta,   -gamma, epsilon) / denom;
  199.         final double nuC   = MathArrays.linearCombination(alpha, epsilon, -gamma, delta)   / denom;

  200.         // compute l and m
  201.         // expanding the general equation, we get: A λ² + B μ² + C λμ + D λ + E μ + F = 0
  202.         // with A = α cos²θ + β sin²θ + 2 γ cosθ sinθ
  203.         //      B = α sin²θ + β cos²θ - 2 γ cosθ sinθ
  204.         //      F = α τC² + β υC² + 2 γ τC υC + 2 δ τC + 2 ε υC + ζ
  205.         // hence we compute directly l = √(-F/A) and m = √(-F/B)
  206.         final double twogcs = 2 * gamma * cosSin;
  207.         final double bigA   = alpha * cos2 + beta * sin2 + twogcs;
  208.         final double bigB   = alpha * sin2 + beta * cos2 - twogcs;
  209.         final double bigF   = (alpha * tauC + 2 * (gamma * nuC + delta)) * tauC +
  210.                               (beta * nuC + 2 * epsilon) * nuC + zeta;
  211.         final double l      = FastMath.sqrt(-bigF / bigA);
  212.         final double m      = FastMath.sqrt(-bigF / bigB);
  213.         if (Double.isNaN(l + m)) {
  214.             // the plane does not intersect the ellipsoid
  215.             return null;
  216.         }

  217.         if (l > m) {
  218.             return new Ellipse(new Vector3D(1, planePoint, tauC, u, nuC, v),
  219.                                new Vector3D( cos, u, sin, v),
  220.                                new Vector3D(-sin, u, cos, v),
  221.                                l, m, frame);
  222.         } else {
  223.             return new Ellipse(new Vector3D(1, planePoint, tauC, u, nuC, v),
  224.                                new Vector3D(sin, u, -cos, v),
  225.                                new Vector3D(cos, u,  sin, v),
  226.                                m, l, frame);
  227.         }

  228.     }

  229.     /** Compute the 2D ellipse at the intersection of the 3D ellipsoid and a plane.
  230.      * @param planePoint point belonging to the plane, in the ellipsoid frame
  231.      * @param planeNormal normal of the plane, in the ellipsoid frame
  232.      * @return plane section or null if there are no intersections
  233.      * @exception MathRuntimeException if the norm of planeNormal is null
  234.      * @param <T> the type of the field elements
  235.      * @since 12.0
  236.      */
  237.     public <T extends CalculusFieldElement<T>> FieldEllipse<T> getPlaneSection(final FieldVector3D<T> planePoint, final FieldVector3D<T> planeNormal)
  238.         throws MathRuntimeException {

  239.         final T zero = planePoint.getX().getField().getZero();
  240.         final T one  = planePoint.getX().getField().getOne();

  241.         // we define the points Q in the plane using two free variables τ and υ as:
  242.         // Q = P + τ u + υ v
  243.         // where u and v are two unit vectors belonging to the plane
  244.         // Q belongs to the 3D ellipsoid so:
  245.         // (xQ / a)² + (yQ / b)² + (zQ / c)² = 1
  246.         // combining both equations, we get:
  247.         //   (xP² + 2 xP (τ xU + υ xV) + (τ xU + υ xV)²) / a²
  248.         // + (yP² + 2 yP (τ yU + υ yV) + (τ yU + υ yV)²) / b²
  249.         // + (zP² + 2 zP (τ zU + υ zV) + (τ zU + υ zV)²) / c²
  250.         // = 1
  251.         // which can be rewritten:
  252.         // α τ² + β υ² + 2 γ τυ + 2 δ τ + 2 ε υ + ζ = 0
  253.         // with
  254.         // α =  xU²  / a² +  yU²  / b² +  zU²  / c² > 0
  255.         // β =  xV²  / a² +  yV²  / b² +  zV²  / c² > 0
  256.         // γ = xU xV / a² + yU yV / b² + zU zV / c²
  257.         // δ = xP xU / a² + yP yU / b² + zP zU / c²
  258.         // ε = xP xV / a² + yP yV / b² + zP zV / c²
  259.         // ζ =  xP²  / a² +  yP²  / b² +  zP²  / c² - 1
  260.         // this is the equation of a conic (here an ellipse)
  261.         // Of course, we note that if the point P belongs to the ellipsoid
  262.         // then ζ = 0 and the equation holds at point P since τ = 0 and υ = 0
  263.         final FieldVector3D<T> u     = planeNormal.orthogonal();
  264.         final FieldVector3D<T> v     = FieldVector3D.crossProduct(planeNormal, u).normalize();
  265.         final T xUOa    = u.getX().divide(a);
  266.         final T yUOb    = u.getY().divide(b);
  267.         final T zUOc    = u.getZ().divide(c);
  268.         final T xVOa    = v.getX().divide(a);
  269.         final T yVOb    = v.getY().divide(b);
  270.         final T zVOc    = v.getZ().divide(c);
  271.         final T xPOa    = planePoint.getX().divide(a);
  272.         final T yPOb    = planePoint.getY().divide(b);
  273.         final T zPOc    = planePoint.getZ().divide(c);
  274.         final T alpha   = xUOa.multiply(xUOa).add(yUOb.multiply(yUOb)).add(zUOc.multiply(zUOc));
  275.         final T beta    = xVOa.multiply(xVOa).add(yVOb.multiply(yVOb)).add(zVOc.multiply(zVOc));
  276.         final T gamma   = alpha.linearCombination(xUOa, xVOa, yUOb, yVOb, zUOc, zVOc);
  277.         final T delta   = alpha.linearCombination(xPOa, xUOa, yPOb, yUOb, zPOc, zUOc);
  278.         final T epsilon = alpha.linearCombination(xPOa, xVOa, yPOb, yVOb, zPOc, zVOc);
  279.         final T zeta    = alpha.linearCombination(xPOa, xPOa, yPOb, yPOb, zPOc, zPOc, one, one.negate());

  280.         // reduce the general equation α τ² + β υ² + 2 γ τυ + 2 δ τ + 2 ε υ + ζ = 0
  281.         // to canonical form (λ/l)² + (μ/m)² = 1
  282.         // using a coordinates change
  283.         //       τ = τC + λ cosθ - μ sinθ
  284.         //       υ = υC + λ sinθ + μ cosθ
  285.         // or equivalently
  286.         //       λ =   (τ - τC) cosθ + (υ - υC) sinθ
  287.         //       μ = - (τ - τC) sinθ + (υ - υC) cosθ
  288.         // τC and υC are the coordinates of the 2D ellipse center with respect to P
  289.         // 2l and 2m and are the axes lengths (major or minor depending on which one is greatest)
  290.         // θ is the angle of the 2D ellipse axis corresponding to axis with length 2l

  291.         // choose θ in order to cancel the coupling term in λμ
  292.         // expanding the general equation, we get: A λ² + B μ² + C λμ + D λ + E μ + F = 0
  293.         // with C = 2[(β - α) cosθ sinθ + γ (cos²θ - sin²θ)]
  294.         // hence the term is cancelled when θ = arctan(t), with γ t² + (α - β) t - γ = 0
  295.         // As the solutions of the quadratic equation obey t₁t₂ = -1, they correspond to
  296.         // angles θ in quadrature to each other. Selecting one solution or the other simply
  297.         // exchanges the principal axes. As we don't care about which axis we want as the
  298.         // first one, we select an arbitrary solution
  299.         final T tanTheta;
  300.         if (FastMath.abs(gamma.getReal()) < Precision.SAFE_MIN) {
  301.             tanTheta = zero;
  302.         } else {
  303.             final T bMA = beta.subtract(alpha);
  304.             tanTheta = (bMA.getReal() >= 0) ?
  305.                        gamma.multiply(-2).divide(bMA.add(FastMath.sqrt(bMA.multiply(bMA).add(gamma.multiply(gamma).multiply(4))))) :
  306.                        gamma.multiply(-2).divide(bMA.subtract(FastMath.sqrt(bMA.multiply(bMA).add(gamma.multiply(gamma).multiply(4)))));
  307.         }
  308.         final T tan2   = tanTheta.multiply(tanTheta);
  309.         final T cos2   = tan2.add(1).reciprocal();
  310.         final T sin2   = tan2.multiply(cos2);
  311.         final T cosSin = tanTheta.multiply(cos2);
  312.         final T cos    = FastMath.sqrt(cos2);
  313.         final T sin    = tanTheta.multiply(cos);

  314.         // choose τC and υC in order to cancel the linear terms in λ and μ
  315.         // expanding the general equation, we get: A λ² + B μ² + C λμ + D λ + E μ + F = 0
  316.         // with D = 2[ (α τC + γ υC + δ) cosθ + (γ τC + β υC + ε) sinθ]
  317.         //      E = 2[-(α τC + γ υC + δ) sinθ + (γ τC + β υC + ε) cosθ]
  318.         // θ can be eliminated by combining the equations
  319.         // D cosθ - E sinθ = 2[α τC + γ υC + δ]
  320.         // E cosθ + D sinθ = 2[γ τC + β υC + ε]
  321.         // hence the terms D and E are both cancelled (regardless of θ) when
  322.         //     τC = (β δ - γ ε) / (γ² - α β)
  323.         //     υC = (α ε - γ δ) / (γ² - α β)
  324.         final T invDenom = gamma.linearCombination(gamma, gamma,   alpha.negate(), beta).reciprocal();
  325.         final T tauC     = gamma.linearCombination(beta,  delta,   gamma.negate(), epsilon).multiply(invDenom);
  326.         final T nuC      = gamma.linearCombination(alpha, epsilon, gamma.negate(), delta).multiply(invDenom);

  327.         // compute l and m
  328.         // expanding the general equation, we get: A λ² + B μ² + C λμ + D λ + E μ + F = 0
  329.         // with A = α cos²θ + β sin²θ + 2 γ cosθ sinθ
  330.         //      B = α sin²θ + β cos²θ - 2 γ cosθ sinθ
  331.         //      F = α τC² + β υC² + 2 γ τC υC + 2 δ τC + 2 ε υC + ζ
  332.         // hence we compute directly l = √(-F/A) and m = √(-F/B)
  333.         final T twogcs = gamma.multiply(cosSin).multiply(2);
  334.         final T bigA   = alpha.multiply(cos2).add(beta.multiply(sin2)).add(twogcs);
  335.         final T bigB   = alpha.multiply(sin2).add(beta.multiply(cos2)).subtract(twogcs);
  336.         final T bigFN  = alpha.multiply(tauC).add(gamma.multiply(nuC).add(delta).multiply(2)).multiply(tauC).
  337.                          add(beta.multiply(nuC).add(epsilon.multiply(2)).multiply(nuC)).
  338.                          add(zeta).
  339.                          negate();
  340.         final T l      = FastMath.sqrt(bigFN.divide(bigA));
  341.         final T m      = FastMath.sqrt(bigFN.divide(bigB));
  342.         if (l.add(m).isNaN()) {
  343.             // the plane does not intersect the ellipsoid
  344.             return null;
  345.         }

  346.         if (l.subtract(m).getReal() > 0) {
  347.             return new FieldEllipse<>(new FieldVector3D<>(tauC.getField().getOne(), planePoint, tauC, u, nuC, v),
  348.                                       new FieldVector3D<>(cos,          u, sin, v),
  349.                                       new FieldVector3D<>(sin.negate(), u, cos, v),
  350.                                       l, m, frame);
  351.         } else {
  352.             return new FieldEllipse<>(new FieldVector3D<>(tauC.getField().getOne(), planePoint, tauC, u, nuC, v),
  353.                                       new FieldVector3D<>(sin, u, cos.negate(), v),
  354.                                       new FieldVector3D<>(cos, u, sin,          v),
  355.                                       m, l, frame);
  356.         }

  357.     }

  358.     /** Find a point on ellipsoid limb, as seen by an external observer.
  359.      * @param observer observer position in ellipsoid frame
  360.      * @param outside point outside ellipsoid in ellipsoid frame, defining the phase around limb
  361.      * @return point on ellipsoid limb
  362.      * @exception MathRuntimeException if ellipsoid center, observer and outside
  363.      * points are aligned
  364.      * @since 7.1
  365.      */
  366.     public Vector3D pointOnLimb(final Vector3D observer, final Vector3D outside)
  367.         throws MathRuntimeException {

  368.         // There is no limb if we are inside the ellipsoid
  369.         if (isInside(observer)) {
  370.             throw new OrekitException(OrekitMessages.POINT_INSIDE_ELLIPSOID);
  371.         }
  372.         // Cut the ellipsoid, to find an elliptical plane section
  373.         final Vector3D normal  = Vector3D.crossProduct(observer, outside);
  374.         final Ellipse  section = getPlaneSection(Vector3D.ZERO, normal);

  375.         // the point on limb is tangential to the ellipse
  376.         // if T(xt, yt) is an ellipse point at which the tangent is drawn
  377.         // if O(xo, yo) is a point outside of the ellipse belonging to the tangent at T,
  378.         // then the two following equations holds:
  379.         // (1) a² yt²   + b² xt²   = a² b²  (T belongs to the ellipse)
  380.         // (2) a² yt yo + b² xt xo = a² b²  (TP is tangent to the ellipse)
  381.         // using the second equation to eliminate yt from the first equation, we get
  382.         // b² (a² - xt xo)² + a² xt² yo² = a⁴ yo²
  383.         // (3) (a² yo² + b² xo²) xt² - 2 a² b² xo xt + a⁴ (b² - yo²) = 0
  384.         // which can easily be solved for xt

  385.         // To avoid numerical errors, the x and y coordinates in the ellipse plane are normalized using:
  386.         // x' = x / a and y' = y / b
  387.         //
  388.         // This gives:
  389.         // (1) y't² + x't² = 1
  390.         // (2) y't y'o + x't x'o = 1
  391.         //
  392.         // And finally:
  393.         // (3) (x'o² + y'o²) x't² - 2 x't x'o + 1 - y'o² = 0
  394.         //
  395.         // Solving for x't, we get the reduced discriminant:
  396.         // delta' = beta'² - alpha' * gamma'
  397.         //
  398.         // With:
  399.         // beta' = x'o
  400.         // alpha' = x'o² + y'o²
  401.         // gamma' = 1 - y'o²
  402.         //
  403.         // Simplifying to  cancel a term of x'o².
  404.         // delta' = y'o² (x'o² + y'o² - 1) = y'o² (alpha' - 1)
  405.         //
  406.         // After solving for xt1, xt2 using (3) the values are substituted into (2) to
  407.         // compute yt1, yt2. Then terms of x'o may be canceled from the expressions for
  408.         // yt1 and yt2. Additionally a point discontinuity is removed at y'o=0 from both
  409.         // yt1 and yt2.
  410.         //
  411.         // y't1 = (y'o - x'o d) / (x'o² + y'o²)
  412.         // y't2 = (x'o y'o + d) / (x + sqrt(delta'))
  413.         //
  414.         // where:
  415.         // d = sign(y'o) sqrt(alpha' - 1)

  416.         // Get the point in ellipse plane frame (2D)
  417.         final Vector2D observer2D = section.toPlane(observer);

  418.         // Normalize and compute intermediary terms
  419.         final double ap = section.getA();
  420.         final double bp = section.getB();
  421.         final double xpo = observer2D.getX() / ap;
  422.         final double ypo = observer2D.getY() / bp;
  423.         final double xpo2 = xpo * xpo;
  424.         final double ypo2 = ypo * ypo;
  425.         final double   alphap      = ypo2 + xpo2;
  426.         final double   gammap      = 1. - ypo2;

  427.         // Compute the roots
  428.         // We know there are two solutions as we already checked the point is outside ellipsoid
  429.         final double sqrt = FastMath.sqrt(alphap - 1);
  430.         final double sqrtp = FastMath.abs(ypo) * sqrt;
  431.         final double sqrtSigned = FastMath.copySign(sqrt, ypo);

  432.         // Compute the roots (ordered by value)
  433.         final double   xpt1;
  434.         final double   xpt2;
  435.         final double   ypt1;
  436.         final double   ypt2;
  437.         if (xpo > 0) {
  438.             final double s = xpo + sqrtp;
  439.             // xpt1 = (beta' + sqrt(delta')) / alpha' (with beta' = x'o)
  440.             xpt1 = s / alphap;
  441.             // x't2 = gamma' / (beta' + sqrt(delta')) since x't1 * x't2 = gamma' / alpha'
  442.             xpt2 = gammap / s;
  443.             // Get the corresponding values of y't
  444.             ypt1 = (ypo - xpo * sqrtSigned) / alphap;
  445.             ypt2 = (xpo * ypo + sqrtSigned) / s;
  446.         } else {
  447.             final double s = xpo - sqrtp;
  448.             // x't1 and x't2 are reverted compared to previous solution
  449.             xpt1 = gammap / s;
  450.             xpt2 = s / alphap;
  451.             // Get the corresponding values of y't
  452.             ypt2 = (ypo + xpo * sqrtSigned) / alphap;
  453.             ypt1 = (xpo * ypo - sqrtSigned) / s;
  454.         }

  455.         // De-normalize and express the two solutions in 3D
  456.         final Vector3D tp1 = section.toSpace(new Vector2D(ap * xpt1, bp * ypt1));
  457.         final Vector3D tp2 = section.toSpace(new Vector2D(ap * xpt2, bp * ypt2));

  458.         // Return the limb point in the direction of the outside point
  459.         return Vector3D.distance(tp1, outside) <= Vector3D.distance(tp2, outside) ? tp1 : tp2;

  460.     }

  461.     /** Find a point on ellipsoid limb, as seen by an external observer.
  462.      * @param observer observer position in ellipsoid frame
  463.      * @param outside point outside ellipsoid in ellipsoid frame, defining the phase around limb
  464.      * @return point on ellipsoid limb
  465.      * @exception MathRuntimeException if ellipsoid center, observer and outside
  466.      * points are aligned
  467.      * @param <T> the type of the field elements
  468.      * @since 12.0
  469.      */
  470.     public <T extends CalculusFieldElement<T>> FieldVector3D<T> pointOnLimb(final FieldVector3D<T> observer, final FieldVector3D<T> outside)
  471.         throws MathRuntimeException {

  472.         // There is no limb if we are inside the ellipsoid
  473.         if (isInside(observer)) {
  474.             throw new OrekitException(OrekitMessages.POINT_INSIDE_ELLIPSOID);
  475.         }
  476.         // Cut the ellipsoid, to find an elliptical plane section
  477.         final FieldVector3D<T> normal  = FieldVector3D.crossProduct(observer, outside);
  478.         final FieldEllipse<T>  section = getPlaneSection(FieldVector3D.getZero(observer.getX().getField()), normal);

  479.         // the point on limb is tangential to the ellipse
  480.         // if T(xt, yt) is an ellipse point at which the tangent is drawn
  481.         // if O(xo, yo) is a point outside of the ellipse belonging to the tangent at T,
  482.         // then the two following equations holds:
  483.         // (1) a² yt²   + b² xt²   = a² b²  (T belongs to the ellipse)
  484.         // (2) a² yt yo + b² xt xo = a² b²  (TP is tangent to the ellipse)
  485.         // using the second equation to eliminate yt from the first equation, we get
  486.         // b² (a² - xt xo)² + a² xt² yo² = a⁴ yo²
  487.         // (3) (a² yo² + b² xo²) xt² - 2 a² b² xo xt + a⁴ (b² - yo²) = 0
  488.         // which can easily be solved for xt

  489.         // To avoid numerical errors, the x and y coordinates in the ellipse plane are normalized using:
  490.         // x' = x / a and y' = y / b
  491.         //
  492.         // This gives:
  493.         // (1) y't² + x't² = 1
  494.         // (2) y't y'o + x't x'o = 1
  495.         //
  496.         // And finally:
  497.         // (3) (x'o² + y'o²) x't² - 2 x't x'o + 1 - y'o² = 0
  498.         //
  499.         // Solving for x't, we get the reduced discriminant:
  500.         // delta' = beta'² - alpha' * gamma'
  501.         //
  502.         // With:
  503.         // beta' = x'o
  504.         // alpha' = x'o² + y'o²
  505.         // gamma' = 1 - y'o²
  506.         //
  507.         // Simplifying to  cancel a term of x'o².
  508.         // delta' = y'o² (x'o² + y'o² - 1) = y'o² (alpha' - 1)
  509.         //
  510.         // After solving for xt1, xt2 using (3) the values are substituted into (2) to
  511.         // compute yt1, yt2. Then terms of x'o may be canceled from the expressions for
  512.         // yt1 and yt2. Additionally a point discontinuity is removed at y'o=0 from both
  513.         // yt1 and yt2.
  514.         //
  515.         // y't1 = (y'o - x'o d) / (x'o² + y'o²)
  516.         // y't2 = (x'o y'o + d) / (x + sqrt(delta'))
  517.         //
  518.         // where:
  519.         // d = sign(y'o) sqrt(alpha' - 1)

  520.         // Get the point in ellipse plane frame (2D)
  521.         final FieldVector2D<T> observer2D = section.toPlane(observer);

  522.         // Normalize and compute intermediary terms
  523.         final T ap     = section.getA();
  524.         final T bp     = section.getB();
  525.         final T xpo    = observer2D.getX().divide(ap);
  526.         final T ypo    = observer2D.getY().divide(bp);
  527.         final T xpo2   = xpo.multiply(xpo);
  528.         final T ypo2   = ypo.multiply(ypo);
  529.         final T alphap = ypo2.add(xpo2);
  530.         final T gammap = ypo2.negate().add(1);

  531.         // Compute the roots
  532.         // We know there are two solutions as we already checked the point is outside ellipsoid
  533.         final T sqrt = FastMath.sqrt(alphap.subtract(1));
  534.         final T sqrtp = FastMath.abs(ypo).multiply(sqrt);
  535.         final T sqrtSigned = FastMath.copySign(sqrt, ypo);

  536.         // Compute the roots (ordered by value)
  537.         final T   xpt1;
  538.         final T   xpt2;
  539.         final T   ypt1;
  540.         final T   ypt2;
  541.         if (xpo.getReal() > 0) {
  542.             final T s = xpo.add(sqrtp);
  543.             // xpt1 = (beta' + sqrt(delta')) / alpha' (with beta' = x'o)
  544.             xpt1 = s.divide(alphap);
  545.             // x't2 = gamma' / (beta' + sqrt(delta')) since x't1 * x't2 = gamma' / alpha'
  546.             xpt2 = gammap.divide(s);
  547.             // Get the corresponding values of y't
  548.             ypt1 = ypo.subtract(xpo.multiply(sqrtSigned)).divide(alphap);
  549.             ypt2 = xpo.multiply(ypo).add(sqrtSigned).divide(s);
  550.         } else {
  551.             final T s = xpo.subtract(sqrtp);
  552.             // x't1 and x't2 are reverted compared to previous solution
  553.             xpt1 = gammap.divide(s);
  554.             xpt2 = s.divide(alphap);
  555.             // Get the corresponding values of y't
  556.             ypt2 = ypo.add(xpo.multiply(sqrtSigned)).divide(alphap);
  557.             ypt1 = xpo.multiply(ypo).subtract(sqrtSigned).divide(s);
  558.         }

  559.         // De-normalize and express the two solutions in 3D
  560.         final FieldVector3D<T> tp1 = section.toSpace(new FieldVector2D<>(ap.multiply(xpt1), bp.multiply(ypt1)));
  561.         final FieldVector3D<T> tp2 = section.toSpace(new FieldVector2D<>(ap.multiply(xpt2), bp.multiply(ypt2)));

  562.         // Return the limb point in the direction of the outside point
  563.         return FieldVector3D.distance(tp1, outside).subtract(FieldVector3D.distance(tp2, outside)).getReal() <= 0 ? tp1 : tp2;

  564.     }

  565. }