Coverage for pygeodesy/geodsolve.py: 94%

80 statements  

« prev     ^ index     » next       coverage.py v7.2.2, created at 2024-04-04 14:33 -0400

1 

2# -*- coding: utf-8 -*- 

3 

4u'''Wrapper to invoke I{Karney}'s U{GeodSolve 

5<https://GeographicLib.SourceForge.io/C++/doc/GeodSolve.1.html>} utility 

6as an (exact) geodesic, but intended I{for testing purposes only}. 

7 

8Set env variable C{PYGEODESY_GEODSOLVE} to the (fully qualified) path 

9of the C{GeodSolve} executable. 

10''' 

11 

12from pygeodesy.basics import _xinstanceof 

13# from pygeodesy.errors import _xkwds # from .karney 

14from pygeodesy.interns import NN, _a12_, _azi1_, _azi2_, \ 

15 _lat1_, _lat2_, _lon1_, _lon2_, _m12_, \ 

16 _M12_, _M21_, _s12_, _S12_, _UNDER_ 

17from pygeodesy.interns import _UNUSED_, _not_ # PYCHOK used! 

18from pygeodesy.karney import _Azi, Caps, _Deg, GeodesicError, _GTuple, \ 

19 _Pass, _Lat, _Lon, _M, _M2, _sincos2d, _xkwds 

20from pygeodesy.lazily import _ALL_DOCS, _ALL_LAZY, _ALL_MODS as _MODS, \ 

21 _PYGEODESY_GEODSOLVE_, _getenv, printf 

22from pygeodesy.namedTuples import Destination3Tuple, Distance3Tuple 

23from pygeodesy.props import Property, Property_RO 

24from pygeodesy.solveBase import _SolveBase, _SolveLineBase 

25from pygeodesy.utily import _unrollon, _Wrap, wrap360 

26 

27__all__ = _ALL_LAZY.geodsolve 

28__version__ = '24.02.21' 

29 

30 

31class GeodSolve12Tuple(_GTuple): 

32 '''12-Tuple C{(lat1, lon1, azi1, lat2, lon2, azi2, s12, a12, m12, M12, M21, S12)} with 

33 angles C{lat1}, C{lon1}, C{azi1}, C{lat2}, C{lon2} and C{azi2} and arc C{a12} all in 

34 C{degrees}, initial C{azi1} and final C{azi2} forward azimuths, distance C{s12} and 

35 reduced length C{m12} in C{meter}, area C{S12} in C{meter} I{squared} and geodesic 

36 scale factors C{M12} and C{M21}, both C{scalar}, see U{GeodSolve 

37 <https://GeographicLib.SourceForge.io/C++/doc/GeodSolve.1.html>}. 

38 ''' 

39 # from GeodSolve --help option -f ... lat1 lon1 azi1 lat2 lon2 azi2 s12 a12 m12 M12 M21 S12 

40 _Names_ = (_lat1_, _lon1_, _azi1_, _lat2_, _lon2_, _azi2_, _s12_, _a12_, _m12_, _M12_, _M21_, _S12_) 

41 _Units_ = (_Lat, _Lon, _Azi, _Lat, _Lon, _Azi, _M, _Deg, _Pass, _Pass, _Pass, _M2) 

42 

43 

44class _GeodesicSolveBase(_SolveBase): 

45 '''(INTERNAL) Base class for L{GeodesicSolve} and L{GeodesicLineSolve}. 

46 ''' 

47 _Error = GeodesicError 

48 _Names_Direct = \ 

49 _Names_Inverse = GeodSolve12Tuple._Names_ 

50 _Solve_name = 'GeodSolve' 

51 _Solve_path = _getenv(_PYGEODESY_GEODSOLVE_, _PYGEODESY_GEODSOLVE_) 

52 

53 @Property_RO 

54 def _b_option(self): 

55 return ('-b',) if self.reverse2 else () 

56 

57 @Property_RO 

58 def _cmdBasic(self): 

59 '''(INTERNAL) Get the basic C{GeodSolve} cmd (C{tuple}). 

60 ''' 

61 return (self.GeodSolve,) + self._b_option \ 

62 + self._e_option \ 

63 + self._E_option \ 

64 + self._p_option \ 

65 + self._u_option + ('-f',) 

66 

67 @Property_RO 

68 def _E_option(self): 

69 return ('-E',) if self.Exact else () 

70 

71 @Property 

72 def GeodSolve(self): 

73 '''Get the U{GeodSolve<https://GeographicLib.SourceForge.io/C++/doc/GeodSolve.1.html>} 

74 executable (C{filename}). 

75 ''' 

76 return self._Solve_path 

77 

78 @GeodSolve.setter # PYCHOK setter! 

79 def GeodSolve(self, path): 

80 '''Set the U{GeodSolve<https://GeographicLib.SourceForge.io/C++/doc/GeodSolve.1.html>} 

81 executable (C{filename}), the (fully qualified) path to the C{GeodSolve} executable. 

82 

83 @raise GeodesicError: Invalid B{C{path}}, B{C{path}} doesn't exist or 

84 isn't the C{GeodSolve} executable. 

85 ''' 

86 self._setSolve(path) 

87 

88 def toStr(self, **prec_sep): # PYCHOK signature 

89 '''Return this C{GeodesicSolve} as string. 

90 

91 @kwarg prec_sep: Keyword argumens C{B{prec}=6} and C{B{sep}=', '} 

92 for the C{float} C{prec}ision, number of decimal digits 

93 (0..9) and the C{sep}arator string to join. Trailing 

94 zero decimals are stripped for B{C{prec}} values of 

95 1 and above, but kept for negative B{C{prec}} values. 

96 

97 @return: GeodesicSolve items (C{str}). 

98 ''' 

99 return _SolveBase._toStr(self, GeodSolve=self.GeodSolve, **prec_sep) 

100 

101 @Property_RO 

102 def _u_option(self): 

103 return ('-u',) if self.unroll else () 

104 

105 

106class GeodesicSolve(_GeodesicSolveBase): 

107 '''Wrapper to invoke I{Karney}'s U{GeodSolve<https://GeographicLib.SourceForge.io/C++/doc/GeodSolve.1.html>} 

108 as an C{Exact} version of I{Karney}'s Python class U{Geodesic<https://GeographicLib.SourceForge.io/C++/doc/ 

109 python/code.html#geographiclib.geodesic.Geodesic>}. 

110 

111 @note: Use property C{GeodSolve} or env variable C{PYGEODESY_GEODSOLVE} to specify the (fully 

112 qualified) path to the C{GeodSolve} executable. 

113 

114 @note: This C{geodesic} is intended I{for testing purposes only}, it invokes the C{GeodSolve} 

115 executable for I{every} method call. 

116 ''' 

117 

118 def Area(self, polyline=False, name=NN): 

119 '''Set up a L{GeodesicAreaExact} to compute area and 

120 perimeter of a polygon. 

121 

122 @kwarg polyline: If C{True} perimeter only, otherwise 

123 area and perimeter (C{bool}). 

124 @kwarg name: Optional name (C{str}). 

125 

126 @return: A L{GeodesicAreaExact} instance. 

127 

128 @note: The B{C{debug}} setting is passed as C{verbose} 

129 to the returned L{GeodesicAreaExact} instance. 

130 ''' 

131 gaX = _MODS.geodesicx.GeodesicAreaExact(self, polyline=polyline, 

132 name=name or self.name) 

133 if self.verbose or self.debug: # PYCHOK no cover 

134 gaX.verbose = True 

135 return gaX 

136 

137 Polygon = Area # for C{geographiclib} compatibility 

138 

139 def Direct3(self, lat1, lon1, azi1, s12): # PYCHOK outmask 

140 '''Return the destination lat, lon and reverse azimuth 

141 (final bearing) in C{degrees}. 

142 

143 @return: L{Destination3Tuple}C{(lat, lon, final)}. 

144 ''' 

145 r = self._GDictDirect(lat1, lon1, azi1, False, s12, floats=False) 

146 return Destination3Tuple(float(r.lat2), float(r.lon2), wrap360(r.azi2), 

147 iteration=r._iteration) 

148 

149 def _DirectLine(self, ll1, azi12, **caps_name): # PYCHOK no cover 

150 '''(INTERNAL) Short-cut version. 

151 ''' 

152 return self.DirectLine(ll1.lat, ll1.lon, azi12, **caps_name) 

153 

154 def DirectLine(self, lat1, lon1, azi1, **caps_name): 

155 '''Set up a L{GeodesicLineSolve} to compute several points 

156 on a single geodesic. 

157 

158 @arg lat1: Latitude of the first point (C{degrees}). 

159 @arg lon1: Longitude of the first point (C{degrees}). 

160 @arg azi1: Azimuth at the first point (compass C{degrees}). 

161 @kwarg caps_name: Bit-or'ed combination of L{Caps} values specifying 

162 the capabilities the L{GeodesicLineSolve} instance should 

163 possess, C{caps=Caps.ALL} always. 

164 

165 @return: A L{GeodesicLineSolve} instance. 

166 

167 @note: If the point is at a pole, the azimuth is defined by keeping 

168 B{C{lon1}} fixed, writing C{B{lat1} = ±(90 − ε)}, and taking 

169 the limit C{ε → 0+}. 

170 

171 @see: C++ U{GeodesicExact.Line 

172 <https://GeographicLib.SourceForge.io/C++/doc/classGeographicLib_1_1GeodesicExact.html>} 

173 and Python U{Geodesic.Line<https://GeographicLib.SourceForge.io/Python/doc/code.html>}. 

174 ''' 

175 return GeodesicLineSolve(self, lat1, lon1, azi1, **_xkwds(caps_name, name=self.name)) 

176 

177 Line = DirectLine 

178 

179 def _Inverse(self, ll1, ll2, wrap, **outmask): # PYCHOK no cover 

180 '''(INTERNAL) Short-cut version, see .ellipsoidalBaseDI.intersecant2. 

181 ''' 

182 if wrap: 

183 ll2 = _unrollon(ll1, _Wrap.point(ll2)) 

184 return self.Inverse(ll1.lat, ll1.lon, ll2.lat, ll2.lon, **outmask) 

185 

186 def Inverse3(self, lat1, lon1, lat2, lon2): # PYCHOK outmask 

187 '''Return the distance in C{meter} and the forward and 

188 reverse azimuths (initial and final bearing) in C{degrees}. 

189 

190 @return: L{Distance3Tuple}C{(distance, initial, final)}. 

191 ''' 

192 r = self._GDictInverse(lat1, lon1, lat2, lon2, floats=False) 

193 return Distance3Tuple(float(r.s12), wrap360(r.azi1), wrap360(r.azi2), 

194 iteration=r._iteration) 

195 

196 def _InverseLine(self, ll1, ll2, wrap, **caps_name): # PYCHOK no cover 

197 '''(INTERNAL) Short-cut version. 

198 ''' 

199 if wrap: 

200 ll2 = _unrollon(ll1, _Wrap.point(ll2)) 

201 return self.InverseLine(ll1.lat, ll1.lon, ll2.lat, ll2.lon, **caps_name) 

202 

203 def InverseLine(self, lat1, lon1, lat2, lon2, **caps_name): # PYCHOK no cover 

204 '''Set up a L{GeodesicLineSolve} to compute several points 

205 on a single geodesic. 

206 

207 @arg lat1: Latitude of the first point (C{degrees}). 

208 @arg lon1: Longitude of the first point (C{degrees}). 

209 @arg lat2: Latitude of the second point (C{degrees}). 

210 @arg lon2: Longitude of the second point (C{degrees}). 

211 @kwarg caps_name: Bit-or'ed combination of L{Caps} values specifying 

212 the capabilities the L{GeodesicLineSolve} instance should 

213 possess, C{caps=Caps.ALL} always. 

214 

215 @return: A L{GeodesicLineSolve} instance. 

216 

217 @note: Both B{C{lat1}} and B{C{lat2}} should in the range C{[-90, +90]}. 

218 

219 @see: C++ U{GeodesicExact.InverseLine 

220 <https://GeographicLib.SourceForge.io/C++/doc/classGeographicLib_1_1GeodesicExact.html>} and 

221 Python U{Geodesic.InverseLine<https://GeographicLib.SourceForge.io/Python/doc/code.html>}. 

222 ''' 

223 r = self.Inverse(lat1, lon1, lat2, lon2) 

224 return GeodesicLineSolve(self, lat1, lon1, r.azi1, **_xkwds(caps_name, name=self.name)) 

225 

226 

227class GeodesicLineSolve(_GeodesicSolveBase, _SolveLineBase): 

228 '''Wrapper to invoke I{Karney}'s U{GeodSolve<https://GeographicLib.SourceForge.io/C++/doc/GeodSolve.1.html>} 

229 as an C{Exact} version of I{Karney}'s Python class U{GeodesicLine<https://GeographicLib.SourceForge.io/C++/doc/ 

230 python/code.html#geographiclib.geodesicline.GeodesicLine>}. 

231 

232 @note: Use property C{GeodSolve} or env variable C{PYGEODESY_GEODSOLVE} to specify the (fully 

233 qualified) path to the C{GeodSolve} executable. 

234 

235 @note: This C{geodesic} is intended I{for testing purposes only}, it invokes the C{GeodSolve} 

236 executable for I{every} method call. 

237 ''' 

238 

239 def __init__(self, geodesic, lat1, lon1, azi1, caps=Caps.ALL, name=NN): 

240 '''New L{GeodesicLineSolve} instance, allowing points to be found along 

241 a geodesic starting at C{(B{lat1}, B{lon1})} with azimuth B{C{azi1}}. 

242 

243 @arg geodesic: The geodesic to use (L{GeodesicSolve}). 

244 @arg lat1: Latitude of the first point (C{degrees}). 

245 @arg lon1: Longitude of the first point (C{degrees}). 

246 @arg azi1: Azimuth at the first points (compass C{degrees}). 

247 @kwarg caps: Bit-or'ed combination of L{Caps} values specifying 

248 the capabilities the L{GeodesicLineSolve} instance 

249 should possess, always C{Caps.ALL}. Use C{Caps.LINE_OFF} 

250 if updates to the B{C{geodesic}} should I{not} be 

251 reflected in this L{GeodesicLineSolve} instance. 

252 @kwarg name: Optional name (C{str}). 

253 

254 @raise GeodesicError: Invalid path for the C{GeodSolve} executable or 

255 or isn't the C{GeodSolve} executable, see 

256 property C{geodesic.GeodSolve}. 

257 

258 @raise TypeError: Invalid B{C{geodesic}}. 

259 ''' 

260 _xinstanceof(GeodesicSolve, geodesic=geodesic) 

261 if (caps & Caps.LINE_OFF): # copy to avoid updates 

262 geodesic = geodesic.copy(deep=False, name=NN(_UNDER_, geodesic.name)) 

263 _SolveLineBase.__init__(self, geodesic, lat1, lon1, caps, name, azi1=azi1) 

264 try: 

265 self.GeodSolve = geodesic.GeodSolve # geodesic or copy of geodesic 

266 except GeodesicError: 

267 pass 

268 

269 def ArcPosition(self, a12, outmask=_UNUSED_): # PYCHOK unused 

270 '''Find the position on the line given B{C{a12}}. 

271 

272 @arg a12: Spherical arc length from the first point to the 

273 second point (C{degrees}). 

274 

275 @return: A C{GDict} with 12 items C{lat1, lon1, azi1, lat2, lon2, 

276 azi2, m12, a12, s12, M12, M21, S12}. 

277 ''' 

278 return self._GDictInvoke(self._cmdArc, True, self._Names_Direct, a12) 

279 

280 @Property_RO 

281 def azi1(self): 

282 '''Get the azimuth at the first point (compass C{degrees}). 

283 ''' 

284 return self._lla1.azi1 

285 

286 azi12 = azi1 # like RhumbLineSolve 

287 

288 @Property_RO 

289 def azi1_sincos2(self): 

290 '''Get the sine and cosine of the first point's azimuth (2-tuple C{(sin, cos)}). 

291 ''' 

292 return _sincos2d(self.azi1) 

293 

294 azi12_sincos2 = azi1_sincos2 

295 

296 @Property_RO 

297 def _cmdArc(self): 

298 '''(INTERNAL) Get the C{GeodSolve} I{-a -L} cmd (C{tuple}). 

299 ''' 

300 return self._cmdDistance + ('-a',) 

301 

302 def Intersecant2(self, lat0, lon0, radius, **kwds): # PYCHOK no cover 

303 '''B{Not implemented}, throws a C{NotImplementedError} always.''' 

304 _MODS.named.notImplemented(self, lat0, lon0, radius, **kwds) 

305 

306 def PlumbTo(self, lat0, lon0, **kwds): # PYCHOK no cover 

307 '''B{Not implemented}, throws a C{NotImplementedError} always.''' 

308 _MODS.named.notImplemented(self, lat0, lon0, **kwds) 

309 

310 def Position(self, s12, outmask=_UNUSED_): # PYCHOK unused 

311 '''Find the position on the line given B{C{s12}}. 

312 

313 @arg s12: Distance from the first point to the second (C{meter}). 

314 

315 @return: A C{GDict} with 12 items C{lat1, lon1, azi1, lat2, lon2, 

316 azi2, m12, a12, s12, M12, M21, S12}, possibly C{a12=NAN}. 

317 ''' 

318 return self._GDictInvoke(self._cmdDistance, True, self._Names_Direct, s12) 

319 

320 def toStr(self, **prec_sep): # PYCHOK signature 

321 '''Return this C{GeodesicLineSolve} as string. 

322 

323 @kwarg prec_sep: Keyword argumens C{B{prec}=6} and C{B{sep}=', '} 

324 for the C{float} C{prec}ision, number of decimal digits 

325 (0..9) and the C{sep}arator string to join. Trailing 

326 zero decimals are stripped for B{C{prec}} values of 

327 1 and above, but kept for negative B{C{prec}} values. 

328 

329 @return: GeodesicLineSolve items (C{str}). 

330 ''' 

331 return _SolveLineBase._toStr(self, azi1=self.azi1, geodesic=self._solve, 

332 GeodSolve=self.GeodSolve, **prec_sep) 

333 

334 

335__all__ += _ALL_DOCS(_GeodesicSolveBase) 

336 

337if __name__ == '__main__': 

338 

339 from sys import argv 

340 

341 gS = GeodesicSolve(name='Test') 

342 gS.verbose = '--verbose' in argv # or '-v' in argv 

343 

344 if gS.GeodSolve in (_PYGEODESY_GEODSOLVE_, None): # not set 

345 gS.GeodSolve = '/opt/local/bin/GeodSolve' # '/opt/local/Cellar/geographiclib/1.51/bin/GeodSolve' # HomeBrew 

346 printf('version: %s', gS.version) 

347 

348 r = gS.Direct(40.6, -73.8, 51, 5.5e6) 

349 printf('Direct: %r', r, nl=1) 

350 printf('Direct3: %r', gS.Direct3(40.6, -73.8, 51, 5.5e6)) 

351 

352 printf('Inverse: %r', gS.Inverse( 40.6, -73.8, 51.6, -0.5), nl=1) 

353 printf('Inverse1: %r', gS.Inverse1(40.6, -73.8, 51.6, -0.5)) 

354 printf('Inverse3: %r', gS.Inverse3(40.6, -73.8, 51.6, -0.5)) 

355 

356 glS = GeodesicLineSolve(gS, 40.6, -73.8, 51, name='LineTest') 

357 p = glS.Position(5.5e6) 

358 printf('Position: %s %r', p == r, p, nl=1) 

359 p = glS.ArcPosition(49.475527) 

360 printf('ArcPosition: %s %r', p == r, p) 

361 

362# % python3 -m pygeodesy.geodsolve 

363 

364# version: /opt/local/bin/GeodSolve: GeographicLib version 1.51 

365 

366# version: /opt/local/bin/GeodSolve: GeographicLib version 1.51 

367 

368# Direct: GDict(M12=0.650911, M21=0.651229, S12=39735075134877.09375, a12=49.475527, azi1=51.0, azi2=107.189397, lat1=40.6, lat2=51.884565, lon1=-73.8, lon2=-1.141173, m12=4844148.703101, s12=5500000.0) 

369# Direct3: Destination3Tuple(lat=51.884565, lon=-1.141173, final=107.189397) 

370 

371# Inverse: GDict(M12=0.64473, M21=0.645046, S12=40041368848742.53125, a12=49.94131, azi1=51.198883, azi2=107.821777, lat1=40.6, lat2=51.6, lon1=-73.8, lon2=-0.5, m12=4877684.602706, s12=5551759.400319) 

372# Inverse1: 49.94131021789904 

373# Inverse3: Distance3Tuple(distance=5551759.400319, initial=51.198883, final=107.821777) 

374 

375# Position: True GDict(M12=0.650911, M21=0.651229, S12=39735075134877.09375, a12=49.475527, azi1=51.0, azi2=107.189397, lat1=40.6, lat2=51.884565, lon1=-73.8, lon2=-1.141173, m12=4844148.703101, s12=5500000.0) 

376# ArcPosition: False GDict(M12=0.650911, M21=0.651229, S12=39735074737272.734375, a12=49.475527, azi1=51.0, azi2=107.189397, lat1=40.6, lat2=51.884565, lon1=-73.8, lon2=-1.141174, m12=4844148.669561, s12=5499999.948497) 

377 

378 

379# % python3 -m pygeodesy.geodsolve --verbose 

380 

381# GeodesicSolve 'Test' 1: /opt/local/bin/GeodSolve --version (invoke) 

382# GeodesicSolve 'Test' 1: /opt/local/bin/GeodSolve: GeographicLib version 1.51 (0) 

383# version: /opt/local/bin/GeodSolve: GeographicLib version 1.51 

384# GeodesicSolve 'Test' 2: /opt/local/bin/GeodSolve -E -p 10 -f \ 40.600000000000001 -73.799999999999997 51.0 5500000.0 (Direct) 

385# GeodesicSolve 'Test' 2: lat1=40.600000000000001, lon1=-73.799999999999997, azi1=51.0, lat2=51.884564505606761, lon2=-1.141172861200829, azi2=107.189397162605886, s12=5500000.0, a12=49.475527463251467, m12=4844148.703101486, M12=0.65091056699808603, M21=0.65122865892196558, S12=39735075134877.094 (0) 

386 

387# Direct: GDict(M12=0.650911, M21=0.651229, S12=39735075134877.09375, a12=49.475527, azi1=51.0, azi2=107.189397, lat1=40.6, lat2=51.884565, lon1=-73.8, lon2=-1.141173, m12=4844148.703101, s12=5500000.0) 

388# GeodesicSolve 'Test' 3: /opt/local/bin/GeodSolve -E -p 10 -f \ 40.600000000000001 -73.799999999999997 51.0 5500000.0 (Direct3) 

389# GeodesicSolve 'Test' 3: lat1=40.600000000000001, lon1=-73.799999999999997, azi1=51.0, lat2=51.884564505606761, lon2=-1.141172861200829, azi2=107.189397162605886, s12=5500000.0, a12=49.475527463251467, m12=4844148.703101486, M12=0.65091056699808603, M21=0.65122865892196558, S12=39735075134877.094 (0) 

390# Direct3: Destination3Tuple(lat=51.884565, lon=-1.141173, final=107.189397) 

391# GeodesicSolve 'Test' 4: /opt/local/bin/GeodSolve -E -p 10 -f -i \ 40.600000000000001 -73.799999999999997 51.600000000000001 -0.5 (Inverse) 

392# GeodesicSolve 'Test' 4: lat1=40.600000000000001, lon1=-73.799999999999997, azi1=51.198882845579824, lat2=51.600000000000001, lon2=-0.5, azi2=107.821776735514248, s12=5551759.4003186841, a12=49.941310217899037, m12=4877684.6027061976, M12=0.64472969205948238, M21=0.64504567852134398, S12=40041368848742.531 (0) 

393 

394# Inverse: GDict(M12=0.64473, M21=0.645046, S12=40041368848742.53125, a12=49.94131, azi1=51.198883, azi2=107.821777, lat1=40.6, lat2=51.6, lon1=-73.8, lon2=-0.5, m12=4877684.602706, s12=5551759.400319) 

395# GeodesicSolve 'Test' 5: /opt/local/bin/GeodSolve -E -p 10 -f -i \ 40.600000000000001 -73.799999999999997 51.600000000000001 -0.5 (Inverse1) 

396# GeodesicSolve 'Test' 5: lat1=40.600000000000001, lon1=-73.799999999999997, azi1=51.198882845579824, lat2=51.600000000000001, lon2=-0.5, azi2=107.821776735514248, s12=5551759.4003186841, a12=49.941310217899037, m12=4877684.6027061976, M12=0.64472969205948238, M21=0.64504567852134398, S12=40041368848742.531 (0) 

397# Inverse1: 49.94131021789904 

398# GeodesicSolve 'Test' 6: /opt/local/bin/GeodSolve -E -p 10 -f -i \ 40.600000000000001 -73.799999999999997 51.600000000000001 -0.5 (Inverse3) 

399# GeodesicSolve 'Test' 6: lat1=40.600000000000001, lon1=-73.799999999999997, azi1=51.198882845579824, lat2=51.600000000000001, lon2=-0.5, azi2=107.821776735514248, s12=5551759.4003186841, a12=49.941310217899037, m12=4877684.6027061976, M12=0.64472969205948238, M21=0.64504567852134398, S12=40041368848742.531 (0) 

400# Inverse3: Distance3Tuple(distance=5551759.400319, initial=51.198883, final=107.821777) 

401 

402# Position: True GDict(M12=0.650911, M21=0.651229, S12=39735075134877.09375, a12=49.475527, azi1=51.0, azi2=107.189397, lat1=40.6, lat2=51.884565, lon1=-73.8, lon2=-1.141173, m12=4844148.703101, s12=5500000.0) 

403# ArcPosition: False GDict(M12=0.650911, M21=0.651229, S12=39735074737272.734375, a12=49.475527, azi1=51.0, azi2=107.189397, lat1=40.6, lat2=51.884565, lon1=-73.8, lon2=-1.141174, m12=4844148.669561, s12=5499999.948497) 

404 

405 

406# **) MIT License 

407# 

408# Copyright (C) 2016-2024 -- mrJean1 at Gmail -- All Rights Reserved. 

409# 

410# Permission is hereby granted, free of charge, to any person obtaining a 

411# copy of this software and associated documentation files (the "Software"), 

412# to deal in the Software without restriction, including without limitation 

413# the rights to use, copy, modify, merge, publish, distribute, sublicense, 

414# and/or sell copies of the Software, and to permit persons to whom the 

415# Software is furnished to do so, subject to the following conditions: 

416# 

417# The above copyright notice and this permission notice shall be included 

418# in all copies or substantial portions of the Software. 

419# 

420# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 

421# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 

422# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 

423# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR 

424# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 

425# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 

426# OTHER DEALINGS IN THE SOFTWARE.