]> git.mxchange.org Git - simgear.git/blob - simgear/math/SGGeodesy.cxx
Should be now more easy to make use of SGMath without having osg.
[simgear.git] / simgear / math / SGGeodesy.cxx
1 // Copyright (C) 2006  Mathias Froehlich - Mathias.Froehlich@web.de
2 //
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.
7 //
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.
12 //
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.
16 //
17
18 #ifdef HAVE_CONFIG_H
19 #  include <simgear_config.h>
20 #endif
21
22 #include <cmath>
23
24 #include <simgear/structure/exception.hxx>
25 #include "SGMath.hxx"
26
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
31
32 // These are derived quantities more useful to the code:
33 #if 0
34 #define _SQUASH (1 - 1/_FLATTENING)
35 #define _STRETCH (1/_SQUASH)
36 #define _POLRAD (EQURAD * _SQUASH)
37 #else
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
45 #endif
46
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;
53
54 // additional derived and precomputable ones
55 // for the geodetic conversion algorithm
56
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;
63
64 #undef _EQURAD
65 #undef _FLATTENING
66 #undef _SQUASH
67 #undef _STRETCH
68 #undef _POLRAD
69 #undef E2
70
71 void
72 SGGeodesy::SGCartToGeod(const SGVec3<double>& cart, SGGeod& geod)
73 {
74   // according to
75   // H. Vermeille,
76   // Direct transformation from geocentric to geodetic ccordinates,
77   // Journal of Geodesy (2002) 76:451-454
78   double X = cart(0);
79   double Y = cart(1);
80   double Z = cart(2);
81   double XXpYY = X*X+Y*Y;
82   double sqrtXXpYY = sqrt(XXpYY);
83   double p = XXpYY*ra2;
84   double q = Z*Z*(1-e2)*ra2;
85   double r = 1/6.0*(p+q-e4);
86   double s = e4*p*q/(4*r*r*r);
87 /* 
88   s*(2+s) is negative for s = [-2..0]
89   slightly negative values for s due to floating point rounding errors
90   cause nan for sqrt(s*(2+s))
91   We can probably clamp the resulting parable to positive numbers
92 */
93   if( s >= -2.0 && s <= 0.0 )
94     s = 0.0;
95   double t = pow(1+s+sqrt(s*(2+s)), 1/3.0);
96   double u = r*(1+t+1/t);
97   double v = sqrt(u*u+e4*q);
98   double w = e2*(u+v-q)/(2*v);
99   double k = sqrt(u+v+w*w)-w;
100   double D = k*sqrtXXpYY/(k+e2);
101   geod.setLongitudeRad(2*atan2(Y, X+sqrtXXpYY));
102   double sqrtDDpZZ = sqrt(D*D+Z*Z);
103   geod.setLatitudeRad(2*atan2(Z, D+sqrtDDpZZ));
104   geod.setElevationM((k+e2-1)*sqrtDDpZZ/k);
105 }
106
107 void
108 SGGeodesy::SGGeodToCart(const SGGeod& geod, SGVec3<double>& cart)
109 {
110   // according to
111   // H. Vermeille,
112   // Direct transformation from geocentric to geodetic ccordinates,
113   // Journal of Geodesy (2002) 76:451-454
114   double lambda = geod.getLongitudeRad();
115   double phi = geod.getLatitudeRad();
116   double h = geod.getElevationM();
117   double sphi = sin(phi);
118   double n = a/sqrt(1-e2*sphi*sphi);
119   double cphi = cos(phi);
120   double slambda = sin(lambda);
121   double clambda = cos(lambda);
122   cart(0) = (h+n)*cphi*clambda;
123   cart(1) = (h+n)*cphi*slambda;
124   cart(2) = (h+n-e2*n)*sphi;
125 }
126
127 double
128 SGGeodesy::SGGeodToSeaLevelRadius(const SGGeod& geod)
129 {
130   // this is just a simplified version of the SGGeodToCart function above,
131   // substitute h = 0, take the 2-norm of the cartesian vector and simplify
132   double phi = geod.getLatitudeRad();
133   double sphi = sin(phi);
134   double sphi2 = sphi*sphi;
135   return a*sqrt((1 + (e4 - 2*e2)*sphi2)/(1 - e2*sphi2));
136 }
137
138 void
139 SGGeodesy::SGCartToGeoc(const SGVec3<double>& cart, SGGeoc& geoc)
140 {
141   double minVal = SGLimits<double>::min();
142   if (fabs(cart(0)) < minVal && fabs(cart(1)) < minVal)
143     geoc.setLongitudeRad(0);
144   else
145     geoc.setLongitudeRad(atan2(cart(1), cart(0)));
146
147   double nxy = sqrt(cart(0)*cart(0) + cart(1)*cart(1));
148   if (fabs(nxy) < minVal && fabs(cart(2)) < minVal)
149     geoc.setLatitudeRad(0);
150   else
151     geoc.setLatitudeRad(atan2(cart(2), nxy));
152
153   geoc.setRadiusM(norm(cart));
154 }
155
156 void
157 SGGeodesy::SGGeocToCart(const SGGeoc& geoc, SGVec3<double>& cart)
158 {
159   double lat = geoc.getLatitudeRad();
160   double lon = geoc.getLongitudeRad();
161   double slat = sin(lat);
162   double clat = cos(lat);
163   double slon = sin(lon);
164   double clon = cos(lon);
165   cart = geoc.getRadiusM()*SGVec3<double>(clat*clon, clat*slon, slat);
166 }
167
168 // Notes:
169 //
170 // The XYZ/cartesian coordinate system in use puts the X axis through
171 // zero lat/lon (off west Africa), the Z axis through the north pole,
172 // and the Y axis through 90 degrees longitude (in the Indian Ocean).
173 //
174 // All latitude and longitude values are in radians.  Altitude is in
175 // meters, with zero on the WGS84 ellipsoid.
176 //
177 // The code below makes use of the notion of "squashed" space.  This
178 // is a 2D cylindrical coordinate system where the radius from the Z
179 // axis is multiplied by SQUASH; the earth in this space is a perfect
180 // circle with a radius of POLRAD.
181
182 ////////////////////////////////////////////////////////////////////////
183 //
184 // Direct and inverse distance functions 
185 //
186 // Proceedings of the 7th International Symposium on Geodetic
187 // Computations, 1985
188 //
189 // "The Nested Coefficient Method for Accurate Solutions of Direct and
190 // Inverse Geodetic Problems With Any Length"
191 //
192 // Zhang Xue-Lian
193 // pp 747-763
194 //
195 // modified for FlightGear to use WGS84 only -- Norman Vine
196
197 static inline double M0( double e2 ) {
198     //double e4 = e2*e2;
199   return SGMiscd::pi()*0.5*(1.0 - e2*( 1.0/4.0 + e2*( 3.0/64.0 + 
200                                                   e2*(5.0/256.0) )));
201 }
202
203
204 // given, lat1, lon1, az1 and distance (s), calculate lat2, lon2
205 // and az2.  Lat, lon, and azimuth are in degrees.  distance in meters
206 static int _geo_direct_wgs_84 ( double lat1, double lon1, double az1,
207                         double s, double *lat2, double *lon2,
208                         double *az2 )
209 {
210     double a = SGGeodesy::EQURAD, rf = SGGeodesy::iFLATTENING;
211     double testv = 1.0E-10;
212     double f = ( rf > 0.0 ? 1.0/rf : 0.0 );
213     double b = a*(1.0-f);
214     double e2 = f*(2.0-f);
215     double phi1 = SGMiscd::deg2rad(lat1), lam1 = SGMiscd::deg2rad(lon1);
216     double sinphi1 = sin(phi1), cosphi1 = cos(phi1);
217     double azm1 = SGMiscd::deg2rad(az1);
218     double sinaz1 = sin(azm1), cosaz1 = cos(azm1);
219         
220         
221     if( fabs(s) < 0.01 ) {      // distance < centimeter => congruency
222         *lat2 = lat1;
223         *lon2 = lon1;
224         *az2 = 180.0 + az1;
225         if( *az2 > 360.0 ) *az2 -= 360.0;
226         return 0;
227     } else if( SGLimitsd::min() < fabs(cosphi1) ) {     // non-polar origin
228         // u1 is reduced latitude
229         double tanu1 = sqrt(1.0-e2)*sinphi1/cosphi1;
230         double sig1 = atan2(tanu1,cosaz1);
231         double cosu1 = 1.0/sqrt( 1.0 + tanu1*tanu1 ), sinu1 = tanu1*cosu1;
232         double sinaz =  cosu1*sinaz1, cos2saz = 1.0-sinaz*sinaz;
233         double us = cos2saz*e2/(1.0-e2);
234
235         // Terms
236         double  ta = 1.0+us*(4096.0+us*(-768.0+us*(320.0-175.0*us)))/16384.0,
237             tb = us*(256.0+us*(-128.0+us*(74.0-47.0*us)))/1024.0,
238             tc = 0;
239
240         // FIRST ESTIMATE OF SIGMA (SIG)
241         double first = s/(b*ta);  // !!
242         double sig = first;
243         double c2sigm, sinsig,cossig, temp,denom,rnumer, dlams, dlam;
244         do {
245             c2sigm = cos(2.0*sig1+sig);
246             sinsig = sin(sig); cossig = cos(sig);
247             temp = sig;
248             sig = first + 
249                 tb*sinsig*(c2sigm+tb*(cossig*(-1.0+2.0*c2sigm*c2sigm) - 
250                                       tb*c2sigm*(-3.0+4.0*sinsig*sinsig)
251                                       *(-3.0+4.0*c2sigm*c2sigm)/6.0)
252                            /4.0);
253         } while( fabs(sig-temp) > testv);
254
255         // LATITUDE OF POINT 2
256         // DENOMINATOR IN 2 PARTS (TEMP ALSO USED LATER)
257         temp = sinu1*sinsig-cosu1*cossig*cosaz1;
258         denom = (1.0-f)*sqrt(sinaz*sinaz+temp*temp);
259
260         // NUMERATOR
261         rnumer = sinu1*cossig+cosu1*sinsig*cosaz1;
262         *lat2 = SGMiscd::rad2deg(atan2(rnumer,denom));
263
264         // DIFFERENCE IN LONGITUDE ON AUXILARY SPHERE (DLAMS )
265         rnumer = sinsig*sinaz1;
266         denom = cosu1*cossig-sinu1*sinsig*cosaz1;
267         dlams = atan2(rnumer,denom);
268
269         // TERM C
270         tc = f*cos2saz*(4.0+f*(4.0-3.0*cos2saz))/16.0;
271
272         // DIFFERENCE IN LONGITUDE
273         dlam = dlams-(1.0-tc)*f*sinaz*(sig+tc*sinsig*
274                                        (c2sigm+
275                                         tc*cossig*(-1.0+2.0*
276                                                    c2sigm*c2sigm)));
277         *lon2 = SGMiscd::rad2deg(lam1+dlam);
278         if (*lon2 > 180.0  ) *lon2 -= 360.0;
279         if (*lon2 < -180.0 ) *lon2 += 360.0;
280
281         // AZIMUTH - FROM NORTH
282         *az2 = SGMiscd::rad2deg(atan2(-sinaz,temp));
283         if ( fabs(*az2) < testv ) *az2 = 0.0;
284         if( *az2 < 0.0) *az2 += 360.0;
285         return 0;
286     } else {                    // phi1 == 90 degrees, polar origin
287         double dM = a*M0(e2) - s;
288         double paz = ( phi1 < 0.0 ? 180.0 : 0.0 );
289         double zero = 0.0f;
290         return _geo_direct_wgs_84( zero, lon1, paz, dM, lat2, lon2, az2 );
291     } 
292 }
293
294 bool
295 SGGeodesy::direct(const SGGeod& p1, double course1,
296                   double distance, SGGeod& p2, double& course2)
297 {
298   double lat2, lon2;
299   int ret = _geo_direct_wgs_84(p1.getLatitudeDeg(), p1.getLongitudeDeg(),
300                                course1, distance, &lat2, &lon2, &course2);
301   p2.setLatitudeDeg(lat2);
302   p2.setLongitudeDeg(lon2);
303   p2.setElevationM(0);
304   return ret == 0;
305 }
306
307 // given lat1, lon1, lat2, lon2, calculate starting and ending
308 // az1, az2 and distance (s).  Lat, lon, and azimuth are in degrees.
309 // distance in meters
310 static int _geo_inverse_wgs_84( double lat1, double lon1, double lat2,
311                         double lon2, double *az1, double *az2,
312                         double *s )
313 {
314     double a = SGGeodesy::EQURAD, rf = SGGeodesy::iFLATTENING;
315     int iter=0;
316     double testv = 1.0E-10;
317     double f = ( rf > 0.0 ? 1.0/rf : 0.0 );
318     double b = a*(1.0-f);
319     // double e2 = f*(2.0-f); // unused in this routine
320     double phi1 = SGMiscd::deg2rad(lat1), lam1 = SGMiscd::deg2rad(lon1);
321     double sinphi1 = sin(phi1), cosphi1 = cos(phi1);
322     double phi2 = SGMiscd::deg2rad(lat2), lam2 = SGMiscd::deg2rad(lon2);
323     double sinphi2 = sin(phi2), cosphi2 = cos(phi2);
324         
325     if( (fabs(lat1-lat2) < testv && 
326          ( fabs(lon1-lon2) < testv)) || (fabs(lat1-90.0) < testv ) )
327     {   
328         // TWO STATIONS ARE IDENTICAL : SET DISTANCE & AZIMUTHS TO ZERO */
329         *az1 = 0.0; *az2 = 0.0; *s = 0.0;
330         return 0;
331     } else if(  fabs(cosphi1) < testv ) {
332         // initial point is polar
333         int k = _geo_inverse_wgs_84( lat2,lon2,lat1,lon1, az1,az2,s );
334         k = k; // avoid compiler error since return result is unused
335         b = *az1; *az1 = *az2; *az2 = b;
336         return 0;
337     } else if( fabs(cosphi2) < testv ) {
338         // terminal point is polar
339         double _lon1 = lon1 + 180.0f;
340         int k = _geo_inverse_wgs_84( lat1, lon1, lat1, _lon1, 
341                                     az1, az2, s );
342         k = k; // avoid compiler error since return result is unused
343         *s /= 2.0;
344         *az2 = *az1 + 180.0;
345         if( *az2 > 360.0 ) *az2 -= 360.0; 
346         return 0;
347     } else if( (fabs( fabs(lon1-lon2) - 180 ) < testv) && 
348                (fabs(lat1+lat2) < testv) ) 
349     {
350         // Geodesic passes through the pole (antipodal)
351         double s1,s2;
352         _geo_inverse_wgs_84( lat1,lon1, lat1,lon2, az1,az2, &s1 );
353         _geo_inverse_wgs_84( lat2,lon2, lat1,lon2, az1,az2, &s2 );
354         *az2 = *az1;
355         *s = s1 + s2;
356         return 0;
357     } else {
358         // antipodal and polar points don't get here
359         double dlam = lam2 - lam1, dlams = dlam;
360         double sdlams,cdlams, sig,sinsig,cossig, sinaz,
361             cos2saz, c2sigm;
362         double tc,temp, us,rnumer,denom, ta,tb;
363         double cosu1,sinu1, sinu2,cosu2;
364
365         // Reduced latitudes
366         temp = (1.0-f)*sinphi1/cosphi1;
367         cosu1 = 1.0/sqrt(1.0+temp*temp);
368         sinu1 = temp*cosu1;
369         temp = (1.0-f)*sinphi2/cosphi2;
370         cosu2 = 1.0/sqrt(1.0+temp*temp);
371         sinu2 = temp*cosu2;
372     
373         do {
374             sdlams = sin(dlams), cdlams = cos(dlams);
375             sinsig = sqrt(cosu2*cosu2*sdlams*sdlams+
376                           (cosu1*sinu2-sinu1*cosu2*cdlams)*
377                           (cosu1*sinu2-sinu1*cosu2*cdlams));
378             cossig = sinu1*sinu2+cosu1*cosu2*cdlams;
379             
380             sig = atan2(sinsig,cossig);
381             sinaz = cosu1*cosu2*sdlams/sinsig;
382             cos2saz = 1.0-sinaz*sinaz;
383             c2sigm = (sinu1 == 0.0 || sinu2 == 0.0 ? cossig : 
384                       cossig-2.0*sinu1*sinu2/cos2saz);
385             tc = f*cos2saz*(4.0+f*(4.0-3.0*cos2saz))/16.0;
386             temp = dlams;
387             dlams = dlam+(1.0-tc)*f*sinaz*
388                 (sig+tc*sinsig*
389                  (c2sigm+tc*cossig*(-1.0+2.0*c2sigm*c2sigm)));
390             if (fabs(dlams) > SGMiscd::pi() && iter++ > 50) {
391                 return iter;
392             }
393         } while ( fabs(temp-dlams) > testv);
394
395         us = cos2saz*(a*a-b*b)/(b*b); // !!
396         // BACK AZIMUTH FROM NORTH
397         rnumer = -(cosu1*sdlams);
398         denom = sinu1*cosu2-cosu1*sinu2*cdlams;
399         *az2 = SGMiscd::rad2deg(atan2(rnumer,denom));
400         if( fabs(*az2) < testv ) *az2 = 0.0;
401         if(*az2 < 0.0) *az2 += 360.0;
402
403         // FORWARD AZIMUTH FROM NORTH
404         rnumer = cosu2*sdlams;
405         denom = cosu1*sinu2-sinu1*cosu2*cdlams;
406         *az1 = SGMiscd::rad2deg(atan2(rnumer,denom));
407         if( fabs(*az1) < testv ) *az1 = 0.0;
408         if(*az1 < 0.0) *az1 += 360.0;
409
410         // Terms a & b
411         ta = 1.0+us*(4096.0+us*(-768.0+us*(320.0-175.0*us)))/
412             16384.0;
413         tb = us*(256.0+us*(-128.0+us*(74.0-47.0*us)))/1024.0;
414
415         // GEODETIC DISTANCE
416         *s = b*ta*(sig-tb*sinsig*
417                    (c2sigm+tb*(cossig*(-1.0+2.0*c2sigm*c2sigm)-tb*
418                                c2sigm*(-3.0+4.0*sinsig*sinsig)*
419                                (-3.0+4.0*c2sigm*c2sigm)/6.0)/
420                     4.0));
421         return 0;
422     }
423 }
424
425 bool
426 SGGeodesy::inverse(const SGGeod& p1, const SGGeod& p2, double& course1,
427                    double& course2, double& distance)
428 {
429   int ret = _geo_inverse_wgs_84(p1.getLatitudeDeg(), p1.getLongitudeDeg(),
430                                 p2.getLatitudeDeg(), p2.getLongitudeDeg(),
431                                 &course1, &course2, &distance);
432   return ret == 0;
433 }
434
435 double
436 SGGeodesy::courseDeg(const SGGeod& p1, const SGGeod& p2)
437 {
438   double course1, course2, distance;
439   int r = _geo_inverse_wgs_84(p1.getLatitudeDeg(), p1.getLongitudeDeg(),
440                                 p2.getLatitudeDeg(), p2.getLongitudeDeg(),
441                                 &course1, &course2, &distance);
442   if (r != 0) {
443     throw sg_exception("SGGeodesy::courseDeg, unable to compute course");
444   }
445   
446   return course1;
447 }
448
449 double
450 SGGeodesy::distanceM(const SGGeod& p1, const SGGeod& p2)
451 {
452   double course1, course2, distance;
453   int r = _geo_inverse_wgs_84(p1.getLatitudeDeg(), p1.getLongitudeDeg(),
454                                 p2.getLatitudeDeg(), p2.getLongitudeDeg(),
455                                 &course1, &course2, &distance);
456   if (r != 0) {
457     throw sg_exception("SGGeodesy::distanceM, unable to compute distance");
458   }
459   
460   return distance;
461 }
462
463 double
464 SGGeodesy::distanceNm(const SGGeod& from, const SGGeod& to)
465 {
466   return distanceM(from, to) * SG_METER_TO_NM;
467 }
468
469 /// Geocentric routines
470
471 void
472 SGGeodesy::advanceRadM(const SGGeoc& geoc, double course, double distance,
473                        SGGeoc& result)
474 {
475   result.setRadiusM(geoc.getRadiusM());
476
477   // lat=asin(sin(lat1)*cos(d)+cos(lat1)*sin(d)*cos(tc))
478   // IF (cos(lat)=0)
479   //   lon=lon1      // endpoint a pole
480   // ELSE
481   //   lon=mod(lon1-asin(sin(tc)*sin(d)/cos(lat))+pi,2*pi)-pi
482   // ENDIF
483   
484   distance *= SG_METER_TO_NM * SG_NM_TO_RAD;
485   
486   double sinDistance = sin(distance);
487   double cosDistance = cos(distance);
488
489   double sinLat = sin(geoc.getLatitudeRad()) * cosDistance +
490     cos(geoc.getLatitudeRad()) * sinDistance * cos(course);
491   sinLat = SGMiscd::clip(sinLat, -1, 1);
492   result.setLatitudeRad(asin(sinLat));
493   double cosLat = cos(result.getLatitudeRad());
494   
495   
496   if (cosLat <= SGLimitsd::min()) {
497     // endpoint a pole
498     result.setLongitudeRad(geoc.getLongitudeRad());
499   } else {
500     double tmp = SGMiscd::clip(sin(course) * sinDistance / cosLat, -1, 1);
501     double lon = SGMiscd::normalizeAngle(geoc.getLongitudeRad() - asin( tmp ));
502     result.setLongitudeRad(lon);
503   }
504 }
505
506 double
507 SGGeodesy::courseRad(const SGGeoc& from, const SGGeoc& to)
508 {
509   double diffLon = to.getLongitudeRad() - from.getLongitudeRad();
510
511   double sinLatFrom = sin(from.getLatitudeRad());
512   double cosLatFrom = cos(from.getLatitudeRad());
513
514   double sinLatTo = sin(to.getLatitudeRad());
515   double cosLatTo = cos(to.getLatitudeRad());
516
517   double x = cosLatTo*sin(diffLon);
518   double y = cosLatFrom*sinLatTo - sinLatFrom*cosLatTo*cos(diffLon);
519
520   // guard atan2 returning NaN's
521   if (fabs(x) <= SGLimitsd::min() && fabs(y) <= SGLimitsd::min())
522     return 0;
523
524   double c = atan2(x, y);
525   if (c >= 0)
526     return SGMiscd::twopi() - c;
527   else
528     return -c;
529 }
530
531 double
532 SGGeodesy::distanceM(const SGGeoc& from, const SGGeoc& to)
533 {
534   // d = 2*asin(sqrt((sin((lat1-lat2)/2))^2 +
535   //            cos(lat1)*cos(lat2)*(sin((lon1-lon2)/2))^2))
536   double cosLatFrom = cos(from.getLatitudeRad());
537   double cosLatTo = cos(to.getLatitudeRad());
538   double tmp1 = sin(0.5*(from.getLatitudeRad() - to.getLatitudeRad()));
539   double tmp2 = sin(0.5*(from.getLongitudeRad() - to.getLongitudeRad()));
540   double square = tmp1*tmp1 + cosLatFrom*cosLatTo*tmp2*tmp2;
541   double s = SGMiscd::min(sqrt(SGMiscd::max(square, 0)), 1);
542   return 2 * asin(s) * SG_RAD_TO_NM * SG_NM_TO_METER;
543 }