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 }