Coverage for pygeodesy/rhumb/solve.py: 89%

103 statements  

« prev     ^ index     » next       coverage.py v7.2.2, created at 2024-06-01 11:43 -0400

1 

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

3 

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

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

6as an (exact) rhumb or rhumb line from I{either GeographicLib 2.0 or 2.2+}. 

7 

8@note: Set env variable C{PYGEODESY_RHUMBSOLVE} to the (fully qualified) 

9 path of the C{RhumbSolve} executable. 

10''' 

11from pygeodesy.basics import _xinstanceof 

12from pygeodesy.constants import _0_0, _180_0, _N_180_0, _over, _90_0 # PYCHOK used! 

13from pygeodesy.errors import RhumbError # PYCHOK used! 

14from pygeodesy.interns import NN, _a12_, _azi12_, _lat2_, _lon2_, _s12_, _S12_, _UNDER_ 

15from pygeodesy.karney import Caps, GDict, _norm180, Rhumb8Tuple, _sincos2d 

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

17 _PYGEODESY_RHUMBSOLVE_ 

18from pygeodesy.namedTuples import Destination3Tuple, Distance3Tuple 

19from pygeodesy.props import deprecated_method, Property, Property_RO 

20from pygeodesy.solveBase import _SolveBase, _SolveLineBase 

21from pygeodesy.utily import _unrollon, _Wrap, wrap360 

22 

23__all__ = _ALL_LAZY.rhumb_solve 

24__version__ = '24.05.31' 

25 

26 

27class _RhumbSolveBase(_SolveBase): 

28 '''(INTERNAL) Base class for L{RhumbSolve} and L{RhumbLineSolve}. 

29 ''' 

30 _Error = RhumbError 

31 _Names_Direct = _lat2_, _lon2_, _S12_ 

32 _Names_Inverse = _azi12_, _s12_, _S12_ 

33 _Solve_name = 'RhumbSolve' 

34 _Solve_path = _getenv(_PYGEODESY_RHUMBSOLVE_, _PYGEODESY_RHUMBSOLVE_) 

35 

36 @Property_RO 

37 def _cmdBasic(self): 

38 '''(INTERNAL) Get the basic C{RhumbSolve} cmd (C{tuple}). 

39 ''' 

40 return (self.RhumbSolve,) + self._e_option \ 

41 + self._p_option \ 

42 + self._s_option 

43 

44 @Property 

45 def RhumbSolve(self): 

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

47 executable (C{filename}). 

48 ''' 

49 return self._Solve_path 

50 

51 @RhumbSolve.setter # PYCHOK setter! 

52 def RhumbSolve(self, path): 

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

54 executable (C{filename}), the (fully qualified) path to the C{RhumbSolve} executable. 

55 

56 @raise RhumbError: Invalid B{C{path}}, B{C{path}} doesn't exist or isn't 

57 the C{RhumbSolve} executable. 

58 ''' 

59 self._setSolve(path) 

60 

61 @Property_RO 

62 def _s_option(self): # == not -E for GeodSolve 

63 return () if self.Exact else ('-s',) 

64 

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

66 '''Return this C{RhumbSolve} as string. 

67 

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

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

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

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

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

73 

74 @return: RhumbSolve items (C{str}). 

75 ''' 

76 return self._toStr(RhumbSolve=self.RhumbSolve, **prec_sep) 

77 

78# @Property_RO 

79# def _u_option(self): 

80# return '-u' if self.unroll else () 

81 

82 

83class RhumbSolve(_RhumbSolveBase): 

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

85 like a class, similar to L{pygeodesy.Rhumb} and L{pygeodesy.RhumbAux}. 

86 

87 @note: Use property C{RhumbSolve} or env variable C{PYGEODESY_RHUMBSOLVE} to specify the (fully 

88 qualified) path to the C{RhumbSolve} executable. 

89 

90 @note: This C{rhumb} is intended I{for testing purposes only}, it invokes the C{RhumbSolve} 

91 executable for I{every} method call. 

92 ''' 

93# def Area(self, polyline=False, **name): 

94# '''Set up a L{RhumbArea} to compute area and 

95# perimeter of a polygon. 

96# 

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

98# area and perimeter (C{bool}). 

99# @kwarg name: Optional C{B{name}=NN} (C{str}). 

100# 

101# @return: A L{RhumbArea} instance. 

102# 

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

104# to the returned L{RhumbAreaExact} instance. 

105# ''' 

106# rA = _MODS.rhumbs.rhumb*.RhumbArea(self, polyline=polyline, 

107# name=self._name__(name)) 

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

109# rA.verbose = True 

110# return rA 

111 

112# Polygon = Area # for C{geographiclib} compatibility 

113 

114 def _azimuth_reverse(self, azimuth): 

115 '''(INTERNAL) Reverse final azimuth C{azimuth}. 

116 ''' 

117 z = _norm180(float(azimuth)) 

118 if self.reverse2: # like .utils.atan2d 

119 z += _180_0 if z < 0 else _N_180_0 

120 return z 

121 

122 def _Direct(self, ll1, azi12, s12, **outmask): 

123 '''(INTERNAL) Short-cut version, see .latlonBase. 

124 ''' 

125 return self.Direct(ll1.lat, ll1.lon, azi12, s12, **outmask) 

126 

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

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

129 (final bearing) in C{degrees}. 

130 

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

132 ''' 

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

134 z = self._azimuth_reverse(r.azi12) 

135 return Destination3Tuple(float(r.lat2), float(r.lon2), wrap360(z), 

136 iteration=r._iteration) 

137 

138 def _DirectLine(self, ll1, azi12, **name_caps): 

139 '''(INTERNAL) Short-cut version, see .latlonBase. 

140 ''' 

141 return self.DirectLine(ll1.lat, ll1.lon, azi12, **name_caps) 

142 

143 def DirectLine(self, lat1, lon1, azi1, caps=Caps.STANDARD, **name): 

144 '''Set up a L{RhumbLineSolve} in terms of the I{direct} rhumb 

145 problem to compute several points on a single rhumb line. 

146 

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

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

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

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

151 the capabilities the L{RhumbLineSolve} instance 

152 should possess, always C{Caps.ALL}. 

153 @kwarg name: Optional C{B{name}=NN} (C{str}). 

154 

155 @return: A L{RhumbLineSolve} instance. 

156 

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

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

159 the limit C{ε → 0+}. 

160 

161 @see: C++ U{RhumbExact.Line 

162 <https://GeographicLib.SourceForge.io/C++/doc/classGeographicLib_1_1RhumbExact.html>} 

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

164 ''' 

165 return RhumbLineSolve(self, lat1, lon1, azi1, caps=caps, name=self._name__(name)) 

166 

167 def _GDictDirect(self, lat, lon, azi1, arcmode, s12_a12, *unused, **floats): # PYCHOK signature 

168 '''(INTERNAL) Get C{_GenDirect}-like result as an 8-item C{GDict}. 

169 ''' 

170 d = _RhumbSolveBase._GDictDirect(self, lat, lon, azi1, arcmode, s12_a12, **floats) 

171 r = GDict(lat1=lat, lon1=lon, azi12=azi1, s12=s12_a12) # a12=_over(s12_a12, self._mpd) 

172 r.update(d) 

173 return r 

174 

175 def _GDictInverse(self, lat1, lon1, lat2, lon2, *unused, **floats): # PYCHOK signature 

176 '''(INTERNAL) Get C{_GenInverse}-like result as an 8-item C{GDict}, but 

177 I{without} C{_SALPs_CALPs_}. 

178 ''' 

179 i = _RhumbSolveBase._GDictInverse(self, lat1, lon1, lat2, lon2, **floats) 

180 a = _over(float(i.s12), self._mpd) # for .Inverse1 

181 r = GDict(lat1=lat1, lon1=lon1, lat2=lat2, lon2=lon2, a12=a) 

182 r.update(i) 

183 return r 

184 

185 def _Inverse(self, ll1, ll2, wrap, **unused): 

186 '''(INTERNAL) Short-cut version, see .latlonBase. 

187 ''' 

188 if wrap: 

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

190 return self._GDictInverse(ll1.lat, ll1.lon, ll2.lat, ll2.lon) 

191 

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

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

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

195 

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

197 ''' 

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

199 z = self._azimuth_reverse(r.azi12) 

200 return Distance3Tuple(float(r.s12), wrap360(r.azi12), wrap360(z), 

201 iteration=r._iteration) 

202 

203 def _InverseLine(self, ll1, ll2, wrap, **name_caps): 

204 '''(INTERNAL) Short-cut version, see .latlonBase. 

205 ''' 

206 if wrap: 

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

208 return self.InverseLine(ll1.lat, ll1.lon, ll2.lat, ll2.lon, **name_caps) 

209 

210 def InverseLine(self, lat1, lon1, lat2, lon2, caps=Caps.STANDARD, **name): 

211 '''Define a L{RhumbLineSolve} in terms of the I{inverse} 

212 rhumb problem. 

213 

214 @arg lat1: Latitude of the first point (C{degrees90}). 

215 @arg lon1: Longitude of the first point (C{degrees180}). 

216 @arg lat2: Latitude of the second point (C{degrees90}). 

217 @arg lon2: Longitude of the second point (C{degrees180}). 

218 @kwarg caps: Optional C{caps}, see L{RhumbLine} C{B{caps}}. 

219 @kwarg name: Optional C{B{name}=NN} (C{str}). 

220 

221 @return: A L{RhumbLineSolve} instance and invoke its method 

222 L{RhumbLine.Position} to compute each point. 

223 

224 @note: Updates to this rhumb are reflected in the returned 

225 rhumb line. 

226 ''' 

227 r = self.Inverse(lat1, lon1, lat2, lon2) # outmask=Caps.AZIMUTH 

228 return RhumbLineSolve(self, lat1, lon1, r.azi12, caps=caps, 

229 name=self._name__(name)) 

230 

231 Line = DirectLine 

232 

233 

234class RhumbLineSolve(_RhumbSolveBase, _SolveLineBase): 

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

236 like a class, similar to L{pygeodesy.RhumbLine} and L{pygeodesy.RhumbLineAux}. 

237 

238 @note: Use property C{RhumbSolve} or env variable C{PYGEODESY_RHUMBSOLVE} to specify the (fully 

239 qualified) path to the C{RhumbSolve} executable. 

240 

241 @note: This C{rhumb line} is intended I{for testing purposes only}, it invokes the C{RhumbSolve} 

242 executable for I{every} method call. 

243 ''' 

244 def __init__(self, rhumb, lat1, lon1, azi12, caps=Caps.STANDARD, **name): 

245 '''New L{RhumbLineSolve} instance, allowing points to be found along 

246 a rhumb starting at C{(B{lat1}, B{lon1})} with azimuth B{C{azi12}}. 

247 

248 @arg rhumb: The rhumb to use (L{RhumbSolve}). 

249 @arg lat1: Latitude of the first point (C{degrees90}). 

250 @arg lon1: Longitude of the first point (C{degrees180}). 

251 @arg azi12: Azimuth of the rhumb line (compass C{degrees180}). 

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

253 the capabilities the L{RhumbLineSolve} instance should 

254 possess, always C{Caps.ALL}. Use C{Caps.LINE_OFF} 

255 if updates to the B{C{rhumb}} should I{not} be 

256 reflected in this L{RhumbLineSolve} instance. 

257 @kwarg name: Optional C{B{name}=NN} (C{str}). 

258 

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

260 

261 @raise RhumbError: Invalid path for C{RhumbSolve} executable or 

262 isn't the C{RhumbSolve} executable, see 

263 property C{B{rhumb}.RhumbSolve}. 

264 

265 @raise TypeError: Invalid B{C{rhumb}}. 

266 ''' 

267 _xinstanceof(RhumbSolve, rhumb=rhumb) 

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

269 rhumb = rhumb.copy(deep=False, name=NN(_UNDER_, rhumb.name)) 

270 _SolveLineBase.__init__(self, rhumb, lat1, lon1, caps, azi12=azi12, **name) 

271 try: 

272 self.RhumbSolve = rhumb.RhumbSolve # rhumb or copy of rhumb 

273 except RhumbError: 

274 pass 

275 

276# def ArcPosition(self, a12, *unused): 

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

278# 

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

280# second point (C{degrees}). 

281# 

282# @return: A C{dict} with 8 items C{lat1, lon1, lat2, lon2, 

283# azi12, a12, s12, S12}. 

284# ''' 

285# s = a12 * self._mpd 

286# a = self._GDictInvoke(self._cmdArc, True, self._Names_Direct, s) 

287# r = GDict(a12=a12, s12=s, **self._lla1) 

288# r.updated(a) 

289# return r 

290 

291 @Property_RO 

292 def azi12(self): 

293 '''Get this rhumb line's azimuth (compass C{degrees}). 

294 ''' 

295 return self._lla1.azi12 

296 

297 azi1 = azi12 # like GeodesicLineSolve 

298 

299 @Property_RO 

300 def azi12_sincos2(self): # PYCHOK no cover 

301 '''Get the sine and cosine of this rhumb line's azimuth (2-tuple C{(sin, cos)}). 

302 ''' 

303 return _sincos2d(self.azi12) 

304 

305 azi1_sincos2 = azi12_sincos2 

306 

307# @Property_RO 

308# def _cmdArc(self): 

309# '''(INTERNAL) Get the C{RhumbSolve} I{-a -L} cmd (C{tuple}). 

310# ''' 

311# return self._cmdDistance + ('-a',) 

312 

313 def Position(self, s12, **unused): 

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

315 

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

317 

318 @return: A L{GDict} with 7 items C{lat1, lon1, lat2, lon2, 

319 azi12, s12, S12}. 

320 ''' 

321 d = self._GDictInvoke(self._cmdDistance, True, self._Names_Direct, s12) 

322 r = GDict(s12=s12, **self._lla1) # a12=_over(s12, self._mpd) 

323 r.update(d) 

324 return r 

325 

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

327 '''Return this C{RhumbLineSolve} as string. 

328 

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

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

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

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

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

334 

335 @return: RhumbLineSolve items (C{str}). 

336 ''' 

337 return _SolveLineBase._toStr(self, azi12=self.azi12, rhumb=self._solve, 

338 RhumbSolve=self.RhumbSolve, **prec_sep) 

339 

340 

341class RhumbSolve7Tuple(Rhumb8Tuple): 

342 '''7-Tuple C{(lat1, lon1, lat2, lon2, azi12, s12, S12)} with lat- C{lat1}, 

343 C{lat2} and longitudes C{lon1}, C{lon2} of both points, the azimuth of 

344 the rhumb line C{azi12}, the distance C{s12} and the area C{S12} under 

345 the rhumb line between both points. 

346 ''' 

347 assert Rhumb8Tuple._Names_.index(_a12_) == 7 

348 _Names_ = Rhumb8Tuple._Names_[:7] # drop a12 

349 _Units_ = Rhumb8Tuple._Units_[:7] 

350 

351 @deprecated_method 

352 def _to7Tuple(self): # PYCHOK no cover 

353 '''DEPRECATED, I{don't use!} 

354 ''' 

355 return _MODS.deprecated.classes.Rhumb7Tuple(self[:7]) 

356 

357 

358__all__ += _ALL_DOCS(_RhumbSolveBase) 

359 

360if __name__ == '__main__': 

361 

362 from pygeodesy import printf 

363 from sys import argv 

364 

365 def rhumb_intercept(rS, lat1, lon1, lat2, lon2, azi2, s23): 

366 # using RhumbSolve and GeodesicExact for I{Karney}'s C++ U{rhumb-intercept.cpp 

367 # <https://SourceForge.net/p/geographiclib/discussion/1026620/thread/2ddc295e/> 

368 from pygeodesy.constants import EPS4 as _TOL 

369 from pygeodesy.karney import _diff182 

370 

371 E = rS.ellipsoid 

372 gX = E.geodesicx # == GeodesicExact(E) 

373 m = gX.STANDARD | gX.REDUCEDLENGTH | gX.GEODESICSCALE 

374 

375 rlS = rS.Line(lat2, lon2, azi2) 

376 sa, _ = rlS.azi12_sincos2 # aka _salp, _calp 

377 for i in range(1, 16): 

378 p = rlS.Position(s23) # outmask=gX.LATITUDE_LONGITUDE 

379 r = gX.Inverse(lat1, lon1, p.lat2, p.lon2, outmask=m) 

380 d, _ = _diff182(azi2, r.azi2, K_2_0=True) 

381 s, c = _sincos2d(d) 

382 printf('%2d %.3f %.8f, %.8f, %.8e', 

383 i, s23, r.lat2, r.lon2, c) 

384 s2, c2 = _sincos2d(r.lat2) 

385 c2 *= E.rocTransverse(r.lat2) 

386 if c2 and r.m12: 

387 s *= (s2 * sa) / c2 - s * r.M21 / r.m12 

388 t = (c / s) if s else _0_0 

389 if abs(t) < _TOL: 

390 break 

391 s23 += t 

392 else: 

393 break 

394 

395 rS = RhumbSolve(name='Test') 

396 rS.verbose = '--verbose' in argv # or '-v' in argv 

397 

398 if rS.RhumbSolve in (_PYGEODESY_RHUMBSOLVE_, None): # not set 

399 rS.RhumbSolve = '/opt/local/bin/RhumbSolve' # '/opt/local/Cellar/geographiclib/2.2/bin/RhumbSolve' # HomeBrew 

400 printf('version: %s', rS.version) 

401 

402 if len(argv) > 6: # 60 0 30 0 45 1e6 

403 t = (14, 's23'), (7, 'lat3'), (11, 'lon3'), (13, 'cos()') 

404 printf(' '.join('%*s' % _ for _ in t)) 

405 rhumb_intercept(rS, *map(float, argv[-6:])) 

406 exit() 

407 

408 r = rS.Direct(40.6, -73.8, 51, 5.5e6) 

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

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

411 

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

413 printf('Inverse1: %r', rS.Inverse1(40.6, -73.8, 51.6, -0.5)) 

414 printf('Inverse3: %r', rS.Inverse3(40.6, -73.8, 51.6, -0.5)) 

415 

416 printf('Inverse: %r', rS.Inverse( 40.6, -73.8, 35.8, 140.3), nl=1) 

417 printf('Inverse1: %r', rS.Inverse1(40.6, -73.8, 35.8, 140.3)) 

418 printf('Inverse3: %r', rS.Inverse3(40.6, -73.8, 35.8, 140.3)) 

419 

420 rlS = RhumbLineSolve(rS, 40.6, -73.8, 51, name='LineTest') 

421 p = rlS.Position(5.5e6) 

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

423# p = rlS.ArcPosition(49.475527) 

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

425 

426# % python3 -m pygeodesy.rhumb.solve 

427 

428# version: /opt/local/bin/RhumbSolve: GeographicLib version 1.51 

429# 

430# Direct: GDict(S12=44095641862956.148438, azi12=51, lat1=40.6, lat2=71.6889, lon1=-73.8, lon2=0.25552, s12=5500000.0) 

431# Direct3: Destination3Tuple(lat=71.6889, lon=0.25552, final=51.0) 

432# 

433# Inverse: GDict(S12=37395209100030.367188, a12=51.929543, azi12=77.76839, lat1=40.6, lat2=51.6, lon1=-73.8, lon2=-0.5, s12=5771083.383328) 

434# Inverse1: 51.92954250756195 

435# Inverse3: Distance3Tuple(distance=5771083.383328, initial=77.76839, final=77.76839) 

436# 

437# Inverse: GDict(S12=-63760642939072.492188, a12=115.02062, azi12=-92.388888, lat1=40.6, lat2=35.8, lon1=-73.8, lon2=140.3, s12=12782581.067684) 

438# Inverse1: 115.02061966879258 

439# Inverse3: Distance3Tuple(distance=12782581.067684, initial=267.611112, final=267.611112) 

440# 

441# Position: True GDict(S12=44095641862956.148438, azi12=51, lat1=40.6, lat2=71.6889, lon1=-73.8, lon2=0.25552, s12=5500000.0) 

442 

443 

444# % python3 -m pygeodesy.rhumb.solve --verbose 

445 

446# RhumbSolve 'Test' 1: /opt/local/bin/RhumbSolve --version (invoke) 

447# RhumbSolve 'Test' 1: /opt/local/bin/RhumbSolve: GeographicLib version 1.51 (0) 

448# version: /opt/local/bin/RhumbSolve: GeographicLib version 1.51 

449# RhumbSolve 'Test' 2: /opt/local/bin/RhumbSolve -p 10 \ 40.600000000000001 -73.799999999999997 51.0 5500000.0 (Direct) 

450# RhumbSolve 'Test' 2: lat2=71.688899882813047, lon2=0.255519824423445, S12=44095641862956.148 (0) 

451 

452# Direct: GDict(S12=44095641862956.148438, azi12=51, lat1=40.6, lat2=71.6889, lon1=-73.8, lon2=0.25552, s12=5500000.0) 

453# RhumbSolve 'Test' 3: /opt/local/bin/RhumbSolve -p 10 \ 40.600000000000001 -73.799999999999997 51.0 5500000.0 (Direct3) 

454# RhumbSolve 'Test' 3: lat2=71.688899882813047, lon2=0.255519824423445, S12=44095641862956.148 (0) 

455# Direct3: Destination3Tuple(lat=71.6889, lon=0.25552, final=51.0) 

456# RhumbSolve 'Test' 4: /opt/local/bin/RhumbSolve -p 10 -i \ 40.600000000000001 -73.799999999999997 51.600000000000001 -0.5 (Inverse) 

457# RhumbSolve 'Test' 4: azi12=77.768389710255661, s12=5771083.3833280317, S12=37395209100030.367 (0) 

458 

459# Inverse: GDict(S12=37395209100030.367188, a12=51.929543, azi12=77.76839, lat1=40.6, lat2=51.6, lon1=-73.8, lon2=-0.5, s12=5771083.383328) 

460# RhumbSolve 'Test' 5: /opt/local/bin/RhumbSolve -p 10 -i \ 40.600000000000001 -73.799999999999997 51.600000000000001 -0.5 (Inverse1) 

461# RhumbSolve 'Test' 5: azi12=77.768389710255661, s12=5771083.3833280317, S12=37395209100030.367 (0) 

462# Inverse1: 51.92954250756195 

463# RhumbSolve 'Test' 6: /opt/local/bin/RhumbSolve -p 10 -i \ 40.600000000000001 -73.799999999999997 51.600000000000001 -0.5 (Inverse3) 

464# RhumbSolve 'Test' 6: azi12=77.768389710255661, s12=5771083.3833280317, S12=37395209100030.367 (0) 

465# Inverse3: Distance3Tuple(distance=5771083.383328, initial=77.76839, final=77.76839) 

466# RhumbSolve 'Test' 7: /opt/local/bin/RhumbSolve -p 10 -i \ 40.600000000000001 -73.799999999999997 35.799999999999997 140.300000000000011 (Inverse) 

467# RhumbSolve 'Test' 7: azi12=-92.388887981699639, s12=12782581.0676841792, S12=-63760642939072.492 (0) 

468 

469# Inverse: GDict(S12=-63760642939072.492188, a12=115.02062, azi12=-92.388888, lat1=40.6, lat2=35.8, lon1=-73.8, lon2=140.3, s12=12782581.067684) 

470# RhumbSolve 'Test' 8: /opt/local/bin/RhumbSolve -p 10 -i \ 40.600000000000001 -73.799999999999997 35.799999999999997 140.300000000000011 (Inverse1) 

471# RhumbSolve 'Test' 8: azi12=-92.388887981699639, s12=12782581.0676841792, S12=-63760642939072.492 (0) 

472# Inverse1: 115.02061966879258 

473# RhumbSolve 'Test' 9: /opt/local/bin/RhumbSolve -p 10 -i \ 40.600000000000001 -73.799999999999997 35.799999999999997 140.300000000000011 (Inverse3) 

474# RhumbSolve 'Test' 9: azi12=-92.388887981699639, s12=12782581.0676841792, S12=-63760642939072.492 (0) 

475# Inverse3: Distance3Tuple(distance=12782581.067684, initial=267.611112, final=267.611112) 

476 

477# Position: True GDict(S12=44095641862956.148438, azi12=51, lat1=40.6, lat2=71.6889, lon1=-73.8, lon2=0.25552, s12=5500000.0) 

478 

479 

480# % python3 -m pygeodesy.rhumb.solve 

481 

482# version: /opt/local/bin/RhumbSolve: GeographicLib version 2.2 

483 

484# Direct: GDict(azi12=51, lat1=40.6, lat2=71.6889, lon1=-73.8, lon2=0.25552, s12=5500000.0, S12=44095641862956.109375) 

485# Direct3: Destination3Tuple(lat=71.6889, lon=0.25552, final=51.0) 

486 

487# Inverse: GDict(a12=51.929543, azi12=77.76839, lat1=40.6, lat2=51.6, lon1=-73.8, lon2=-0.5, s12=5771083.383328, S12=37395209100030.390625) 

488# Inverse1: 51.92954250756191 

489# Inverse3: Distance3Tuple(distance=5771083.383328, initial=77.76839, final=77.76839) 

490 

491# Inverse: GDict(a12=115.02062, azi12=-92.388888, lat1=40.6, lat2=35.8, lon1=-73.8, lon2=140.3, s12=12782581.067684, S12=-63760642939072.5) 

492# Inverse1: 115.02061966879249 

493# Inverse3: Distance3Tuple(distance=12782581.067684, initial=267.611112, final=267.611112) 

494 

495# Position: True GDict(azi12=51, lat1=40.6, lat2=71.6889, lon1=-73.8, lon2=0.25552, s12=5500000.0, S12=44095641862956.109375) 

496 

497 

498# % python3 -m pygeodesy.rhumb.solve --verbose 

499 

500# RhumbSolve 'Test' 1: /opt/local/bin/RhumbSolve --version (invoke) 

501# RhumbSolve 'Test' 1: /opt/local/bin/RhumbSolve: GeographicLib version 2.2 (0) 

502# version: /opt/local/bin/RhumbSolve: GeographicLib version 2.2 

503# RhumbSolve 'Test' 2: /opt/local/bin/RhumbSolve -p 10 \ 40.600000000000001 -73.799999999999997 51.0 5500000.0 (Direct) 

504# RhumbSolve 'Test' 2: lat2=71.688899882813018, lon2=0.255519824423402, S12=44095641862956.109 (0) 

505 

506# Direct: GDict(azi12=51, lat1=40.6, lat2=71.6889, lon1=-73.8, lon2=0.25552, s12=5500000.0, S12=44095641862956.109375) 

507# RhumbSolve 'Test' 3: /opt/local/bin/RhumbSolve -p 10 \ 40.600000000000001 -73.799999999999997 51.0 5500000.0 (Direct3) 

508# RhumbSolve 'Test' 3: lat2=71.688899882813018, lon2=0.255519824423402, S12=44095641862956.109 (0) 

509# Direct3: Destination3Tuple(lat=71.6889, lon=0.25552, final=51.0) 

510# RhumbSolve 'Test' 4: /opt/local/bin/RhumbSolve -p 10 -i \ 40.600000000000001 -73.799999999999997 51.600000000000001 -0.5 (Inverse) 

511# RhumbSolve 'Test' 4: azi12=77.768389710255661, s12=5771083.383328028, S12=37395209100030.391 (0) 

512 

513# Inverse: GDict(a12=51.929543, azi12=77.76839, lat1=40.6, lat2=51.6, lon1=-73.8, lon2=-0.5, s12=5771083.383328, S12=37395209100030.390625) 

514# RhumbSolve 'Test' 5: /opt/local/bin/RhumbSolve -p 10 -i \ 40.600000000000001 -73.799999999999997 51.600000000000001 -0.5 (Inverse1) 

515# RhumbSolve 'Test' 5: azi12=77.768389710255661, s12=5771083.383328028, S12=37395209100030.391 (0) 

516# Inverse1: 51.92954250756191 

517# RhumbSolve 'Test' 6: /opt/local/bin/RhumbSolve -p 10 -i \ 40.600000000000001 -73.799999999999997 51.600000000000001 -0.5 (Inverse3) 

518# RhumbSolve 'Test' 6: azi12=77.768389710255661, s12=5771083.383328028, S12=37395209100030.391 (0) 

519# Inverse3: Distance3Tuple(distance=5771083.383328, initial=77.76839, final=77.76839) 

520# RhumbSolve 'Test' 7: /opt/local/bin/RhumbSolve -p 10 -i \ 40.600000000000001 -73.799999999999997 35.799999999999997 140.300000000000011 (Inverse) 

521# RhumbSolve 'Test' 7: azi12=-92.388887981699654, s12=12782581.0676841699, S12=-63760642939072.5 (0) 

522 

523# Inverse: GDict(a12=115.02062, azi12=-92.388888, lat1=40.6, lat2=35.8, lon1=-73.8, lon2=140.3, s12=12782581.067684, S12=-63760642939072.5) 

524# RhumbSolve 'Test' 8: /opt/local/bin/RhumbSolve -p 10 -i \ 40.600000000000001 -73.799999999999997 35.799999999999997 140.300000000000011 (Inverse1) 

525# RhumbSolve 'Test' 8: azi12=-92.388887981699654, s12=12782581.0676841699, S12=-63760642939072.5 (0) 

526# Inverse1: 115.02061966879249 

527# RhumbSolve 'Test' 9: /opt/local/bin/RhumbSolve -p 10 -i \ 40.600000000000001 -73.799999999999997 35.799999999999997 140.300000000000011 (Inverse3) 

528# RhumbSolve 'Test' 9: azi12=-92.388887981699654, s12=12782581.0676841699, S12=-63760642939072.5 (0) 

529# Inverse3: Distance3Tuple(distance=12782581.067684, initial=267.611112, final=267.611112) 

530 

531# Position: True GDict(azi12=51, lat1=40.6, lat2=71.6889, lon1=-73.8, lon2=0.25552, s12=5500000.0, S12=44095641862956.109375) 

532 

533 

534# % python3 -m pygeodesy.rhumb.solve 60 0 30 0 45 1e6 

535 

536# version: /opt/local/bin/RhumbSolve: GeographicLib version 2.2 

537# s23 lat3 lon3 cos() 

538# 1 1000000.000 36.37559999, 7.58982303, -5.83098638e-01 

539# 2 4532573.097 58.84251798, 41.57078946, 4.05349594e-01 

540# 3 2233216.895 44.22871762, 17.86660260, -2.91432608e-01 

541# 4 3168401.173 50.17678842, 26.60741388, 3.00555188e-02 

542# 5 3082690.347 49.63189746, 25.76374255, -1.49446251e-04 

543# 6 3083112.629 49.63458216, 25.76787599, -2.59865190e-09 

544# 7 3083112.636 49.63458221, 25.76787606, 4.96052409e-16 

545# 8 3083112.636 49.63458221, 25.76787606, -4.96052409e-16 

546# 9 3083112.636 49.63458221, 25.76787606, 4.96052409e-16 

547# 10 3083112.636 49.63458221, 25.76787606, -4.96052409e-16 

548# 11 3083112.636 49.63458221, 25.76787606, 4.96052409e-16 

549# 12 3083112.636 49.63458221, 25.76787606, -4.96052409e-16 

550# 13 3083112.636 49.63458221, 25.76787606, 4.96052409e-16 

551# 14 3083112.636 49.63458221, 25.76787606, -4.96052409e-16 

552# 15 3083112.636 49.63458221, 25.76787606, 4.96052409e-16 

553 

554 

555# **) MIT License 

556# 

557# Copyright (C) 2022-2024 -- mrJean1 at Gmail -- All Rights Reserved. 

558# 

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

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

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

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

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

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

565# 

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

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

568# 

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

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

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

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

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

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

575# OTHER DEALINGS IN THE SOFTWARE.