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