Line |
Branch |
Exec |
Source |
1 |
|
|
// This file is part of INSTINCT, the INS Toolkit for Integrated |
2 |
|
|
// Navigation Concepts and Training by the Institute of Navigation of |
3 |
|
|
// the University of Stuttgart, Germany. |
4 |
|
|
|
5 |
|
|
/// @file EGM96.hpp |
6 |
|
|
/// @brief EGM96 geoid model |
7 |
|
|
/// @author T. Topp (topp@ins.uni-stuttgart.de) |
8 |
|
|
/// @date 2024-07-05 |
9 |
|
|
/// |
10 |
|
|
/// Source code: https://github.com/emericg/EGM96/blob/master/EGM96.c |
11 |
|
|
/// Copyright (c) 2006 D.Ineiev <ineiev@yahoo.co.uk> |
12 |
|
|
/// Copyright (c) 2020 Emeric Grange <emeric.grange@gmail.com> |
13 |
|
|
/// |
14 |
|
|
/// This software is provided 'as-is', without any express or implied warranty. |
15 |
|
|
/// In no event will the authors be held liable for any damages arising from |
16 |
|
|
/// the use of this software. |
17 |
|
|
/// |
18 |
|
|
/// Permission is granted to anyone to use this software for any purpose, |
19 |
|
|
/// including commercial applications, and to alter it and redistribute it |
20 |
|
|
/// freely, subject to the following restrictions: |
21 |
|
|
/// |
22 |
|
|
/// 1. The origin of this software must not be misrepresented; you must not |
23 |
|
|
/// claim that you wrote the original software. If you use this software |
24 |
|
|
/// in a product, an acknowledgment in the product documentation would be |
25 |
|
|
/// appreciated but is not required. |
26 |
|
|
/// 2. Altered source versions must be plainly marked as such, and must not be |
27 |
|
|
/// misrepresented as being the original software. |
28 |
|
|
/// 3. This notice may not be removed or altered from any source distribution. |
29 |
|
|
|
30 |
|
|
/* |
31 |
|
|
* This program is designed for the calculation of a geoid undulation at a point |
32 |
|
|
* whose latitude and longitude is specified. |
33 |
|
|
* |
34 |
|
|
* This program is designed to be used with the constants of EGM96 and those of |
35 |
|
|
* the WGS84(g873) system. The undulation will refer to the WGS84 ellipsoid. |
36 |
|
|
* |
37 |
|
|
* It's designed to use the potential coefficient model EGM96 and a set of |
38 |
|
|
* spherical harmonic coefficients of a correction term. |
39 |
|
|
* The correction term is composed of several different components, the primary |
40 |
|
|
* one being the conversion of a height anomaly to a geoid undulation. |
41 |
|
|
* The principles of this procedure were initially described in the paper: |
42 |
|
|
* - use of potential coefficient models for geoid undulation determination using |
43 |
|
|
* a spherical harmonic representation of the height anomaly/geoid undulation |
44 |
|
|
* difference by R.H. Rapp, Journal of Geodesy, 1996. |
45 |
|
|
* |
46 |
|
|
* This program is a modification of the program described in the following report: |
47 |
|
|
* - a fortran program for the computation of gravimetric quantities from high |
48 |
|
|
* degree spherical harmonic expansions, Richard H. Rapp, report 334, Department |
49 |
|
|
* of Geodetic Science and Surveying, the Ohio State University, Columbus, 1982 |
50 |
|
|
*/ |
51 |
|
|
/* |
52 |
|
|
* Changes: |
53 |
|
|
* - Auto-formatting |
54 |
|
|
* - Namespace NAV added |
55 |
|
|
* - Added "NOLINT" instructions to disable clang-tidy |
56 |
|
|
*/ |
57 |
|
|
|
58 |
|
|
#include "EGM96.hpp" |
59 |
|
|
#include "internal/EGM96_Data.hpp" |
60 |
|
|
#include <cmath> |
61 |
|
|
|
62 |
|
|
/* ************************************************************************** */ |
63 |
|
|
namespace NAV |
64 |
|
|
{ |
65 |
|
|
#define _coeffs (65341) //!< Size of correction and harmonic coefficients arrays (361*181) |
66 |
|
|
#define _nmax (360) //!< Maximum degree and orders of harmonic coefficients. |
67 |
|
|
#define _361 (361) |
68 |
|
|
|
69 |
|
|
/* ************************************************************************** */ |
70 |
|
|
|
71 |
|
✗ |
double hundu(double p[_coeffs + 1], // NOLINT |
72 |
|
|
double sinml[_361 + 1], double cosml[_361 + 1], // NOLINT |
73 |
|
|
double gr, double re) |
74 |
|
|
{ |
75 |
|
|
// WGS 84 gravitational constant in m³/s² (mass of Earth’s atmosphere included) |
76 |
|
✗ |
const double GM = 0.3986004418e15; |
77 |
|
|
// WGS 84 datum surface equatorial radius |
78 |
|
✗ |
const double ae = 6378137.0; |
79 |
|
|
|
80 |
|
✗ |
double ar = ae / re; |
81 |
|
✗ |
double arn = ar; |
82 |
|
✗ |
double ac = 0; |
83 |
|
✗ |
double a = 0; |
84 |
|
|
|
85 |
|
✗ |
unsigned k = 3; |
86 |
|
✗ |
for (unsigned n = 2; n <= _nmax; n++) |
87 |
|
|
{ |
88 |
|
✗ |
arn *= ar; |
89 |
|
✗ |
k++; |
90 |
|
✗ |
double sum = p[k] * Geoid::EGM96::internal::egm96_data[k][2]; // NOLINT |
91 |
|
✗ |
double sumc = p[k] * Geoid::EGM96::internal::egm96_data[k][0]; // NOLINT |
92 |
|
|
|
93 |
|
✗ |
for (unsigned m = 1; m <= n; m++) |
94 |
|
|
{ |
95 |
|
✗ |
k++; |
96 |
|
✗ |
double tempc = Geoid::EGM96::internal::egm96_data[k][0] * cosml[m] + Geoid::EGM96::internal::egm96_data[k][1] * sinml[m]; // NOLINT |
97 |
|
✗ |
double temp = Geoid::EGM96::internal::egm96_data[k][2] * cosml[m] + Geoid::EGM96::internal::egm96_data[k][3] * sinml[m]; // NOLINT |
98 |
|
✗ |
sumc += p[k] * tempc; |
99 |
|
✗ |
sum += p[k] * temp; |
100 |
|
|
} |
101 |
|
✗ |
ac += sumc; |
102 |
|
✗ |
a += sum * arn; |
103 |
|
|
} |
104 |
|
✗ |
ac += Geoid::EGM96::internal::egm96_data[1][0] + (p[2] * Geoid::EGM96::internal::egm96_data[2][0]) + (p[3] * (Geoid::EGM96::internal::egm96_data[3][0] * cosml[1] + Geoid::EGM96::internal::egm96_data[3][1] * sinml[1])); |
105 |
|
|
|
106 |
|
|
// Add haco = ac/100 to convert height anomaly on the ellipsoid to the undulation |
107 |
|
|
// Add -0.53m to make undulation refer to the WGS84 ellipsoid |
108 |
|
|
|
109 |
|
✗ |
return ((a * GM) / (gr * re)) + (ac / 100.0) - 0.53; |
110 |
|
|
} |
111 |
|
|
|
112 |
|
✗ |
void dscml(double rlon, double sinml[_361 + 1], double cosml[_361 + 1]) // NOLINT |
113 |
|
|
{ |
114 |
|
✗ |
double a = sin(rlon); |
115 |
|
✗ |
double b = cos(rlon); |
116 |
|
|
|
117 |
|
✗ |
sinml[1] = a; |
118 |
|
✗ |
cosml[1] = b; |
119 |
|
✗ |
sinml[2] = 2 * b * a; |
120 |
|
✗ |
cosml[2] = 2 * b * b - 1; |
121 |
|
|
|
122 |
|
✗ |
for (unsigned m = 3; m <= _nmax; m++) |
123 |
|
|
{ |
124 |
|
✗ |
sinml[m] = 2 * b * sinml[m - 1] - sinml[m - 2]; |
125 |
|
✗ |
cosml[m] = 2 * b * cosml[m - 1] - cosml[m - 2]; |
126 |
|
|
} |
127 |
|
✗ |
} |
128 |
|
|
|
129 |
|
|
/*! |
130 |
|
|
* \param m : order. |
131 |
|
|
* \param theta : Colatitude (radians). |
132 |
|
|
* \param rleg : Normalized legendre function. |
133 |
|
|
* |
134 |
|
|
* This subroutine computes all normalized legendre function in 'rleg'. |
135 |
|
|
* The dimensions of array 'rleg' must be at least equal to nmax+1. |
136 |
|
|
* All calculations are in double precision. |
137 |
|
|
* |
138 |
|
|
* Original programmer: Oscar L. Colombo, Dept. of Geodetic Science the Ohio State University, August 1980. |
139 |
|
|
* ineiev: I removed the derivatives, for they are never computed here. |
140 |
|
|
*/ |
141 |
|
✗ |
void legfdn(unsigned m, double theta, double rleg[_361 + 1]) // NOLINT |
142 |
|
|
{ |
143 |
|
|
static double drts[1301], dirt[1301], cothet, sithet, rlnn[_361 + 1]; // NOLINT |
144 |
|
|
static int ir; // TODO 'ir' must be set to zero before the first call to this sub. |
145 |
|
|
|
146 |
|
✗ |
unsigned nmax1 = _nmax + 1; |
147 |
|
✗ |
unsigned nmax2p = (2 * _nmax) + 1; |
148 |
|
✗ |
unsigned m1 = m + 1; |
149 |
|
✗ |
unsigned m2 = m + 2; |
150 |
|
✗ |
unsigned m3 = m + 3; |
151 |
|
|
unsigned n, n1, n2; // NOLINT |
152 |
|
|
|
153 |
|
✗ |
if (!ir) |
154 |
|
|
{ |
155 |
|
✗ |
ir = 1; |
156 |
|
✗ |
for (n = 1; n <= nmax2p; n++) |
157 |
|
|
{ |
158 |
|
✗ |
drts[n] = sqrt(n); // NOLINT |
159 |
|
✗ |
dirt[n] = 1 / drts[n]; // NOLINT |
160 |
|
|
} |
161 |
|
|
} |
162 |
|
|
|
163 |
|
✗ |
cothet = cos(theta); |
164 |
|
✗ |
sithet = sin(theta); |
165 |
|
|
|
166 |
|
|
// compute the legendre functions |
167 |
|
✗ |
rlnn[1] = 1; |
168 |
|
✗ |
rlnn[2] = sithet * drts[3]; |
169 |
|
✗ |
for (n1 = 3; n1 <= m1; n1++) |
170 |
|
|
{ |
171 |
|
✗ |
n = n1 - 1; |
172 |
|
✗ |
n2 = 2 * n; |
173 |
|
✗ |
rlnn[n1] = drts[n2 + 1] * dirt[n2] * sithet * rlnn[n]; // NOLINT |
174 |
|
|
} |
175 |
|
|
|
176 |
|
✗ |
switch (m) // NOLINT |
177 |
|
|
{ |
178 |
|
✗ |
case 1: |
179 |
|
✗ |
rleg[2] = rlnn[2]; |
180 |
|
✗ |
rleg[3] = drts[5] * cothet * rleg[2]; |
181 |
|
✗ |
break; |
182 |
|
✗ |
case 0: |
183 |
|
✗ |
rleg[1] = 1; |
184 |
|
✗ |
rleg[2] = cothet * drts[3]; |
185 |
|
✗ |
break; |
186 |
|
|
} |
187 |
|
✗ |
rleg[m1] = rlnn[m1]; // NOLINT |
188 |
|
|
|
189 |
|
✗ |
if (m2 <= nmax1) |
190 |
|
|
{ |
191 |
|
✗ |
rleg[m2] = drts[m1 * 2 + 1] * cothet * rleg[m1]; // NOLINT |
192 |
|
✗ |
if (m3 <= nmax1) |
193 |
|
|
{ |
194 |
|
✗ |
for (n1 = m3; n1 <= nmax1; n1++) |
195 |
|
|
{ |
196 |
|
✗ |
n = n1 - 1; |
197 |
|
✗ |
if ((!m && n < 2) || (m == 1 && n < 3)) continue; // NOLINT |
198 |
|
✗ |
n2 = 2 * n; |
199 |
|
✗ |
rleg[n1] = drts[n2 + 1] * dirt[n + m] * dirt[n - m] * (drts[n2 - 1] * cothet * rleg[n1 - 1] - drts[n + m - 1] * drts[n - m - 1] * dirt[n2 - 3] * rleg[n1 - 2]); // NOLINT |
200 |
|
|
} |
201 |
|
|
} |
202 |
|
|
} |
203 |
|
✗ |
} |
204 |
|
|
|
205 |
|
|
namespace |
206 |
|
|
{ |
207 |
|
|
|
208 |
|
|
/*! |
209 |
|
|
* \param lat : Latitude in radians. |
210 |
|
|
* \param lon : Longitude in radians. |
211 |
|
|
* \param re : Geocentric radius. |
212 |
|
|
* \param rlat : Geocentric latitude. |
213 |
|
|
* \param gr : Normal gravity (m/sec²). |
214 |
|
|
* |
215 |
|
|
* This subroutine computes geocentric distance to the point, the geocentric |
216 |
|
|
* latitude, and an approximate value of normal gravity at the point based the |
217 |
|
|
* constants of the WGS84(g873) system are used. |
218 |
|
|
*/ |
219 |
|
✗ |
void radgra(double lat, double lon, double* rlat, double* gr, double* re) |
220 |
|
|
{ |
221 |
|
✗ |
const double a = 6378137.0; |
222 |
|
✗ |
const double e2 = 0.00669437999013; |
223 |
|
✗ |
const double geqt = 9.7803253359; |
224 |
|
✗ |
const double k = 0.00193185265246; |
225 |
|
✗ |
double t1 = sin(lat) * sin(lat); |
226 |
|
✗ |
double n = a / sqrt(1.0 - (e2 * t1)); |
227 |
|
✗ |
double t2 = n * cos(lat); |
228 |
|
✗ |
double x = t2 * cos(lon); |
229 |
|
✗ |
double y = t2 * sin(lon); |
230 |
|
✗ |
double z = (n * (1 - e2)) * sin(lat); |
231 |
|
|
|
232 |
|
✗ |
*re = sqrt((x * x) + (y * y) + (z * z)); // compute the geocentric radius |
233 |
|
✗ |
*rlat = atan(z / sqrt((x * x) + (y * y))); // compute the geocentric latitude |
234 |
|
✗ |
*gr = geqt * (1 + (k * t1)) / sqrt(1 - (e2 * t1)); // compute normal gravity (m/sec²) |
235 |
|
✗ |
} |
236 |
|
|
|
237 |
|
|
/*! |
238 |
|
|
* \brief Compute the geoid undulation from the EGM96 potential coefficient model, for a given latitude and longitude. |
239 |
|
|
* \param lat : Latitude in radians. |
240 |
|
|
* \param lon : Longitude in radians. |
241 |
|
|
* \return The geoid undulation / altitude offset (in meters). |
242 |
|
|
*/ |
243 |
|
✗ |
double undulation(double lat, double lon) |
244 |
|
|
{ |
245 |
|
|
double p[_coeffs + 1], sinml[_361 + 1], cosml[_361 + 1], rleg[_361 + 1]; // NOLINT |
246 |
|
|
|
247 |
|
|
double rlat, gr, re; // NOLINT |
248 |
|
✗ |
unsigned nmax1 = _nmax + 1; |
249 |
|
|
|
250 |
|
|
// compute the geocentric latitude, geocentric radius, normal gravity |
251 |
|
✗ |
radgra(lat, lon, &rlat, &gr, &re); |
252 |
|
✗ |
rlat = (M_PI / 2) - rlat; |
253 |
|
|
|
254 |
|
✗ |
for (unsigned j = 1; j <= nmax1; j++) |
255 |
|
|
{ |
256 |
|
✗ |
unsigned m = j - 1; |
257 |
|
✗ |
legfdn(m, rlat, rleg); |
258 |
|
✗ |
for (unsigned i = j; i <= nmax1; i++) |
259 |
|
|
{ |
260 |
|
✗ |
p[(((i - 1) * i) / 2) + m + 1] = rleg[i]; // NOLINT |
261 |
|
|
} |
262 |
|
|
} |
263 |
|
✗ |
dscml(lon, sinml, cosml); |
264 |
|
|
|
265 |
|
✗ |
return hundu(p, sinml, cosml, gr, re); |
266 |
|
|
} |
267 |
|
|
|
268 |
|
|
} // namespace |
269 |
|
|
|
270 |
|
|
/* ************************************************************************** */ |
271 |
|
|
|
272 |
|
✗ |
double egm96_compute_altitude_offset(double lat, double lon) |
273 |
|
|
{ |
274 |
|
✗ |
return undulation(lat, lon); |
275 |
|
|
} |
276 |
|
|
|
277 |
|
|
/* ************************************************************************** */ |
278 |
|
|
} // namespace NAV |
279 |
|
|
|