1

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

3

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

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

6 as 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'''

11# from pygeodesy.basics import _xinstanceof # from .karney

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

13 from pygeodesy.errors import RhumbError # PYCHOK used!

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

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

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

17 _PYGEODESY_RHUMBSOLVE_

18 from pygeodesy.namedTuples import Destination3Tuple, Distance3Tuple

19 from pygeodesy.props import deprecated_method, Property, Property_RO

20 from pygeodesy.solveBase import _SolveBase, _SolveLineBase

21 from pygeodesy.utily import _unrollon, _Wrap, wrap360

22

23__all__ = _ALL_LAZY.rhumbsolve

24__version__ = '23.09.18'

25

26

27 class _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

83 class 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=NN):

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 name (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.rhumb*.RhumbArea(self, polyline=polyline,

107# name=name or self.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=NN):

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

154 @return: A L{RhumbLineSolve} instance.

155

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

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

158 the limit C{ε → 0+}.

159

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

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

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

163 '''

164 return RhumbLineSolve(self, lat1, lon1, azi1, caps=caps, name=name or self.name)

165

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

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

168 '''

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

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

171 r.update(d)

172 return r

173

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

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

176 I{without} C{_SALPs_CALPs_}.

177 '''

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

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

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

181 r.update(i)

182 return r

183

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

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

186 '''

187 if wrap:

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

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

190

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

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

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

194

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

196 '''

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

198 z = self._azimuth_reverse(r.azi12)

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

200 iteration=r._iteration)

201

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

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

204 '''

205 if wrap:

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

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

208

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

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

211 rhumb problem.

212

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

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

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

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

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

218

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

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

221

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

223 rhumb line.

224 '''

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

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

227 name=name or self.name)

228

229 Line = DirectLine

230

231

232 class RhumbLineSolve(_RhumbSolveBase, _SolveLineBase):

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

234 the C{Exact} version of class L{pygeodesy.RhumbLine}.

235

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

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

238

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

240 executable for I{every} method call.

241 '''

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

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

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

245

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

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

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

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

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

251 the capabilities the L{RhumbLineSolve} instance should

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

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

254 reflected in this L{RhumbLineSolve} instance.

255

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

257

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

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

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

261

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

263 '''

264 _xinstanceof(RhumbSolve, rhumb=rhumb)

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

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

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

268 try:

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

270 except RhumbError:

271 pass

272

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

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

275#

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

277# second point (C{degrees}).

278#

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

280# azi12, a12, s12, S12}.

281# '''

282# s = a12 * self._mpd

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

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

285# r.updated(a)

286# return r

287

288 @Property_RO

289 def azi12(self):

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

291 '''

292 return self._lla1.azi12

293

294 azi1 = azi12 # like GeodesicLineSolve

295

296 @Property_RO

297 def azi12_sincos2(self): # PYCHOK no cover

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

299 '''

300 return _sincos2d(self.azi12)

301

302 azi1_sincos2 = azi12_sincos2

303

304# @Property_RO

305# def _cmdArc(self):

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

307# '''

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

309

310 def Position(self, s12, **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 L{GDict} with 7 items C{lat1, lon1, lat2, lon2,

316 azi12, s12, S12}.

317 '''

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

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

320 r.update(d)

321 return r

322

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

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

325

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

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

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

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

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

331

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

333 '''

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

335 RhumbSolve=self.RhumbSolve, **prec_sep)

336

337

338 class RhumbSolve7Tuple(Rhumb8Tuple):

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

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

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

342 the rhumb line between both points.

343 '''

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

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

346 _Units_ = Rhumb8Tuple._Units_[:7]

347

348 @deprecated_method

349 def _to7Tuple(self): # PYCHOK no cover

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

351 '''

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

353

354

355__all__ += _ALL_DOCS(_RhumbSolveBase)

356

357 if __name__ == '__main__':

358

359 from pygeodesy.lazily import printf

360 from sys import argv

361

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

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

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

365 from pygeodesy.constants import EPS4 as _TOL

366 from pygeodesy.karney import _diff182

367

368 E = rS.ellipsoid

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

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

371

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

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

374 for i in range(1, 16):

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

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

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

378 s, c = _sincos2d(d)

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

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

381 s2, c2 = _sincos2d(r.lat2)

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

383 if c2 and r.m12:

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

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

386 if abs(t) < _TOL:

387 break

388 s23 += t

389 else:

390 break

391

392 rS = RhumbSolve(name='Test')

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

394

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

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

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

398

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

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

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

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

403 exit()

404

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

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

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

408

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

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

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

412

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

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

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

416

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

418 p = rlS.Position(5.5e6)

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

420# p = rlS.ArcPosition(49.475527)

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

422

423# % python3 -m pygeodesy.rhumbsolve

424

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

426#

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

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

429#

430# 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)

431# Inverse1: 51.92954250756195

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

433#

434# 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)

435# Inverse1: 115.02061966879258

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

437#

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

439

440

441# % python3 -m pygeodesy.rhumbsolve --verbose

442

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

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

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

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

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

448

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

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

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

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

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

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

455

456# 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)

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

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

459# Inverse1: 51.92954250756195

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

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

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

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

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

465

466# 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)

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

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

469# Inverse1: 115.02061966879258

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

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

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

473

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

475

476

477# % python3 -m pygeodesy.rhumbsolve

478

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

480

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

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

483

484# 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)

485# Inverse1: 51.92954250756191

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

487

488# 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)

489# Inverse1: 115.02061966879249

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

491

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

493

494

495# % python3 -m pygeodesy.rhumbsolve --verbose

496

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

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

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

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

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

502

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

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

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

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

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

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

509

510# 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)

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

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

513# Inverse1: 51.92954250756191

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

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

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

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

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

519

520# 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)

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

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

523# Inverse1: 115.02061966879249

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

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

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

527

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

529

530

531# % python3 -m pygeodesy.rhumbsolve 60 0 30 0 45 1e6

532

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

534# s23 lat3 lon3 cos()

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

550

551# **) MIT License

552#

553# Copyright (C) 2022-2023 -- mrJean1 at Gmail -- All Rights Reserved.

554#

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

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

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

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

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

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

561#

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

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

564#

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

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

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

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

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

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

571# OTHER DEALINGS IN THE SOFTWARE.