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