Geoid.java

  1. /* Contributed in the public domain.
  2.  * Licensed to CS Systèmes d'Information (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.models.earth;

  18. import org.hipparchus.Field;
  19. import org.hipparchus.RealFieldElement;
  20. import org.hipparchus.analysis.RealFieldUnivariateFunction;
  21. import org.hipparchus.analysis.UnivariateFunction;
  22. import org.hipparchus.analysis.solvers.AllowedSolution;
  23. import org.hipparchus.analysis.solvers.BracketingNthOrderBrentSolver;
  24. import org.hipparchus.analysis.solvers.FieldBracketingNthOrderBrentSolver;
  25. import org.hipparchus.analysis.solvers.UnivariateSolver;
  26. import org.hipparchus.exception.MathRuntimeException;
  27. import org.hipparchus.geometry.euclidean.threed.FieldLine;
  28. import org.hipparchus.geometry.euclidean.threed.FieldVector3D;
  29. import org.hipparchus.geometry.euclidean.threed.Line;
  30. import org.hipparchus.geometry.euclidean.threed.Vector3D;
  31. import org.hipparchus.util.FastMath;
  32. import org.orekit.bodies.FieldGeodeticPoint;
  33. import org.orekit.bodies.GeodeticPoint;
  34. import org.orekit.errors.OrekitException;
  35. import org.orekit.forces.gravity.HolmesFeatherstoneAttractionModel;
  36. import org.orekit.forces.gravity.potential.NormalizedSphericalHarmonicsProvider;
  37. import org.orekit.forces.gravity.potential.TideSystem;
  38. import org.orekit.frames.FieldTransform;
  39. import org.orekit.frames.Frame;
  40. import org.orekit.frames.Transform;
  41. import org.orekit.time.AbsoluteDate;
  42. import org.orekit.time.FieldAbsoluteDate;
  43. import org.orekit.utils.TimeStampedPVCoordinates;

  44. /**
  45.  * A geoid is a level surface of the gravity potential of a body. The gravity
  46.  * potential, W, is split so W = U + T, where U is the normal potential (defined
  47.  * by the ellipsoid) and T is the anomalous potential.[3](eq. 2-137)
  48.  *
  49.  * <p> The {@link #getIntersectionPoint(Line, Vector3D, Frame, AbsoluteDate)}
  50.  * method is tailored specifically for Earth's geoid. All of the other methods
  51.  * in this class are general and will work for an arbitrary body.
  52.  *
  53.  * <p> There are several components that are needed to define a geoid[1]:
  54.  *
  55.  * <ul> <li>Geopotential field. These are the coefficients of the spherical
  56.  * harmonics: S<sub>n,m</sub> and C<sub>n,m</sub></li>
  57.  *
  58.  * <li>Reference Ellipsoid. The ellipsoid is used to define the undulation of
  59.  * the geoid (distance between ellipsoid and geoid) and U<sub>0</sub> the value
  60.  * of the normal gravity potential at the surface of the ellipsoid.</li>
  61.  *
  62.  * <li>W<sub>0</sub>, the potential at the geoid. The value of the potential on
  63.  * the level surface. This is taken to be U<sub>0</sub>, the normal gravity
  64.  * potential at the surface of the {@link ReferenceEllipsoid}.</li>
  65.  *
  66.  * <li>Permanent Tide System. This implementation assumes that the geopotential
  67.  * field and the reference ellipsoid use the same permanent tide system. If the
  68.  * assumption is false it will produce errors of about 0.5 m. Conversion between
  69.  * tide systems is a possible improvement.[1,2]</li>
  70.  *
  71.  * <li>Topographic Masses. That is mass outside of the geoid, e.g. mountains.
  72.  * This implementation ignores topographic masses, which causes up to 3m error
  73.  * in the Himalayas, and ~ 1.5m error in the Rockies. This could be improved
  74.  * through the use of DTED and calculating height anomalies or using the
  75.  * correction coefficients.[1]</li> </ul>
  76.  *
  77.  * <p> This implementation also assumes that the normal to the reference
  78.  * ellipsoid is the same as the normal to the geoid. This assumption enables the
  79.  * equation: (height above geoid) = (height above ellipsoid) - (undulation),
  80.  * which is used in {@link #transform(GeodeticPoint)} and {@link
  81.  * #transform(Vector3D, Frame, AbsoluteDate)}.
  82.  *
  83.  * <p> In testing, the error in the undulations calculated by this class were
  84.  * off by less than 3 meters, which matches the assumptions outlined above.
  85.  *
  86.  * <p> References:
  87.  *
  88.  * <ol> <li>Dru A. Smith. There is no such thing as "The" EGM96 geoid: Subtle
  89.  * points on the use of a global geopotential model. IGeS Bulletin No. 8:17-28,
  90.  * 1998. <a href= "http://www.ngs.noaa.gov/PUBS_LIB/EGM96_GEOID_PAPER/egm96_geoid_paper.html"
  91.  * >http ://www.ngs.noaa.gov/PUBS_LIB/EGM96_GEOID_PAPER/egm96_geoid_paper.html</a></li>
  92.  *
  93.  * <li> Martin Losch, Verena Seufer. How to Compute Geoid Undulations (Geoid
  94.  * Height Relative to a Given Reference Ellipsoid) from Spherical Harmonic
  95.  * Coefficients for Satellite Altimetry Applications. , 2003. <a
  96.  * href="http://mitgcm.org/~mlosch/geoidcookbook.pdf">mitgcm.org/~mlosch/geoidcookbook.pdf</a>
  97.  * </li>
  98.  *
  99.  * <li>Weikko A. Heiskanen, Helmut Moritz. Physical Geodesy. W. H. Freeman and
  100.  * Company, 1967. (especially sections 2.13 and equation 2-144 Bruns
  101.  * Formula)</li>
  102.  *
  103.  * <li>S. A. Holmes, W. E. Featherstone. A unified approach to the Clenshaw
  104.  * summation and the recursive computation of very high degree and order
  105.  * normalised associated Legendre functions. Journal of Geodesy, 76(5):279,
  106.  * 2002.</li>
  107.  *
  108.  * <li>DMA TR 8350.2. 1984.</li>
  109.  *
  110.  * <li>Department of Defense World Geodetic System 1984. 2000. NIMA TR 8350.2
  111.  * Third Edition, Amendment 1.</li> </ol>
  112.  *
  113.  * @author Evan Ward
  114.  */
  115. public class Geoid implements EarthShape {

  116.     /**
  117.      * uid is date of last modification.
  118.      */
  119.     private static final long serialVersionUID = 20150312L;

  120.     /**
  121.      * A number larger than the largest undulation. Wikipedia says the geoid
  122.      * height is in [-106, 85]. I chose 100 to be safe.
  123.      */
  124.     private static final double MAX_UNDULATION = 100;
  125.     /**
  126.      * A number smaller than the smallest undulation. Wikipedia says the geoid
  127.      * height is in [-106, 85]. I chose -150 to be safe.
  128.      */
  129.     private static final double MIN_UNDULATION = -150;
  130.     /**
  131.      * the maximum number of evaluations for the line search in {@link
  132.      * #getIntersectionPoint(Line, Vector3D, Frame, AbsoluteDate)}.
  133.      */
  134.     private static final int MAX_EVALUATIONS = 100;

  135.     /**
  136.      * the default date to use when evaluating the {@link #harmonics}. Used when
  137.      * no other dates are available. Should be removed in a future release.
  138.      */
  139.     private final AbsoluteDate defaultDate;
  140.     /**
  141.      * the reference ellipsoid.
  142.      */
  143.     private final ReferenceEllipsoid referenceEllipsoid;
  144.     /**
  145.      * the geo-potential combined with an algorithm for evaluating the spherical
  146.      * harmonics. The Holmes and Featherstone method is very robust.
  147.      */
  148.     private final transient HolmesFeatherstoneAttractionModel harmonics;

  149.     /**
  150.      * Creates a geoid from the given geopotential, reference ellipsoid and the
  151.      * assumptions in the comment for {@link Geoid}.
  152.      *
  153.      * @param geopotential       the gravity potential. Only the anomalous
  154.      *                           potential will be used. It is assumed that the
  155.      *                           {@code geopotential} and the {@code
  156.      *                           referenceEllipsoid} are defined in the same
  157.      *                           frame. Usually a {@link org.orekit.forces.gravity.potential.GravityFieldFactory#getConstantNormalizedProvider(int,
  158.      *                           int) constant geopotential} is used to define a
  159.      *                           time-invariant Geoid.
  160.      * @param referenceEllipsoid the normal gravity potential.
  161.      * @throws NullPointerException if {@code geopotential == null ||
  162.      *                              referenceEllipsoid == null}
  163.      */
  164.     public Geoid(final NormalizedSphericalHarmonicsProvider geopotential,
  165.                  final ReferenceEllipsoid referenceEllipsoid) {
  166.         // parameter check
  167.         if (geopotential == null || referenceEllipsoid == null) {
  168.             throw new NullPointerException();
  169.         }

  170.         // subtract the ellipsoid from the geopotential
  171.         final SubtractEllipsoid potential = new SubtractEllipsoid(geopotential,
  172.                 referenceEllipsoid);

  173.         // set instance parameters
  174.         this.referenceEllipsoid = referenceEllipsoid;
  175.         this.harmonics = new HolmesFeatherstoneAttractionModel(
  176.                 referenceEllipsoid.getBodyFrame(), potential);
  177.         this.defaultDate = geopotential.getReferenceDate();
  178.     }

  179.     @Override
  180.     public Frame getBodyFrame() {
  181.         // same as for reference ellipsoid.
  182.         return this.getEllipsoid().getBodyFrame();
  183.     }

  184.     /**
  185.      * Gets the Undulation of the Geoid, N at the given position. N is the
  186.      * distance between the {@link #getEllipsoid() reference ellipsoid} and the
  187.      * geoid. The latitude and longitude parameters are both defined with
  188.      * respect to the reference ellipsoid. For EGM96 and the WGS84 ellipsoid the
  189.      * undulation is between -107m and +86m.
  190.      *
  191.      * <p> NOTE: Restrictions are not put on the range of the arguments {@code
  192.      * geodeticLatitude} and {@code longitude}.
  193.      *
  194.      * @param geodeticLatitude geodetic latitude (angle between the local normal
  195.      *                         and the equatorial plane on the reference
  196.      *                         ellipsoid), in radians.
  197.      * @param longitude        on the reference ellipsoid, in radians.
  198.      * @param date             of evaluation. Used for time varying geopotential
  199.      *                         fields.
  200.      * @return the undulation in m, positive means the geoid is higher than the
  201.      * ellipsoid.
  202.      * @throws OrekitException if an error occurs converting latitude and
  203.      *                         longitude
  204.      * @see Geoid
  205.      * @see <a href="http://en.wikipedia.org/wiki/Geoid">Geoid on Wikipedia</a>
  206.      */
  207.     public double getUndulation(final double geodeticLatitude,
  208.                                 final double longitude,
  209.                                 final AbsoluteDate date) throws OrekitException {
  210.             /*
  211.              * equations references are to the algorithm printed in the geoid
  212.              * cookbook[2]. See comment for Geoid.
  213.              */
  214.         // reference ellipsoid
  215.         final ReferenceEllipsoid ellipsoid = this.getEllipsoid();

  216.         // position in geodetic coordinates
  217.         final GeodeticPoint gp = new GeodeticPoint(geodeticLatitude, longitude, 0);
  218.         // position in Cartesian coordinates, is converted to geocentric lat and
  219.         // lon in the Holmes and Featherstone class
  220.         final Vector3D position = ellipsoid.transform(gp);

  221.         // get normal gravity from ellipsoid, eq 15
  222.         final double normalGravity = ellipsoid
  223.                 .getNormalGravity(geodeticLatitude);

  224.         // calculate disturbing potential, T, eq 30.
  225.         final double mu = this.harmonics.getMu();
  226.         final double T  = this.harmonics.nonCentralPart(date, position, mu);
  227.         // calculate undulation, eq 30
  228.         return T / normalGravity;
  229.     }

  230.     @Override
  231.     public ReferenceEllipsoid getEllipsoid() {
  232.         return this.referenceEllipsoid;
  233.     }

  234.     /**
  235.      * This class implements equations 20-24 in the geoid cook book.(Losch and
  236.      * Seufer) It modifies C<sub>2n,0</sub> where n = 1,2,...,5.
  237.      *
  238.      * @see "DMA TR 8350.2. 1984."
  239.      */
  240.     private static final class SubtractEllipsoid implements
  241.             NormalizedSphericalHarmonicsProvider {
  242.         /**
  243.          * provider of the fully normalized coefficients, includes the reference
  244.          * ellipsoid.
  245.          */
  246.         private final NormalizedSphericalHarmonicsProvider provider;
  247.         /**
  248.          * the reference ellipsoid to subtract from {@link #provider}.
  249.          */
  250.         private final ReferenceEllipsoid ellipsoid;

  251.         /**
  252.          * @param provider  potential used for GM<sub>g</sub> and a<sub>g</sub>,
  253.          *                  and of course the coefficients Cnm, and Snm.
  254.          * @param ellipsoid Used to calculate the fully normalized
  255.          *                  J<sub>2n</sub>
  256.          */
  257.         private SubtractEllipsoid(
  258.                 final NormalizedSphericalHarmonicsProvider provider,
  259.                 final ReferenceEllipsoid ellipsoid) {
  260.             super();
  261.             this.provider = provider;
  262.             this.ellipsoid = ellipsoid;
  263.         }

  264.         @Override
  265.         public int getMaxDegree() {
  266.             return this.provider.getMaxDegree();
  267.         }

  268.         @Override
  269.         public int getMaxOrder() {
  270.             return this.provider.getMaxOrder();
  271.         }

  272.         @Override
  273.         public double getMu() {
  274.             return this.provider.getMu();
  275.         }

  276.         @Override
  277.         public double getAe() {
  278.             return this.provider.getAe();
  279.         }

  280.         @Override
  281.         public AbsoluteDate getReferenceDate() {
  282.             return this.provider.getReferenceDate();
  283.         }

  284.         @Override
  285.         public double getOffset(final AbsoluteDate date) {
  286.             return this.provider.getOffset(date);
  287.         }

  288.         @Override
  289.         public NormalizedSphericalHarmonics onDate(final AbsoluteDate date)
  290.             throws OrekitException {
  291.             return new NormalizedSphericalHarmonics() {

  292.                 /** the original harmonics */
  293.                 private final NormalizedSphericalHarmonics delegate = provider.onDate(date);

  294.                 @Override
  295.                 public double getNormalizedCnm(final int n, final int m) throws OrekitException {
  296.                     return getCorrectedCnm(n, m, this.delegate.getNormalizedCnm(n, m));
  297.                 }

  298.                 @Override
  299.                 public double getNormalizedSnm(final int n, final int m) throws OrekitException {
  300.                     return this.delegate.getNormalizedSnm(n, m);
  301.                 }

  302.                 @Override
  303.                 public AbsoluteDate getDate() {
  304.                     return date;
  305.                 }
  306.             };
  307.         }

  308.         /**
  309.          * Get the corrected Cnm for different GM or a values.
  310.          *
  311.          * @param n              degree
  312.          * @param m              order
  313.          * @param uncorrectedCnm uncorrected Cnm coefficient
  314.          * @return the corrected Cnm coefficient.
  315.          */
  316.         private double getCorrectedCnm(final int n,
  317.                                        final int m,
  318.                                        final double uncorrectedCnm) {
  319.             double Cnm = uncorrectedCnm;
  320.             // n = 2,4,6,8, or 10 and m = 0
  321.             if (m == 0 && n <= 10 && n % 2 == 0 && n > 0) {
  322.                 // correction factor for different GM and a, 1 if no difference
  323.                 final double gmRatio = this.ellipsoid.getGM() / this.getMu();
  324.                 final double aRatio = this.ellipsoid.getEquatorialRadius() /
  325.                         this.getAe();
  326.                 /*
  327.                  * eq 20 in the geoid cook book[2], with eq 3-61 in chapter 3 of
  328.                  * DMA TR 8350.2
  329.                  */
  330.                 // halfN = 1,2,3,4,5 for n = 2,4,6,8,10, respectively
  331.                 final int halfN = n / 2;
  332.                 Cnm = Cnm - gmRatio * FastMath.pow(aRatio, halfN) *
  333.                         this.ellipsoid.getC2n0(halfN);
  334.             }
  335.             // return is a modified Cnm
  336.             return Cnm;
  337.         }

  338.         @Override
  339.         public TideSystem getTideSystem() {
  340.             return this.provider.getTideSystem();
  341.         }

  342.     }

  343.     /**
  344.      * {@inheritDoc}
  345.      *
  346.      * <p> The intersection point is computed using a line search along the
  347.      * specified line. This is accurate when the geoid is slowly varying.
  348.      */
  349.     @Override
  350.     public GeodeticPoint getIntersectionPoint(final Line lineInFrame,
  351.                                               final Vector3D closeInFrame,
  352.                                               final Frame frame,
  353.                                               final AbsoluteDate date)
  354.         throws OrekitException {
  355.         /*
  356.          * It is assumed that the geoid is slowly varying over it's entire
  357.          * surface. Therefore there will one local intersection.
  358.          */
  359.         // transform to body frame
  360.         final Frame bodyFrame = this.getBodyFrame();
  361.         final Transform frameToBody = frame.getTransformTo(bodyFrame, date);
  362.         final Vector3D close = frameToBody.transformPosition(closeInFrame);
  363.         final Line lineInBodyFrame = frameToBody.transformLine(lineInFrame);

  364.         // set the line's direction so the solved for value is always positive
  365.         final Line line;
  366.         if (lineInBodyFrame.getAbscissa(close) < 0) {
  367.             line = lineInBodyFrame.revert();
  368.         } else {
  369.             line = lineInBodyFrame;
  370.         }

  371.         final ReferenceEllipsoid ellipsoid = this.getEllipsoid();
  372.         // calculate end points
  373.         // distance from line to center of earth, squared
  374.         final double d2 = line.pointAt(0.0).getNormSq();
  375.         // the minimum abscissa, squared
  376.         final double n = ellipsoid.getPolarRadius() + MIN_UNDULATION;
  377.         final double minAbscissa2 = n * n - d2;
  378.         // smaller end point of the interval = 0.0 or intersection with
  379.         // min_undulation sphere
  380.         final double lowPoint = FastMath.sqrt(FastMath.max(minAbscissa2, 0.0));
  381.         // the maximum abscissa, squared
  382.         final double x = ellipsoid.getEquatorialRadius() + MAX_UNDULATION;
  383.         final double maxAbscissa2 = x * x - d2;
  384.         // larger end point of the interval
  385.         final double highPoint = FastMath.sqrt(maxAbscissa2);

  386.         // line search function
  387.         final UnivariateFunction heightFunction = new UnivariateFunction() {
  388.             @Override
  389.             public double value(final double x) {
  390.                 try {
  391.                     final GeodeticPoint geodetic =
  392.                             transform(line.pointAt(x), bodyFrame, date);
  393.                     return geodetic.getAltitude();
  394.                 } catch (OrekitException e) {
  395.                     // due to frame transform -> re-throw
  396.                     throw new RuntimeException(e);
  397.                 }
  398.             }
  399.         };

  400.         // compute answer
  401.         if (maxAbscissa2 < 0) {
  402.             // ray does not pierce bounding sphere -> no possible intersection
  403.             return null;
  404.         }
  405.         // solve line search problem to find the intersection
  406.         final UnivariateSolver solver = new BracketingNthOrderBrentSolver();
  407.         try {
  408.             final double abscissa = solver.solve(MAX_EVALUATIONS, heightFunction, lowPoint, highPoint);
  409.             // return intersection point
  410.             return this.transform(line.pointAt(abscissa), bodyFrame, date);
  411.         } catch (MathRuntimeException e) {
  412.             // no intersection
  413.             return null;
  414.         }
  415.     }

  416.     @Override
  417.     public Vector3D projectToGround(final Vector3D point,
  418.                                     final AbsoluteDate date,
  419.                                     final Frame frame) throws OrekitException {
  420.         final GeodeticPoint gp = this.transform(point, frame, date);
  421.         final GeodeticPoint gpZero =
  422.                 new GeodeticPoint(gp.getLatitude(), gp.getLongitude(), 0);
  423.         final Transform bodyToFrame = this.getBodyFrame().getTransformTo(frame, date);
  424.         return bodyToFrame.transformPosition(this.transform(gpZero));
  425.     }

  426.     /**
  427.      * {@inheritDoc}
  428.      *
  429.      * <p> The intersection point is computed using a line search along the
  430.      * specified line. This is accurate when the geoid is slowly varying.
  431.      */
  432.     @Override
  433.     public <T extends RealFieldElement<T>> FieldGeodeticPoint<T> getIntersectionPoint(final FieldLine<T> lineInFrame,
  434.                                                                                       final FieldVector3D<T> closeInFrame,
  435.                                                                                       final Frame frame,
  436.                                                                                       final FieldAbsoluteDate<T> date)
  437.         throws OrekitException {

  438.         final Field<T> field = date.getField();
  439.         /*
  440.          * It is assumed that the geoid is slowly varying over it's entire
  441.          * surface. Therefore there will one local intersection.
  442.          */
  443.         // transform to body frame
  444.         final Frame bodyFrame = this.getBodyFrame();
  445.         final FieldTransform<T> frameToBody = frame.getTransformTo(bodyFrame, date);
  446.         final FieldVector3D<T> close = frameToBody.transformPosition(closeInFrame);
  447.         final FieldLine<T> lineInBodyFrame = frameToBody.transformLine(lineInFrame);

  448.         // set the line's direction so the solved for value is always positive
  449.         final FieldLine<T> line;
  450.         if (lineInBodyFrame.getAbscissa(close).getReal() < 0) {
  451.             line = lineInBodyFrame.revert();
  452.         } else {
  453.             line = lineInBodyFrame;
  454.         }

  455.         final ReferenceEllipsoid ellipsoid = this.getEllipsoid();
  456.         // calculate end points
  457.         // distance from line to center of earth, squared
  458.         final T d2 = line.pointAt(0.0).getNormSq();
  459.         // the minimum abscissa, squared
  460.         final double n = ellipsoid.getPolarRadius() + MIN_UNDULATION;
  461.         final T minAbscissa2 = d2.negate().add(n * n);
  462.         // smaller end point of the interval = 0.0 or intersection with
  463.         // min_undulation sphere
  464.         final T lowPoint = minAbscissa2.getReal() < 0 ? field.getZero() : minAbscissa2.sqrt();
  465.         // the maximum abscissa, squared
  466.         final double x = ellipsoid.getEquatorialRadius() + MAX_UNDULATION;
  467.         final T maxAbscissa2 = d2.negate().add(x * x);
  468.         // larger end point of the interval
  469.         final T highPoint = maxAbscissa2.sqrt();

  470.         // line search function
  471.         final RealFieldUnivariateFunction<T> heightFunction = z -> {
  472.             try {
  473.                 final FieldGeodeticPoint<T> geodetic =
  474.                         transform(line.pointAt(z), bodyFrame, date);
  475.                 return geodetic.getAltitude();
  476.             } catch (OrekitException e) {
  477.                 // due to frame transform -> re-throw
  478.                 throw new RuntimeException(e);
  479.             }
  480.         };

  481.         // compute answer
  482.         if (maxAbscissa2.getReal() < 0) {
  483.             // ray does not pierce bounding sphere -> no possible intersection
  484.             return null;
  485.         }
  486.         // solve line search problem to find the intersection
  487.         final FieldBracketingNthOrderBrentSolver<T> solver =
  488.                         new FieldBracketingNthOrderBrentSolver<>(field.getZero().add(1.0e-14),
  489.                                                                  field.getZero().add(1.0e-6),
  490.                                                                  field.getZero().add(1.0e-15),
  491.                                                                  5);
  492.         try {
  493.             final T abscissa = solver.solve(MAX_EVALUATIONS, heightFunction, lowPoint, highPoint,
  494.                                             AllowedSolution.ANY_SIDE);
  495.             // return intersection point
  496.             return this.transform(line.pointAt(abscissa), bodyFrame, date);
  497.         } catch (MathRuntimeException e) {
  498.             // no intersection
  499.             return null;
  500.         }
  501.     }

  502.     @Override
  503.     public TimeStampedPVCoordinates projectToGround(
  504.             final TimeStampedPVCoordinates pv,
  505.             final Frame frame) throws OrekitException {
  506.         throw new UnsupportedOperationException();
  507.     }

  508.     /**
  509.      * {@inheritDoc}
  510.      *
  511.      * @param date date of the conversion. Used for computing frame
  512.      *             transformations and for time dependent geopotential.
  513.      * @return The surface relative point at the same location. Altitude is
  514.      * orthometric height, that is height above the {@link Geoid}. Latitude and
  515.      * longitude are both geodetic and defined with respect to the {@link
  516.      * #getEllipsoid() reference ellipsoid}.
  517.      * @see #transform(GeodeticPoint)
  518.      * @see <a href="http://en.wikipedia.org/wiki/Orthometric_height">Orthometric_height</a>
  519.      */
  520.     @Override
  521.     public GeodeticPoint transform(final Vector3D point, final Frame frame,
  522.                                    final AbsoluteDate date) throws OrekitException {
  523.         // convert using reference ellipsoid, altitude referenced to ellipsoid
  524.         final GeodeticPoint ellipsoidal = this.getEllipsoid().transform(
  525.                 point, frame, date);
  526.         // convert altitude to orthometric using the undulation.
  527.         final double undulation = this.getUndulation(ellipsoidal.getLatitude(),
  528.                 ellipsoidal.getLongitude(), date);
  529.         // add undulation to the altitude
  530.         return new GeodeticPoint(
  531.                 ellipsoidal.getLatitude(),
  532.                 ellipsoidal.getLongitude(),
  533.                 ellipsoidal.getAltitude() - undulation
  534.         );
  535.     }

  536.     /**
  537.      * {@inheritDoc}
  538.      *
  539.      * @param date date of the conversion. Used for computing frame
  540.      *             transformations and for time dependent geopotential.
  541.      * @return The surface relative point at the same location. Altitude is
  542.      * orthometric height, that is height above the {@link Geoid}. Latitude and
  543.      * longitude are both geodetic and defined with respect to the {@link
  544.      * #getEllipsoid() reference ellipsoid}.
  545.      * @see #transform(GeodeticPoint)
  546.      * @see <a href="http://en.wikipedia.org/wiki/Orthometric_height">Orthometric_height</a>
  547.      */
  548.     @Override
  549.     public <T extends RealFieldElement<T>> FieldGeodeticPoint<T> transform(final FieldVector3D<T> point, final Frame frame,
  550.                                                                            final FieldAbsoluteDate<T> date)
  551.         throws OrekitException {
  552.         // convert using reference ellipsoid, altitude referenced to ellipsoid
  553.         final FieldGeodeticPoint<T> ellipsoidal = this.getEllipsoid().transform(
  554.                 point, frame, date);
  555.         // convert altitude to orthometric using the undulation.
  556.         final double undulation = this.getUndulation(ellipsoidal.getLatitude().getReal(),
  557.                                                      ellipsoidal.getLongitude().getReal(),
  558.                                                      date.toAbsoluteDate());
  559.         // add undulation to the altitude
  560.         return new FieldGeodeticPoint<>(
  561.                 ellipsoidal.getLatitude(),
  562.                 ellipsoidal.getLongitude(),
  563.                 ellipsoidal.getAltitude().subtract(undulation)
  564.         );
  565.     }

  566.     /**
  567.      * {@inheritDoc}
  568.      *
  569.      * @param point The surface relative point to transform. Altitude is
  570.      *              orthometric height, that is height above the {@link Geoid}.
  571.      *              Latitude and longitude are both geodetic and defined with
  572.      *              respect to the {@link #getEllipsoid() reference ellipsoid}.
  573.      * @return point at the same location but as a Cartesian point in the {@link
  574.      * #getBodyFrame() body frame}.
  575.      * @see #transform(Vector3D, Frame, AbsoluteDate)
  576.      */
  577.     @Override
  578.     public Vector3D transform(final GeodeticPoint point) {
  579.         try {
  580.             // convert orthometric height to height above ellipsoid using undulation
  581.             // TODO pass in date to allow user to specify
  582.             final double undulation = this.getUndulation(
  583.                     point.getLatitude(),
  584.                     point.getLongitude(),
  585.                     this.defaultDate
  586.             );
  587.             final GeodeticPoint ellipsoidal = new GeodeticPoint(
  588.                     point.getLatitude(),
  589.                     point.getLongitude(),
  590.                     point.getAltitude() + undulation
  591.             );
  592.             // transform using reference ellipsoid
  593.             return this.getEllipsoid().transform(ellipsoidal);
  594.         } catch (OrekitException e) {
  595.             //this method, as defined in BodyShape, is not permitted to throw
  596.             //an OrekitException, so wrap in an exception we can throw.
  597.             throw new RuntimeException(e);
  598.         }
  599.     }

  600.     /**
  601.      * {@inheritDoc}
  602.      *
  603.      * @param point The surface relative point to transform. Altitude is
  604.      *              orthometric height, that is height above the {@link Geoid}.
  605.      *              Latitude and longitude are both geodetic and defined with
  606.      *              respect to the {@link #getEllipsoid() reference ellipsoid}.
  607.      * @param <T> type of the field elements
  608.      * @return point at the same location but as a Cartesian point in the {@link
  609.      * #getBodyFrame() body frame}.
  610.      * @see #transform(Vector3D, Frame, AbsoluteDate)
  611.      * @since 9.0
  612.      */
  613.     @Override
  614.     public <T extends RealFieldElement<T>> FieldVector3D<T> transform(final FieldGeodeticPoint<T> point) {
  615.         try {
  616.             // convert orthometric height to height above ellipsoid using undulation
  617.             // TODO pass in date to allow user to specify
  618.             final double undulation = this.getUndulation(
  619.                     point.getLatitude().getReal(),
  620.                     point.getLongitude().getReal(),
  621.                     this.defaultDate
  622.             );
  623.             final FieldGeodeticPoint<T> ellipsoidal = new FieldGeodeticPoint<>(
  624.                     point.getLatitude(),
  625.                     point.getLongitude(),
  626.                     point.getAltitude().add(undulation)
  627.             );
  628.             // transform using reference ellipsoid
  629.             return this.getEllipsoid().transform(ellipsoidal);
  630.         } catch (OrekitException e) {
  631.             //this method, as defined in BodyShape, is not permitted to throw
  632.             //an OrekitException, so wrap in an exception we can throw.
  633.             throw new RuntimeException(e);
  634.         }
  635.     }

  636. }