1   /* Contributed in the public domain.
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.models.earth;
18  
19  import org.hipparchus.Field;
20  import org.hipparchus.CalculusFieldElement;
21  import org.hipparchus.analysis.CalculusFieldUnivariateFunction;
22  import org.hipparchus.analysis.UnivariateFunction;
23  import org.hipparchus.analysis.solvers.AllowedSolution;
24  import org.hipparchus.analysis.solvers.BracketingNthOrderBrentSolver;
25  import org.hipparchus.analysis.solvers.FieldBracketingNthOrderBrentSolver;
26  import org.hipparchus.analysis.solvers.UnivariateSolver;
27  import org.hipparchus.exception.MathRuntimeException;
28  import org.hipparchus.geometry.euclidean.threed.FieldLine;
29  import org.hipparchus.geometry.euclidean.threed.FieldVector3D;
30  import org.hipparchus.geometry.euclidean.threed.Line;
31  import org.hipparchus.geometry.euclidean.threed.Vector3D;
32  import org.hipparchus.util.FastMath;
33  import org.orekit.bodies.FieldGeodeticPoint;
34  import org.orekit.bodies.GeodeticPoint;
35  import org.orekit.errors.OrekitException;
36  import org.orekit.forces.gravity.HolmesFeatherstoneAttractionModel;
37  import org.orekit.forces.gravity.potential.GravityFields;
38  import org.orekit.forces.gravity.potential.NormalizedSphericalHarmonicsProvider;
39  import org.orekit.forces.gravity.potential.TideSystem;
40  import org.orekit.frames.FieldTransform;
41  import org.orekit.frames.Frame;
42  import org.orekit.frames.Transform;
43  import org.orekit.time.AbsoluteDate;
44  import org.orekit.time.FieldAbsoluteDate;
45  import org.orekit.utils.TimeStampedPVCoordinates;
46  
47  /**
48   * A geoid is a level surface of the gravity potential of a body. The gravity
49   * potential, W, is split so W = U + T, where U is the normal potential (defined
50   * by the ellipsoid) and T is the anomalous potential.[3](eq. 2-137)
51   *
52   * <p> The {@link #getIntersectionPoint(Line, Vector3D, Frame, AbsoluteDate)}
53   * method is tailored specifically for Earth's geoid. All of the other methods
54   * in this class are general and will work for an arbitrary body.
55   *
56   * <p> There are several components that are needed to define a geoid[1]:
57   *
58   * <ul> <li>Geopotential field. These are the coefficients of the spherical
59   * harmonics: S<sub>n,m</sub> and C<sub>n,m</sub></li>
60   *
61   * <li>Reference Ellipsoid. The ellipsoid is used to define the undulation of
62   * the geoid (distance between ellipsoid and geoid) and U<sub>0</sub> the value
63   * of the normal gravity potential at the surface of the ellipsoid.</li>
64   *
65   * <li>W<sub>0</sub>, the potential at the geoid. The value of the potential on
66   * the level surface. This is taken to be U<sub>0</sub>, the normal gravity
67   * potential at the surface of the {@link ReferenceEllipsoid}.</li>
68   *
69   * <li>Permanent Tide System. This implementation assumes that the geopotential
70   * field and the reference ellipsoid use the same permanent tide system. If the
71   * assumption is false it will produce errors of about 0.5 m. Conversion between
72   * tide systems is a possible improvement.[1,2]</li>
73   *
74   * <li>Topographic Masses. That is mass outside of the geoid, e.g. mountains.
75   * This implementation ignores topographic masses, which causes up to 3m error
76   * in the Himalayas, and ~ 1.5m error in the Rockies. This could be improved
77   * through the use of DTED and calculating height anomalies or using the
78   * correction coefficients.[1]</li> </ul>
79   *
80   * <p> This implementation also assumes that the normal to the reference
81   * ellipsoid is the same as the normal to the geoid. This assumption enables the
82   * equation: (height above geoid) = (height above ellipsoid) - (undulation),
83   * which is used in {@link #transform(GeodeticPoint)} and {@link
84   * #transform(Vector3D, Frame, AbsoluteDate)}.
85   *
86   * <p> In testing, the error in the undulations calculated by this class were
87   * off by less than 3 meters, which matches the assumptions outlined above.
88   *
89   * <p> References:
90   *
91   * <ol> <li>Dru A. Smith. There is no such thing as "The" EGM96 geoid: Subtle
92   * points on the use of a global geopotential model. IGeS Bulletin No. 8:17-28,
93   * 1998. <a href= "http://www.ngs.noaa.gov/PUBS_LIB/EGM96_GEOID_PAPER/egm96_geoid_paper.html"
94   * >http://www.ngs.noaa.gov/PUBS_LIB/EGM96_GEOID_PAPER/egm96_geoid_paper.html</a></li>
95   *
96   * <li> Martin Losch, Verena Seufer. How to Compute Geoid Undulations (Geoid
97   * Height Relative to a Given Reference Ellipsoid) from Spherical Harmonic
98   * Coefficients for Satellite Altimetry Applications. , 2003. <a
99   * href="http://mitgcm.org/~mlosch/geoidcookbook.pdf">mitgcm.org/~mlosch/geoidcookbook.pdf</a>
100  * </li>
101  *
102  * <li>Weikko A. Heiskanen, Helmut Moritz. Physical Geodesy. W. H. Freeman and
103  * Company, 1967. (especially sections 2.13 and equation 2-144 Bruns
104  * Formula)</li>
105  *
106  * <li>S. A. Holmes, W. E. Featherstone. A unified approach to the Clenshaw
107  * summation and the recursive computation of very high degree and order
108  * normalised associated Legendre functions. Journal of Geodesy, 76(5):279,
109  * 2002.</li>
110  *
111  * <li>DMA TR 8350.2. 1984.</li>
112  *
113  * <li>Department of Defense World Geodetic System 1984. 2000. NIMA TR 8350.2
114  * Third Edition, Amendment 1.</li> </ol>
115  *
116  * @author Evan Ward
117  */
118 public class Geoid implements EarthShape {
119 
120     /**
121      * uid is date of last modification.
122      */
123     private static final long serialVersionUID = 20150312L;
124 
125     /**
126      * A number larger than the largest undulation. Wikipedia says the geoid
127      * height is in [-106, 85]. I chose 100 to be safe.
128      */
129     private static final double MAX_UNDULATION = 100;
130     /**
131      * A number smaller than the smallest undulation. Wikipedia says the geoid
132      * height is in [-106, 85]. I chose -150 to be safe.
133      */
134     private static final double MIN_UNDULATION = -150;
135     /**
136      * the maximum number of evaluations for the line search in {@link
137      * #getIntersectionPoint(Line, Vector3D, Frame, AbsoluteDate)}.
138      */
139     private static final int MAX_EVALUATIONS = 100;
140 
141     /**
142      * the default date to use when evaluating the {@link #harmonics}. Used when
143      * no other dates are available. Should be removed in a future release.
144      */
145     private final AbsoluteDate defaultDate;
146     /**
147      * the reference ellipsoid.
148      */
149     private final ReferenceEllipsoid referenceEllipsoid;
150     /**
151      * the geo-potential combined with an algorithm for evaluating the spherical
152      * harmonics. The Holmes and Featherstone method is very robust.
153      */
154     private final transient HolmesFeatherstoneAttractionModel harmonics;
155 
156     /**
157      * Creates a geoid from the given geopotential, reference ellipsoid and the
158      * assumptions in the comment for {@link Geoid}.
159      *
160      * @param geopotential       the gravity potential. Only the anomalous
161      *                           potential will be used. It is assumed that the
162      *                           {@code geopotential} and the {@code
163      *                           referenceEllipsoid} are defined in the same
164      *                           frame. Usually a {@link GravityFields#getConstantNormalizedProvider(int,
165      *                           int) constant geopotential} is used to define a
166      *                           time-invariant Geoid.
167      * @param referenceEllipsoid the normal gravity potential.
168      * @throws NullPointerException if {@code geopotential == null ||
169      *                              referenceEllipsoid == null}
170      */
171     public Geoid(final NormalizedSphericalHarmonicsProvider geopotential,
172                  final ReferenceEllipsoid referenceEllipsoid) {
173         // parameter check
174         if (geopotential == null || referenceEllipsoid == null) {
175             throw new NullPointerException();
176         }
177 
178         // subtract the ellipsoid from the geopotential
179         final SubtractEllipsoid potential = new SubtractEllipsoid(geopotential,
180                 referenceEllipsoid);
181 
182         // set instance parameters
183         this.referenceEllipsoid = referenceEllipsoid;
184         this.harmonics = new HolmesFeatherstoneAttractionModel(
185                 referenceEllipsoid.getBodyFrame(), potential);
186         this.defaultDate = geopotential.getReferenceDate();
187     }
188 
189     @Override
190     public Frame getBodyFrame() {
191         // same as for reference ellipsoid.
192         return this.getEllipsoid().getBodyFrame();
193     }
194 
195     /**
196      * Gets the Undulation of the Geoid, N at the given position. N is the
197      * distance between the {@link #getEllipsoid() reference ellipsoid} and the
198      * geoid. The latitude and longitude parameters are both defined with
199      * respect to the reference ellipsoid. For EGM96 and the WGS84 ellipsoid the
200      * undulation is between -107m and +86m.
201      *
202      * <p> NOTE: Restrictions are not put on the range of the arguments {@code
203      * geodeticLatitude} and {@code longitude}.
204      *
205      * @param geodeticLatitude geodetic latitude (angle between the local normal
206      *                         and the equatorial plane on the reference
207      *                         ellipsoid), in radians.
208      * @param longitude        on the reference ellipsoid, in radians.
209      * @param date             of evaluation. Used for time varying geopotential
210      *                         fields.
211      * @return the undulation in m, positive means the geoid is higher than the
212      * ellipsoid.
213      * @see Geoid
214      * @see <a href="http://en.wikipedia.org/wiki/Geoid">Geoid on Wikipedia</a>
215      */
216     public double getUndulation(final double geodeticLatitude,
217                                 final double longitude,
218                                 final AbsoluteDate date) {
219         /*
220          * equations references are to the algorithm printed in the geoid
221          * cookbook[2]. See comment for Geoid.
222          */
223         // reference ellipsoid
224         final ReferenceEllipsoid ellipsoid = this.getEllipsoid();
225 
226         // position in geodetic coordinates
227         final GeodeticPoint gp = new GeodeticPoint(geodeticLatitude, longitude, 0);
228         // position in Cartesian coordinates, is converted to geocentric lat and
229         // lon in the Holmes and Featherstone class
230         final Vector3D position = ellipsoid.transform(gp);
231 
232         // get normal gravity from ellipsoid, eq 15
233         final double normalGravity = ellipsoid
234                 .getNormalGravity(geodeticLatitude);
235 
236         // calculate disturbing potential, T, eq 30.
237         final double mu = this.harmonics.getMu();
238         final double T  = this.harmonics.nonCentralPart(date, position, mu);
239         // calculate undulation, eq 30
240         return T / normalGravity;
241     }
242 
243     @Override
244     public ReferenceEllipsoid getEllipsoid() {
245         return this.referenceEllipsoid;
246     }
247 
248     /**
249      * This class implements equations 20-24 in the geoid cook book.(Losch and
250      * Seufer) It modifies C<sub>2n,0</sub> where n = 1,2,...,5.
251      *
252      * @see "DMA TR 8350.2. 1984."
253      */
254     private static final class SubtractEllipsoid implements
255             NormalizedSphericalHarmonicsProvider {
256         /**
257          * provider of the fully normalized coefficients, includes the reference
258          * ellipsoid.
259          */
260         private final NormalizedSphericalHarmonicsProvider provider;
261         /**
262          * the reference ellipsoid to subtract from {@link #provider}.
263          */
264         private final ReferenceEllipsoid ellipsoid;
265 
266         /**
267          * @param provider  potential used for GM<sub>g</sub> and a<sub>g</sub>,
268          *                  and of course the coefficients Cnm, and Snm.
269          * @param ellipsoid Used to calculate the fully normalized
270          *                  J<sub>2n</sub>
271          */
272         private SubtractEllipsoid(
273                 final NormalizedSphericalHarmonicsProvider provider,
274                 final ReferenceEllipsoid ellipsoid) {
275             super();
276             this.provider = provider;
277             this.ellipsoid = ellipsoid;
278         }
279 
280         @Override
281         public int getMaxDegree() {
282             return this.provider.getMaxDegree();
283         }
284 
285         @Override
286         public int getMaxOrder() {
287             return this.provider.getMaxOrder();
288         }
289 
290         @Override
291         public double getMu() {
292             return this.provider.getMu();
293         }
294 
295         @Override
296         public double getAe() {
297             return this.provider.getAe();
298         }
299 
300         @Override
301         public AbsoluteDate getReferenceDate() {
302             return this.provider.getReferenceDate();
303         }
304 
305         @Override
306         public double getOffset(final AbsoluteDate date) {
307             return this.provider.getOffset(date);
308         }
309 
310         @Override
311         public NormalizedSphericalHarmonics onDate(final AbsoluteDate date) {
312             return new NormalizedSphericalHarmonics() {
313 
314                 /** the original harmonics */
315                 private final NormalizedSphericalHarmonics delegate = provider.onDate(date);
316 
317                 @Override
318                 public double getNormalizedCnm(final int n, final int m) {
319                     return getCorrectedCnm(n, m, this.delegate.getNormalizedCnm(n, m));
320                 }
321 
322                 @Override
323                 public double getNormalizedSnm(final int n, final int m) {
324                     return this.delegate.getNormalizedSnm(n, m);
325                 }
326 
327                 @Override
328                 public AbsoluteDate getDate() {
329                     return date;
330                 }
331             };
332         }
333 
334         /**
335          * Get the corrected Cnm for different GM or a values.
336          *
337          * @param n              degree
338          * @param m              order
339          * @param uncorrectedCnm uncorrected Cnm coefficient
340          * @return the corrected Cnm coefficient.
341          */
342         private double getCorrectedCnm(final int n,
343                                        final int m,
344                                        final double uncorrectedCnm) {
345             double Cnm = uncorrectedCnm;
346             // n = 2,4,6,8, or 10 and m = 0
347             if (m == 0 && n <= 10 && n % 2 == 0 && n > 0) {
348                 // correction factor for different GM and a, 1 if no difference
349                 final double gmRatio = this.ellipsoid.getGM() / this.getMu();
350                 final double aRatio = this.ellipsoid.getEquatorialRadius() /
351                         this.getAe();
352                 /*
353                  * eq 20 in the geoid cook book[2], with eq 3-61 in chapter 3 of
354                  * DMA TR 8350.2
355                  */
356                 // halfN = 1,2,3,4,5 for n = 2,4,6,8,10, respectively
357                 final int halfN = n / 2;
358                 Cnm = Cnm - gmRatio * FastMath.pow(aRatio, halfN) *
359                         this.ellipsoid.getC2n0(halfN);
360             }
361             // return is a modified Cnm
362             return Cnm;
363         }
364 
365         @Override
366         public TideSystem getTideSystem() {
367             return this.provider.getTideSystem();
368         }
369 
370     }
371 
372     /**
373      * {@inheritDoc}
374      *
375      * <p> The intersection point is computed using a line search along the
376      * specified line. This is accurate when the geoid is slowly varying.
377      */
378     @Override
379     public GeodeticPoint getIntersectionPoint(final Line lineInFrame,
380                                               final Vector3D closeInFrame,
381                                               final Frame frame,
382                                               final AbsoluteDate date) {
383         /*
384          * It is assumed that the geoid is slowly varying over it's entire
385          * surface. Therefore there will one local intersection.
386          */
387         // transform to body frame
388         final Frame bodyFrame = this.getBodyFrame();
389         final Transform frameToBody = frame.getTransformTo(bodyFrame, date);
390         final Vector3D close = frameToBody.transformPosition(closeInFrame);
391         final Line lineInBodyFrame = frameToBody.transformLine(lineInFrame);
392 
393         // set the line's direction so the solved for value is always positive
394         final Line line;
395         if (lineInBodyFrame.getAbscissa(close) < 0) {
396             line = lineInBodyFrame.revert();
397         } else {
398             line = lineInBodyFrame;
399         }
400 
401         final ReferenceEllipsoid ellipsoid = this.getEllipsoid();
402         // calculate end points
403         // distance from line to center of earth, squared
404         final double d2 = line.pointAt(0.0).getNormSq();
405         // the minimum abscissa, squared
406         final double n = ellipsoid.getPolarRadius() + MIN_UNDULATION;
407         final double minAbscissa2 = n * n - d2;
408         // smaller end point of the interval = 0.0 or intersection with
409         // min_undulation sphere
410         final double lowPoint = FastMath.sqrt(FastMath.max(minAbscissa2, 0.0));
411         // the maximum abscissa, squared
412         final double x = ellipsoid.getEquatorialRadius() + MAX_UNDULATION;
413         final double maxAbscissa2 = x * x - d2;
414         // larger end point of the interval
415         final double highPoint = FastMath.sqrt(maxAbscissa2);
416 
417         // line search function
418         final UnivariateFunction heightFunction = new UnivariateFunction() {
419             @Override
420             public double value(final double x) {
421                 try {
422                     final GeodeticPoint geodetic =
423                             transform(line.pointAt(x), bodyFrame, date);
424                     return geodetic.getAltitude();
425                 } catch (OrekitException e) {
426                     // due to frame transform -> re-throw
427                     throw new RuntimeException(e);
428                 }
429             }
430         };
431 
432         // compute answer
433         if (maxAbscissa2 < 0) {
434             // ray does not pierce bounding sphere -> no possible intersection
435             return null;
436         }
437         // solve line search problem to find the intersection
438         final UnivariateSolver solver = new BracketingNthOrderBrentSolver();
439         try {
440             final double abscissa = solver.solve(MAX_EVALUATIONS, heightFunction, lowPoint, highPoint);
441             // return intersection point
442             return this.transform(line.pointAt(abscissa), bodyFrame, date);
443         } catch (MathRuntimeException e) {
444             // no intersection
445             return null;
446         }
447     }
448 
449     @Override
450     public Vector3D projectToGround(final Vector3D point,
451                                     final AbsoluteDate date,
452                                     final Frame frame) {
453         final GeodeticPoint gp = this.transform(point, frame, date);
454         final GeodeticPoint gpZero =
455                 new GeodeticPoint(gp.getLatitude(), gp.getLongitude(), 0);
456         final Transform bodyToFrame = this.getBodyFrame().getTransformTo(frame, date);
457         return bodyToFrame.transformPosition(this.transform(gpZero));
458     }
459 
460     /**
461      * {@inheritDoc}
462      *
463      * <p> The intersection point is computed using a line search along the
464      * specified line. This is accurate when the geoid is slowly varying.
465      */
466     @Override
467     public <T extends CalculusFieldElement<T>> FieldGeodeticPoint<T> getIntersectionPoint(final FieldLine<T> lineInFrame,
468                                                                                       final FieldVector3D<T> closeInFrame,
469                                                                                       final Frame frame,
470                                                                                       final FieldAbsoluteDate<T> date) {
471 
472         final Field<T> field = date.getField();
473         /*
474          * It is assumed that the geoid is slowly varying over it's entire
475          * surface. Therefore there will one local intersection.
476          */
477         // transform to body frame
478         final Frame bodyFrame = this.getBodyFrame();
479         final FieldTransform<T> frameToBody = frame.getTransformTo(bodyFrame, date);
480         final FieldVector3D<T> close = frameToBody.transformPosition(closeInFrame);
481         final FieldLine<T> lineInBodyFrame = frameToBody.transformLine(lineInFrame);
482 
483         // set the line's direction so the solved for value is always positive
484         final FieldLine<T> line;
485         if (lineInBodyFrame.getAbscissa(close).getReal() < 0) {
486             line = lineInBodyFrame.revert();
487         } else {
488             line = lineInBodyFrame;
489         }
490 
491         final ReferenceEllipsoid ellipsoid = this.getEllipsoid();
492         // calculate end points
493         // distance from line to center of earth, squared
494         final T d2 = line.pointAt(0.0).getNormSq();
495         // the minimum abscissa, squared
496         final double n = ellipsoid.getPolarRadius() + MIN_UNDULATION;
497         final T minAbscissa2 = d2.negate().add(n * n);
498         // smaller end point of the interval = 0.0 or intersection with
499         // min_undulation sphere
500         final T lowPoint = minAbscissa2.getReal() < 0 ? field.getZero() : minAbscissa2.sqrt();
501         // the maximum abscissa, squared
502         final double x = ellipsoid.getEquatorialRadius() + MAX_UNDULATION;
503         final T maxAbscissa2 = d2.negate().add(x * x);
504         // larger end point of the interval
505         final T highPoint = maxAbscissa2.sqrt();
506 
507         // line search function
508         final CalculusFieldUnivariateFunction<T> heightFunction = z -> {
509             try {
510                 final FieldGeodeticPoint<T> geodetic =
511                         transform(line.pointAt(z), bodyFrame, date);
512                 return geodetic.getAltitude();
513             } catch (OrekitException e) {
514                 // due to frame transform -> re-throw
515                 throw new RuntimeException(e);
516             }
517         };
518 
519         // compute answer
520         if (maxAbscissa2.getReal() < 0) {
521             // ray does not pierce bounding sphere -> no possible intersection
522             return null;
523         }
524         // solve line search problem to find the intersection
525         final FieldBracketingNthOrderBrentSolver<T> solver =
526                         new FieldBracketingNthOrderBrentSolver<>(field.getZero().add(1.0e-14),
527                                                                  field.getZero().add(1.0e-6),
528                                                                  field.getZero().add(1.0e-15),
529                                                                  5);
530         try {
531             final T abscissa = solver.solve(MAX_EVALUATIONS, heightFunction, lowPoint, highPoint,
532                                             AllowedSolution.ANY_SIDE);
533             // return intersection point
534             return this.transform(line.pointAt(abscissa), bodyFrame, date);
535         } catch (MathRuntimeException e) {
536             // no intersection
537             return null;
538         }
539     }
540 
541     @Override
542     public TimeStampedPVCoordinates projectToGround(
543             final TimeStampedPVCoordinates pv,
544             final Frame frame) {
545         throw new UnsupportedOperationException();
546     }
547 
548     /**
549      * {@inheritDoc}
550      *
551      * @param date date of the conversion. Used for computing frame
552      *             transformations and for time dependent geopotential.
553      * @return The surface relative point at the same location. Altitude is
554      * orthometric height, that is height above the {@link Geoid}. Latitude and
555      * longitude are both geodetic and defined with respect to the {@link
556      * #getEllipsoid() reference ellipsoid}.
557      * @see #transform(GeodeticPoint)
558      * @see <a href="http://en.wikipedia.org/wiki/Orthometric_height">Orthometric_height</a>
559      */
560     @Override
561     public GeodeticPoint transform(final Vector3D point, final Frame frame,
562                                    final AbsoluteDate date) {
563         // convert using reference ellipsoid, altitude referenced to ellipsoid
564         final GeodeticPoint ellipsoidal = this.getEllipsoid().transform(
565                 point, frame, date);
566         // convert altitude to orthometric using the undulation.
567         final double undulation = this.getUndulation(ellipsoidal.getLatitude(),
568                 ellipsoidal.getLongitude(), date);
569         // add undulation to the altitude
570         return new GeodeticPoint(
571                 ellipsoidal.getLatitude(),
572                 ellipsoidal.getLongitude(),
573                 ellipsoidal.getAltitude() - undulation
574         );
575     }
576 
577     /**
578      * {@inheritDoc}
579      *
580      * @param date date of the conversion. Used for computing frame
581      *             transformations and for time dependent geopotential.
582      * @return The surface relative point at the same location. Altitude is
583      * orthometric height, that is height above the {@link Geoid}. Latitude and
584      * longitude are both geodetic and defined with respect to the {@link
585      * #getEllipsoid() reference ellipsoid}.
586      * @see #transform(GeodeticPoint)
587      * @see <a href="http://en.wikipedia.org/wiki/Orthometric_height">Orthometric_height</a>
588      */
589     @Override
590     public <T extends CalculusFieldElement<T>> FieldGeodeticPoint<T> transform(final FieldVector3D<T> point, final Frame frame,
591                                                                            final FieldAbsoluteDate<T> date) {
592         // convert using reference ellipsoid, altitude referenced to ellipsoid
593         final FieldGeodeticPoint<T> ellipsoidal = this.getEllipsoid().transform(
594                 point, frame, date);
595         // convert altitude to orthometric using the undulation.
596         final double undulation = this.getUndulation(ellipsoidal.getLatitude().getReal(),
597                                                      ellipsoidal.getLongitude().getReal(),
598                                                      date.toAbsoluteDate());
599         // add undulation to the altitude
600         return new FieldGeodeticPoint<>(
601                 ellipsoidal.getLatitude(),
602                 ellipsoidal.getLongitude(),
603                 ellipsoidal.getAltitude().subtract(undulation)
604         );
605     }
606 
607     /**
608      * {@inheritDoc}
609      *
610      * @param point The surface relative point to transform. Altitude is
611      *              orthometric height, that is height above the {@link Geoid}.
612      *              Latitude and longitude are both geodetic and defined with
613      *              respect to the {@link #getEllipsoid() reference ellipsoid}.
614      * @return point at the same location but as a Cartesian point in the {@link
615      * #getBodyFrame() body frame}.
616      * @see #transform(Vector3D, Frame, AbsoluteDate)
617      */
618     @Override
619     public Vector3D transform(final GeodeticPoint point) {
620         try {
621             // convert orthometric height to height above ellipsoid using undulation
622             // TODO pass in date to allow user to specify
623             final double undulation = this.getUndulation(
624                     point.getLatitude(),
625                     point.getLongitude(),
626                     this.defaultDate
627             );
628             final GeodeticPoint ellipsoidal = new GeodeticPoint(
629                     point.getLatitude(),
630                     point.getLongitude(),
631                     point.getAltitude() + undulation
632             );
633             // transform using reference ellipsoid
634             return this.getEllipsoid().transform(ellipsoidal);
635         } catch (OrekitException e) {
636             //this method, as defined in BodyShape, is not permitted to throw
637             //an OrekitException, so wrap in an exception we can throw.
638             throw new RuntimeException(e);
639         }
640     }
641 
642     /**
643      * {@inheritDoc}
644      *
645      * @param point The surface relative point to transform. Altitude is
646      *              orthometric height, that is height above the {@link Geoid}.
647      *              Latitude and longitude are both geodetic and defined with
648      *              respect to the {@link #getEllipsoid() reference ellipsoid}.
649      * @param <T> type of the field elements
650      * @return point at the same location but as a Cartesian point in the {@link
651      * #getBodyFrame() body frame}.
652      * @see #transform(Vector3D, Frame, AbsoluteDate)
653      * @since 9.0
654      */
655     @Override
656     public <T extends CalculusFieldElement<T>> FieldVector3D<T> transform(final FieldGeodeticPoint<T> point) {
657         try {
658             // convert orthometric height to height above ellipsoid using undulation
659             // TODO pass in date to allow user to specify
660             final double undulation = this.getUndulation(
661                     point.getLatitude().getReal(),
662                     point.getLongitude().getReal(),
663                     this.defaultDate
664             );
665             final FieldGeodeticPoint<T> ellipsoidal = new FieldGeodeticPoint<>(
666                     point.getLatitude(),
667                     point.getLongitude(),
668                     point.getAltitude().add(undulation)
669             );
670             // transform using reference ellipsoid
671             return this.getEllipsoid().transform(ellipsoidal);
672         } catch (OrekitException e) {
673             //this method, as defined in BodyShape, is not permitted to throw
674             //an OrekitException, so wrap in an exception we can throw.
675             throw new RuntimeException(e);
676         }
677     }
678 
679 }