1 // Copyright (C) 2006 Mathias Froehlich - Mathias.Froehlich@web.de
3 // This library is free software; you can redistribute it and/or
4 // modify it under the terms of the GNU Library General Public
5 // License as published by the Free Software Foundation; either
6 // version 2 of the License, or (at your option) any later version.
8 // This library is distributed in the hope that it will be useful,
9 // but WITHOUT ANY WARRANTY; without even the implied warranty of
10 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
11 // Library General Public License for more details.
13 // You should have received a copy of the GNU General Public License
14 // along with this program; if not, write to the Free Software
15 // Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 # include <simgear_config.h>
24 #include <simgear/structure/exception.hxx>
27 // These are hard numbers from the WGS84 standard. DON'T MODIFY
28 // unless you want to change the datum.
29 #define _EQURAD 6378137.0
30 #define _FLATTENING 298.257223563
32 // These are derived quantities more useful to the code:
34 #define _SQUASH (1 - 1/_FLATTENING)
35 #define _STRETCH (1/_SQUASH)
36 #define _POLRAD (EQURAD * _SQUASH)
38 // High-precision versions of the above produced with an arbitrary
39 // precision calculator (the compiler might lose a few bits in the FPU
40 // operations). These are specified to 81 bits of mantissa, which is
41 // higher than any FPU known to me:
42 #define _SQUASH 0.9966471893352525192801545
43 #define _STRETCH 1.0033640898209764189003079
44 #define _POLRAD 6356752.3142451794975639668
47 // The constants from the WGS84 standard
48 const double SGGeodesy::EQURAD = _EQURAD;
49 const double SGGeodesy::iFLATTENING = _FLATTENING;
50 const double SGGeodesy::SQUASH = _SQUASH;
51 const double SGGeodesy::STRETCH = _STRETCH;
52 const double SGGeodesy::POLRAD = _POLRAD;
54 // additional derived and precomputable ones
55 // for the geodetic conversion algorithm
57 #define E2 fabs(1 - _SQUASH*_SQUASH)
58 static double a = _EQURAD;
59 static double ra2 = 1/(_EQURAD*_EQURAD);
60 //static double e = sqrt(E2);
61 static double e2 = E2;
62 static double e4 = E2*E2;
72 SGGeodesy::SGCartToGeod(const SGVec3<double>& cart, SGGeod& geod)
76 // Direct transformation from geocentric to geodetic ccordinates,
77 // Journal of Geodesy (2002) 76:451-454
81 double XXpYY = X*X+Y*Y;
82 if( XXpYY + Z*Z < 25 ) {
83 // This function fails near the geocenter region, so catch that special case here.
84 // Define the innermost sphere of small radius as earth center and return the
85 // coordinates 0/0/-EQURAD. It may be any other place on geoide's surface,
86 // the Northpole, Hawaii or Wentorf. This one was easy to code ;-)
87 geod.setLongitudeRad( 0.0 );
88 geod.setLongitudeRad( 0.0 );
89 geod.setElevationM( -EQURAD );
93 double sqrtXXpYY = sqrt(XXpYY);
95 double q = Z*Z*(1-e2)*ra2;
96 double r = 1/6.0*(p+q-e4);
97 double s = e4*p*q/(4*r*r*r);
99 s*(2+s) is negative for s = [-2..0]
100 slightly negative values for s due to floating point rounding errors
101 cause nan for sqrt(s*(2+s))
102 We can probably clamp the resulting parable to positive numbers
104 if( s >= -2.0 && s <= 0.0 )
106 double t = pow(1+s+sqrt(s*(2+s)), 1/3.0);
107 double u = r*(1+t+1/t);
108 double v = sqrt(u*u+e4*q);
109 double w = e2*(u+v-q)/(2*v);
110 double k = sqrt(u+v+w*w)-w;
111 double D = k*sqrtXXpYY/(k+e2);
112 geod.setLongitudeRad(2*atan2(Y, X+sqrtXXpYY));
113 double sqrtDDpZZ = sqrt(D*D+Z*Z);
114 geod.setLatitudeRad(2*atan2(Z, D+sqrtDDpZZ));
115 geod.setElevationM((k+e2-1)*sqrtDDpZZ/k);
119 SGGeodesy::SGGeodToCart(const SGGeod& geod, SGVec3<double>& cart)
123 // Direct transformation from geocentric to geodetic ccordinates,
124 // Journal of Geodesy (2002) 76:451-454
125 double lambda = geod.getLongitudeRad();
126 double phi = geod.getLatitudeRad();
127 double h = geod.getElevationM();
128 double sphi = sin(phi);
129 double n = a/sqrt(1-e2*sphi*sphi);
130 double cphi = cos(phi);
131 double slambda = sin(lambda);
132 double clambda = cos(lambda);
133 cart(0) = (h+n)*cphi*clambda;
134 cart(1) = (h+n)*cphi*slambda;
135 cart(2) = (h+n-e2*n)*sphi;
139 SGGeodesy::SGGeodToSeaLevelRadius(const SGGeod& geod)
141 // this is just a simplified version of the SGGeodToCart function above,
142 // substitute h = 0, take the 2-norm of the cartesian vector and simplify
143 double phi = geod.getLatitudeRad();
144 double sphi = sin(phi);
145 double sphi2 = sphi*sphi;
146 return a*sqrt((1 + (e4 - 2*e2)*sphi2)/(1 - e2*sphi2));
150 SGGeodesy::SGCartToGeoc(const SGVec3<double>& cart, SGGeoc& geoc)
152 double minVal = SGLimits<double>::min();
153 if (fabs(cart(0)) < minVal && fabs(cart(1)) < minVal)
154 geoc.setLongitudeRad(0);
156 geoc.setLongitudeRad(atan2(cart(1), cart(0)));
158 double nxy = sqrt(cart(0)*cart(0) + cart(1)*cart(1));
159 if (fabs(nxy) < minVal && fabs(cart(2)) < minVal)
160 geoc.setLatitudeRad(0);
162 geoc.setLatitudeRad(atan2(cart(2), nxy));
164 geoc.setRadiusM(norm(cart));
168 SGGeodesy::SGGeocToCart(const SGGeoc& geoc, SGVec3<double>& cart)
170 double lat = geoc.getLatitudeRad();
171 double lon = geoc.getLongitudeRad();
172 double slat = sin(lat);
173 double clat = cos(lat);
174 double slon = sin(lon);
175 double clon = cos(lon);
176 cart = geoc.getRadiusM()*SGVec3<double>(clat*clon, clat*slon, slat);
181 // The XYZ/cartesian coordinate system in use puts the X axis through
182 // zero lat/lon (off west Africa), the Z axis through the north pole,
183 // and the Y axis through 90 degrees longitude (in the Indian Ocean).
185 // All latitude and longitude values are in radians. Altitude is in
186 // meters, with zero on the WGS84 ellipsoid.
188 // The code below makes use of the notion of "squashed" space. This
189 // is a 2D cylindrical coordinate system where the radius from the Z
190 // axis is multiplied by SQUASH; the earth in this space is a perfect
191 // circle with a radius of POLRAD.
193 ////////////////////////////////////////////////////////////////////////
195 // Direct and inverse distance functions
197 // Proceedings of the 7th International Symposium on Geodetic
198 // Computations, 1985
200 // "The Nested Coefficient Method for Accurate Solutions of Direct and
201 // Inverse Geodetic Problems With Any Length"
206 // modified for FlightGear to use WGS84 only -- Norman Vine
208 static inline double M0( double e2 ) {
210 return SGMiscd::pi()*0.5*(1.0 - e2*( 1.0/4.0 + e2*( 3.0/64.0 +
215 // given, lat1, lon1, az1 and distance (s), calculate lat2, lon2
216 // and az2. Lat, lon, and azimuth are in degrees. distance in meters
217 static int _geo_direct_wgs_84 ( double lat1, double lon1, double az1,
218 double s, double *lat2, double *lon2,
221 double a = SGGeodesy::EQURAD, rf = SGGeodesy::iFLATTENING;
222 double testv = 1.0E-10;
223 double f = ( rf > 0.0 ? 1.0/rf : 0.0 );
224 double b = a*(1.0-f);
225 double e2 = f*(2.0-f);
226 double phi1 = SGMiscd::deg2rad(lat1), lam1 = SGMiscd::deg2rad(lon1);
227 double sinphi1 = sin(phi1), cosphi1 = cos(phi1);
228 double azm1 = SGMiscd::deg2rad(az1);
229 double sinaz1 = sin(azm1), cosaz1 = cos(azm1);
232 if( fabs(s) < 0.01 ) { // distance < centimeter => congruency
236 if( *az2 > 360.0 ) *az2 -= 360.0;
238 } else if( SGLimitsd::min() < fabs(cosphi1) ) { // non-polar origin
239 // u1 is reduced latitude
240 double tanu1 = sqrt(1.0-e2)*sinphi1/cosphi1;
241 double sig1 = atan2(tanu1,cosaz1);
242 double cosu1 = 1.0/sqrt( 1.0 + tanu1*tanu1 ), sinu1 = tanu1*cosu1;
243 double sinaz = cosu1*sinaz1, cos2saz = 1.0-sinaz*sinaz;
244 double us = cos2saz*e2/(1.0-e2);
247 double ta = 1.0+us*(4096.0+us*(-768.0+us*(320.0-175.0*us)))/16384.0,
248 tb = us*(256.0+us*(-128.0+us*(74.0-47.0*us)))/1024.0,
251 // FIRST ESTIMATE OF SIGMA (SIG)
252 double first = s/(b*ta); // !!
254 double c2sigm, sinsig,cossig, temp,denom,rnumer, dlams, dlam;
256 c2sigm = cos(2.0*sig1+sig);
257 sinsig = sin(sig); cossig = cos(sig);
260 tb*sinsig*(c2sigm+tb*(cossig*(-1.0+2.0*c2sigm*c2sigm) -
261 tb*c2sigm*(-3.0+4.0*sinsig*sinsig)
262 *(-3.0+4.0*c2sigm*c2sigm)/6.0)
264 } while( fabs(sig-temp) > testv);
266 // LATITUDE OF POINT 2
267 // DENOMINATOR IN 2 PARTS (TEMP ALSO USED LATER)
268 temp = sinu1*sinsig-cosu1*cossig*cosaz1;
269 denom = (1.0-f)*sqrt(sinaz*sinaz+temp*temp);
272 rnumer = sinu1*cossig+cosu1*sinsig*cosaz1;
273 *lat2 = SGMiscd::rad2deg(atan2(rnumer,denom));
275 // DIFFERENCE IN LONGITUDE ON AUXILARY SPHERE (DLAMS )
276 rnumer = sinsig*sinaz1;
277 denom = cosu1*cossig-sinu1*sinsig*cosaz1;
278 dlams = atan2(rnumer,denom);
281 tc = f*cos2saz*(4.0+f*(4.0-3.0*cos2saz))/16.0;
283 // DIFFERENCE IN LONGITUDE
284 dlam = dlams-(1.0-tc)*f*sinaz*(sig+tc*sinsig*
288 *lon2 = SGMiscd::rad2deg(lam1+dlam);
289 if (*lon2 > 180.0 ) *lon2 -= 360.0;
290 if (*lon2 < -180.0 ) *lon2 += 360.0;
292 // AZIMUTH - FROM NORTH
293 *az2 = SGMiscd::rad2deg(atan2(-sinaz,temp));
294 if ( fabs(*az2) < testv ) *az2 = 0.0;
295 if( *az2 < 0.0) *az2 += 360.0;
297 } else { // phi1 == 90 degrees, polar origin
298 double dM = a*M0(e2) - s;
299 double paz = ( phi1 < 0.0 ? 180.0 : 0.0 );
301 return _geo_direct_wgs_84( zero, lon1, paz, dM, lat2, lon2, az2 );
306 SGGeodesy::direct(const SGGeod& p1, double course1,
307 double distance, SGGeod& p2, double& course2)
310 int ret = _geo_direct_wgs_84(p1.getLatitudeDeg(), p1.getLongitudeDeg(),
311 course1, distance, &lat2, &lon2, &course2);
312 p2.setLatitudeDeg(lat2);
313 p2.setLongitudeDeg(lon2);
318 // given lat1, lon1, lat2, lon2, calculate starting and ending
319 // az1, az2 and distance (s). Lat, lon, and azimuth are in degrees.
320 // distance in meters
321 static int _geo_inverse_wgs_84( double lat1, double lon1, double lat2,
322 double lon2, double *az1, double *az2,
325 double a = SGGeodesy::EQURAD, rf = SGGeodesy::iFLATTENING;
327 double testv = 1.0E-10;
328 double f = ( rf > 0.0 ? 1.0/rf : 0.0 );
329 double b = a*(1.0-f);
330 // double e2 = f*(2.0-f); // unused in this routine
331 double phi1 = SGMiscd::deg2rad(lat1), lam1 = SGMiscd::deg2rad(lon1);
332 double sinphi1 = sin(phi1), cosphi1 = cos(phi1);
333 double phi2 = SGMiscd::deg2rad(lat2), lam2 = SGMiscd::deg2rad(lon2);
334 double sinphi2 = sin(phi2), cosphi2 = cos(phi2);
336 if( (fabs(lat1-lat2) < testv &&
337 ( fabs(lon1-lon2) < testv)) || (fabs(lat1-90.0) < testv ) )
339 // TWO STATIONS ARE IDENTICAL : SET DISTANCE & AZIMUTHS TO ZERO */
340 *az1 = 0.0; *az2 = 0.0; *s = 0.0;
342 } else if( fabs(cosphi1) < testv ) {
343 // initial point is polar
344 int k = _geo_inverse_wgs_84( lat2,lon2,lat1,lon1, az1,az2,s );
345 k = k; // avoid compiler error since return result is unused
346 b = *az1; *az1 = *az2; *az2 = b;
348 } else if( fabs(cosphi2) < testv ) {
349 // terminal point is polar
350 double _lon1 = lon1 + 180.0f;
351 int k = _geo_inverse_wgs_84( lat1, lon1, lat1, _lon1,
353 k = k; // avoid compiler error since return result is unused
356 if( *az2 > 360.0 ) *az2 -= 360.0;
358 } else if( (fabs( fabs(lon1-lon2) - 180 ) < testv) &&
359 (fabs(lat1+lat2) < testv) )
361 // Geodesic passes through the pole (antipodal)
363 _geo_inverse_wgs_84( lat1,lon1, lat1,lon2, az1,az2, &s1 );
364 _geo_inverse_wgs_84( lat2,lon2, lat1,lon2, az1,az2, &s2 );
369 // antipodal and polar points don't get here
370 double dlam = lam2 - lam1, dlams = dlam;
371 double sdlams,cdlams, sig,sinsig,cossig, sinaz,
373 double tc,temp, us,rnumer,denom, ta,tb;
374 double cosu1,sinu1, sinu2,cosu2;
377 temp = (1.0-f)*sinphi1/cosphi1;
378 cosu1 = 1.0/sqrt(1.0+temp*temp);
380 temp = (1.0-f)*sinphi2/cosphi2;
381 cosu2 = 1.0/sqrt(1.0+temp*temp);
385 sdlams = sin(dlams), cdlams = cos(dlams);
386 sinsig = sqrt(cosu2*cosu2*sdlams*sdlams+
387 (cosu1*sinu2-sinu1*cosu2*cdlams)*
388 (cosu1*sinu2-sinu1*cosu2*cdlams));
389 cossig = sinu1*sinu2+cosu1*cosu2*cdlams;
391 sig = atan2(sinsig,cossig);
392 sinaz = cosu1*cosu2*sdlams/sinsig;
393 cos2saz = 1.0-sinaz*sinaz;
394 c2sigm = (sinu1 == 0.0 || sinu2 == 0.0 ? cossig :
395 cossig-2.0*sinu1*sinu2/cos2saz);
396 tc = f*cos2saz*(4.0+f*(4.0-3.0*cos2saz))/16.0;
398 dlams = dlam+(1.0-tc)*f*sinaz*
400 (c2sigm+tc*cossig*(-1.0+2.0*c2sigm*c2sigm)));
401 if (fabs(dlams) > SGMiscd::pi() && iter++ > 50) {
404 } while ( fabs(temp-dlams) > testv);
406 us = cos2saz*(a*a-b*b)/(b*b); // !!
407 // BACK AZIMUTH FROM NORTH
408 rnumer = -(cosu1*sdlams);
409 denom = sinu1*cosu2-cosu1*sinu2*cdlams;
410 *az2 = SGMiscd::rad2deg(atan2(rnumer,denom));
411 if( fabs(*az2) < testv ) *az2 = 0.0;
412 if(*az2 < 0.0) *az2 += 360.0;
414 // FORWARD AZIMUTH FROM NORTH
415 rnumer = cosu2*sdlams;
416 denom = cosu1*sinu2-sinu1*cosu2*cdlams;
417 *az1 = SGMiscd::rad2deg(atan2(rnumer,denom));
418 if( fabs(*az1) < testv ) *az1 = 0.0;
419 if(*az1 < 0.0) *az1 += 360.0;
422 ta = 1.0+us*(4096.0+us*(-768.0+us*(320.0-175.0*us)))/
424 tb = us*(256.0+us*(-128.0+us*(74.0-47.0*us)))/1024.0;
427 *s = b*ta*(sig-tb*sinsig*
428 (c2sigm+tb*(cossig*(-1.0+2.0*c2sigm*c2sigm)-tb*
429 c2sigm*(-3.0+4.0*sinsig*sinsig)*
430 (-3.0+4.0*c2sigm*c2sigm)/6.0)/
437 SGGeodesy::inverse(const SGGeod& p1, const SGGeod& p2, double& course1,
438 double& course2, double& distance)
440 int ret = _geo_inverse_wgs_84(p1.getLatitudeDeg(), p1.getLongitudeDeg(),
441 p2.getLatitudeDeg(), p2.getLongitudeDeg(),
442 &course1, &course2, &distance);
447 SGGeodesy::courseDeg(const SGGeod& p1, const SGGeod& p2)
449 double course1, course2, distance;
450 int r = _geo_inverse_wgs_84(p1.getLatitudeDeg(), p1.getLongitudeDeg(),
451 p2.getLatitudeDeg(), p2.getLongitudeDeg(),
452 &course1, &course2, &distance);
454 throw sg_exception("SGGeodesy::courseDeg, unable to compute course");
461 SGGeodesy::distanceM(const SGGeod& p1, const SGGeod& p2)
463 double course1, course2, distance;
464 int r = _geo_inverse_wgs_84(p1.getLatitudeDeg(), p1.getLongitudeDeg(),
465 p2.getLatitudeDeg(), p2.getLongitudeDeg(),
466 &course1, &course2, &distance);
468 throw sg_exception("SGGeodesy::distanceM, unable to compute distance");
475 SGGeodesy::distanceNm(const SGGeod& from, const SGGeod& to)
477 return distanceM(from, to) * SG_METER_TO_NM;
480 /// Geocentric routines
483 SGGeodesy::advanceRadM(const SGGeoc& geoc, double course, double distance,
486 result.setRadiusM(geoc.getRadiusM());
488 // lat=asin(sin(lat1)*cos(d)+cos(lat1)*sin(d)*cos(tc))
490 // lon=lon1 // endpoint a pole
492 // lon=mod(lon1-asin(sin(tc)*sin(d)/cos(lat))+pi,2*pi)-pi
495 distance *= SG_METER_TO_NM * SG_NM_TO_RAD;
497 double sinDistance = sin(distance);
498 double cosDistance = cos(distance);
500 double sinLat = sin(geoc.getLatitudeRad()) * cosDistance +
501 cos(geoc.getLatitudeRad()) * sinDistance * cos(course);
502 sinLat = SGMiscd::clip(sinLat, -1, 1);
503 result.setLatitudeRad(asin(sinLat));
504 double cosLat = cos(result.getLatitudeRad());
507 if (cosLat <= SGLimitsd::min()) {
509 result.setLongitudeRad(geoc.getLongitudeRad());
511 double tmp = SGMiscd::clip(sin(course) * sinDistance / cosLat, -1, 1);
512 double lon = SGMiscd::normalizeAngle(geoc.getLongitudeRad() - asin( tmp ));
513 result.setLongitudeRad(lon);
518 SGGeodesy::courseRad(const SGGeoc& from, const SGGeoc& to)
520 double diffLon = to.getLongitudeRad() - from.getLongitudeRad();
522 double sinLatFrom = sin(from.getLatitudeRad());
523 double cosLatFrom = cos(from.getLatitudeRad());
525 double sinLatTo = sin(to.getLatitudeRad());
526 double cosLatTo = cos(to.getLatitudeRad());
528 double x = cosLatTo*sin(diffLon);
529 double y = cosLatFrom*sinLatTo - sinLatFrom*cosLatTo*cos(diffLon);
531 // guard atan2 returning NaN's
532 if (fabs(x) <= SGLimitsd::min() && fabs(y) <= SGLimitsd::min())
535 double c = atan2(x, y);
537 return SGMiscd::twopi() - c;
543 SGGeodesy::distanceRad(const SGGeoc& from, const SGGeoc& to)
545 // d = 2*asin(sqrt((sin((lat1-lat2)/2))^2 +
546 // cos(lat1)*cos(lat2)*(sin((lon1-lon2)/2))^2))
547 double cosLatFrom = cos(from.getLatitudeRad());
548 double cosLatTo = cos(to.getLatitudeRad());
549 double tmp1 = sin(0.5*(from.getLatitudeRad() - to.getLatitudeRad()));
550 double tmp2 = sin(0.5*(from.getLongitudeRad() - to.getLongitudeRad()));
551 double square = tmp1*tmp1 + cosLatFrom*cosLatTo*tmp2*tmp2;
552 double s = SGMiscd::min(sqrt(SGMiscd::max(square, 0)), 1);
558 SGGeodesy::distanceM(const SGGeoc& from, const SGGeoc& to)
560 return distanceRad(from, to) * SG_RAD_TO_NM * SG_NM_TO_METER;