Coverage for pygeodesy/rhumbsolve.py: 89%
103 statements
« prev ^ index » next coverage.py v7.2.2, created at 2023-08-07 07:28 -0400
« prev ^ index » next coverage.py v7.2.2, created at 2023-08-07 07:28 -0400
2# -*- coding: utf-8 -*-
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 fom I{either GeographicLib 2.0 or 2.2+}.
8Set env variable C{PYGEODESY_RHUMBSOLVE} to the (fully qualified) path
9of the C{RhumbSolve} executable.
10'''
11# from pygeodesy.basics import _xinstanceof # from .karney
12from pygeodesy.constants import _0_0, _180_0, _N_180_0, _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, _xinstanceof
16from pygeodesy.lazily import _ALL_DOCS, _ALL_LAZY, _ALL_MODS as _MODS, _getenv
17from pygeodesy.namedTuples import Destination3Tuple, Distance3Tuple
18from pygeodesy.props import deprecated_method, Property, Property_RO
19from pygeodesy.solveBase import _SolveBase, _SolveLineBase
20from pygeodesy.utily import _unrollon, _Wrap, wrap360
22__all__ = _ALL_LAZY.rhumbsolve
23__version__ = '23.08.04'
25_PYGEODESY_RHUMBSOLVE_ = 'PYGEODESY_RHUMBSOLVE' # PYCHOK used!
28class _RhumbSolveBase(_SolveBase):
29 '''(INTERNAL) Base class for L{RhumbSolve} and L{RhumbLineSolve}.
30 '''
31 _Error = RhumbError
32 _Names_Direct = _lat2_, _lon2_, _S12_
33 _Names_Inverse = _azi12_, _s12_, _S12_
34 _Solve_name = 'RhumbSolve'
35 _Solve_path = _getenv(_PYGEODESY_RHUMBSOLVE_, _PYGEODESY_RHUMBSOLVE_)
37 @Property_RO
38 def _cmdBasic(self):
39 '''(INTERNAL) Get the basic C{RhumbSolve} cmd (C{tuple}).
40 '''
41 return (self.RhumbSolve,) + self._e_option \
42 + self._p_option \
43 + self._s_option
45 @Property
46 def RhumbSolve(self):
47 '''Get the U{RhumbSolve<https://GeographicLib.SourceForge.io/C++/doc/RhumbSolve.1.html>}
48 executable (C{filename}).
49 '''
50 return self._Solve_path
52 @RhumbSolve.setter # PYCHOK setter!
53 def RhumbSolve(self, path):
54 '''Set the U{RhumbSolve<https://GeographicLib.SourceForge.io/C++/doc/RhumbSolve.1.html>}
55 executable (C{filename}), the (fully qualified) path to the C{RhumbSolve} executable.
57 @raise RhumbError: Invalid B{C{path}}, B{C{path}} doesn't exist or isn't
58 the C{RhumbSolve} executable.
59 '''
60 self._setSolve(path)
62 @Property_RO
63 def _s_option(self): # == not -E for GeodSolve
64 return () if self.Exact else ('-s',)
66 def toStr(self, **prec_sep): # PYCHOK signature
67 '''Return this C{RhumbSolve} as string.
69 @kwarg prec_sep: Keyword argumens C{B{prec}=6} and C{B{sep}=', '}
70 for the C{float} C{prec}ision, number of decimal digits
71 (0..9) and the C{sep}arator string to join. Trailing
72 zero decimals are stripped for B{C{prec}} values of
73 1 and above, but kept for negative B{C{prec}} values.
75 @return: RhumbSolve items (C{str}).
76 '''
77 return self._toStr(RhumbSolve=self.RhumbSolve, **prec_sep)
79# @Property_RO
80# def _u_option(self):
81# return '-u' if self.unroll else ()
84class RhumbSolve(_RhumbSolveBase):
85 '''Wrapper to invoke I{Karney}'s U{RhumbSolve<https://GeographicLib.SourceForge.io/C++/doc/RhumbSolve.1.html>}
86 like a class, similar to L{pygeodesy.Rhumb} and L{pygeodesy.RhumbAux}.
88 @note: Use property C{RhumbSolve} or env variable C{PYGEODESY_RHUMBSOLVE} to specify the (fully
89 qualified) path to the C{RhumbSolve} executable.
91 @note: This C{rhumb} is intended I{for testing purposes only}, it invokes the C{RhumbSolve}
92 executable for I{every} method call.
93 '''
94# def Area(self, polyline=False, name=NN):
95# '''Set up a L{RhumbArea} to compute area and
96# perimeter of a polygon.
97#
98# @kwarg polyline: If C{True} perimeter only, otherwise
99# area and perimeter (C{bool}).
100# @kwarg name: Optional name (C{str}).
101#
102# @return: A L{RhumbArea} instance.
103#
104# @note: The B{C{debug}} setting is passed as C{verbose}
105# to the returned L{RhumbAreaExact} instance.
106# '''
107# rA = _MODS.rhumb*.RhumbArea(self, polyline=polyline,
108# name=name or self.name)
109# if self.verbose or self.debug: # PYCHOK no cover
110# rA.verbose = True
111# return rA
113# Polygon = Area # for C{geographiclib} compatibility
115 def _azimuth_reverse(self, azimuth):
116 '''(INTERNAL) Reverse final azimuth C{azimuth}.
117 '''
118 z = _norm180(float(azimuth))
119 if self.reverse2: # like .utils.atan2d
120 z += _180_0 if z < 0 else _N_180_0
121 return z
123 def _Direct(self, ll1, azi12, s12, **outmask):
124 '''(INTERNAL) Short-cut version, see .latlonBase.
125 '''
126 return self.Direct(ll1.lat, ll1.lon, azi12, s12, **outmask)
128 def Direct3(self, lat1, lon1, azi1, s12): # PYCHOK outmask
129 '''Return the destination lat, lon and reverse azimuth
130 (final bearing) in C{degrees}.
132 @return: L{Destination3Tuple}C{(lat, lon, final)}.
133 '''
134 r = self._GDictDirect(lat1, lon1, azi1, False, s12, floats=False)
135 z = self._azimuth_reverse(r.azi12)
136 return Destination3Tuple(float(r.lat2), float(r.lon2), wrap360(z),
137 iteration=r._iteration)
139 def _DirectLine(self, ll1, azi12, **name_caps):
140 '''(INTERNAL) Short-cut version, see .latlonBase.
141 '''
142 return self.DirectLine(ll1.lat, ll1.lon, azi12, **name_caps)
144 def DirectLine(self, lat1, lon1, azi1, caps=Caps.STANDARD, name=NN):
145 '''Set up a L{RhumbLineSolve} in terms of the I{direct} rhumb
146 problem to compute several points on a single rhumb line.
148 @arg lat1: Latitude of the first point (C{degrees}).
149 @arg lon1: Longitude of the first point (C{degrees}).
150 @arg azi1: Azimuth at the first point (compass C{degrees}).
151 @kwarg caps: Bit-or'ed combination of L{Caps} values specifying
152 the capabilities the L{RhumbLineSolve} instance
153 should possess, always C{Caps.ALL}.
155 @return: A L{RhumbLineSolve} instance.
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+}.
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=name or self.name)
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=s12_a12 / self.ellipsoid._L_90
172 r.update(d)
173 return r
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 = float(i.s12) / self.ellipsoid._L_90 # for .Inverse1
181 r = GDict(lat1=lat1, lon1=lon1, lat2=lat2, lon2=lon2, a12=a)
182 r.update(i)
183 return r
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)
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}.
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)
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)
210 def InverseLine(self, lat1, lon1, lat2, lon2, caps=Caps.STANDARD, name=NN):
211 '''Define a L{RhumbLineSolve} in terms of the I{inverse}
212 rhumb problem.
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}}.
220 @return: A L{RhumbLineSolve} instance and invoke its method
221 L{RhumbLine.Position} to compute each point.
223 @note: Updates to this rhumb are reflected in the returned
224 rhumb line.
225 '''
226 r = self.Inverse(lat1, lon1, lat2, lon2) # outmask=Caps.AZIMUTH
227 return RhumbLineSolve(self, lat1, lon1, r.azi12, caps=caps,
228 name=name or self.name)
230 Line = DirectLine
233class RhumbLineSolve(_RhumbSolveBase, _SolveLineBase):
234 '''Wrapper to invoke I{Karney}'s U{RhumbSolve<https://GeographicLib.SourceForge.io/C++/doc/RhumbSolve.1.html>},
235 the C{Exact} version of class L{pygeodesy.RhumbLine}.
237 @note: Use property C{RhumbSolve} or env variable C{PYGEODESY_RHUMBSOLVE} to specify the (fully
238 qualified) path to the C{RhumbSolve} executable.
240 @note: This C{rhumb line} is intended I{for testing purposes only}, it invokes the C{RhumbSolve}
241 executable for I{every} method call.
242 '''
243 def __init__(self, rhumb, lat1, lon1, azi12, caps=Caps.STANDARD, name=NN):
244 '''New L{RhumbLineSolve} instance, allowing points to be found along
245 a rhumb starting at C{(B{lat1}, B{lon1})} with azimuth B{C{azi12}}.
247 @arg rhumb: The rhumb to use (L{RhumbSolve}).
248 @arg lat1: Latitude of the first point (C{degrees90}).
249 @arg lon1: Longitude of the first point (C{degrees180}).
250 @arg azi12: Azimuth of the rhumb line (compass C{degrees180}).
251 @kwarg caps: Bit-or'ed combination of L{Caps} values specifying
252 the capabilities the L{RhumbLineSolve} instance should
253 possess, always C{Caps.ALL}. Use C{Caps.LINE_OFF}
254 if updates to the B{C{rhumb}} should I{not} be
255 reflected in this L{RhumbLineSolve} instance.
257 @kwarg name: Optional name (C{str}).
259 @raise RhumbError: Invalid path for C{RhumbSolve} executable or
260 isn't the C{RhumbSolve} executable, see
261 property C{B{rhumb}.RhumbSolve}.
263 @raise TypeError: Invalid B{C{rhumb}}.
264 '''
265 _xinstanceof(RhumbSolve, rhumb=rhumb)
266 if (caps & Caps.LINE_OFF): # copy to avoid updates
267 rhumb = rhumb.copy(deep=False, name=NN(_UNDER_, rhumb.name))
268 _SolveLineBase.__init__(self, rhumb, lat1, lon1, caps, name, azi12=azi12)
269 try:
270 self.RhumbSolve = rhumb.RhumbSolve # rhumb or copy of rhumb
271 except RhumbError:
272 pass
274# def ArcPosition(self, a12, *unused):
275# '''Find the position on the line given B{C{a12}}.
276#
277# @arg a12: Spherical arc length from the first point to the
278# second point (C{degrees}).
279#
280# @return: A C{dict} with 8 items C{lat1, lon1, lat2, lon2,
281# azi12, a12, s12, S12}.
282# '''
283# s = a12 * self.ellipsoid._L_90
284# a = self._GDictInvoke(self._cmdArc, True, self._Names_Direct, s)
285# r = GDict(a12=a12, s12=s, **self._lla1)
286# r.updated(a)
287# return r
289 @Property_RO
290 def azi12(self):
291 '''Get this rhumb line's azimuth (compass C{degrees}).
292 '''
293 return self._lla1.azi12
295 azi1 = azi12 # like GeodesicLineSolve
297 @Property_RO
298 def azi12_sincos2(self): # PYCHOK no cover
299 '''Get the sine and cosine of this rhumb line's azimuth (2-tuple C{(sin, cos)}).
300 '''
301 return _sincos2d(self.azi12)
303 azi1_sincos2 = azi12_sincos2
305# @Property_RO
306# def _cmdArc(self):
307# '''(INTERNAL) Get the C{RhumbSolve} I{-a -L} cmd (C{tuple}).
308# '''
309# return self._cmdDistance + ('-a',)
311 def Position(self, s12, **unused):
312 '''Find the position on the line given B{C{s12}}.
314 @arg s12: Distance from the first point to the second (C{meter}).
316 @return: A L{GDict} with 7 items C{lat1, lon1, lat2, lon2,
317 azi12, s12, S12}.
318 '''
319 d = self._GDictInvoke(self._cmdDistance, True, self._Names_Direct, s12)
320 r = GDict(s12=s12, **self._lla1) # a12=s12 / self.ellipsoid._L_90
321 r.update(d)
322 return r
324 def toStr(self, **prec_sep): # PYCHOK signature
325 '''Return this C{RhumbLineSolve} as string.
327 @kwarg prec_sep: Keyword argumens C{B{prec}=6} and C{B{sep}=', '}
328 for the C{float} C{prec}ision, number of decimal digits
329 (0..9) and the C{sep}arator string to join. Trailing
330 zero decimals are stripped for B{C{prec}} values of
331 1 and above, but kept for negative B{C{prec}} values.
333 @return: RhumbLineSolve items (C{str}).
334 '''
335 return _SolveLineBase._toStr(self, azi12=self.azi12, rhumb=self._solve,
336 RhumbSolve=self.RhumbSolve, **prec_sep)
339class RhumbSolve7Tuple(Rhumb8Tuple):
340 '''7-Tuple C{(lat1, lon1, lat2, lon2, azi12, s12, S12)} with lat- C{lat1},
341 C{lat2} and longitudes C{lon1}, C{lon2} of both points, the azimuth of the
342 rhumb line C{azi12}, the distance C{s12} and the area C{S12} under the
343 rhumb line between both points.
344 '''
345 assert Rhumb8Tuple._Names_.index(_a12_) == 7
346 _Names_ = Rhumb8Tuple._Names_[:7] # drop a12
347 _Units_ = Rhumb8Tuple._Units_[:7]
349 @deprecated_method
350 def _to7Tuple(self): # PYCHOK no cover
351 '''DEPRECATED, I{don't use!}
352 '''
353 return _MODS.deprecated.Rhumb7Tuple(self[:7])
356__all__ += _ALL_DOCS(_RhumbSolveBase)
358if __name__ == '__main__':
360 from pygeodesy.lazily import printf
361 from sys import argv
363 def rhumb_intercept(rS, lat1, lon1, lat2, lon2, azi2, s23):
364 # using RhumbSolve and GeodesicExact for I{Karney}'s C++ U{rhumb-intercept.cpp
365 # <https://SourceForge.net/p/geographiclib/discussion/1026620/thread/2ddc295e/>
366 from pygeodesy.constants import EPS4 as _TOL
367 from pygeodesy.karney import _diff182
369 E = rS.ellipsoid
370 gX = E.geodesicx # == GeodesicExact(E)
371 m = gX.STANDARD | gX.REDUCEDLENGTH | gX.GEODESICSCALE
373 rlS = rS.Line(lat2, lon2, azi2)
374 sa, _ = rlS.azi12_sincos2 # aka _salp, _calp
375 for i in range(1, 16):
376 p = rlS.Position(s23) # outmask=gX.LATITUDE_LONGITUDE
377 r = gX.Inverse(lat1, lon1, p.lat2, p.lon2, outmask=m)
378 d, _ = _diff182(azi2, r.azi2, K_2_0=True)
379 s, c = _sincos2d(d)
380 printf('%2d %.3f %.8f, %.8f, %.8e',
381 i, s23, r.lat2, r.lon2, c)
382 s2, c2 = _sincos2d(r.lat2)
383 c2 *= E.rocTransverse(r.lat2)
384 if c2 and r.m12:
385 s *= (s2 * sa) / c2 - s * r.M21 / r.m12
386 t = (c / s) if s else _0_0
387 if abs(t) < _TOL:
388 break
389 s23 += t
390 else:
391 break
393 rS = RhumbSolve(name='Test')
394 rS.verbose = '--verbose' in argv # or '-v' in argv
396 if rS.RhumbSolve in (_PYGEODESY_RHUMBSOLVE_, None): # not set
397 rS.RhumbSolve = '/opt/local/bin/RhumbSolve' # '/opt/local/Cellar/geographiclib/2.2/bin/RhumbSolve' # HomeBrew
398 printf('version: %s', rS.version)
400 if len(argv) > 6: # 60 0 30 0 45 1e6
401 t = (14, 's23'), (7, 'lat3'), (11, 'lon3'), (13, 'cos()')
402 printf(' '.join('%*s' % _ for _ in t))
403 rhumb_intercept(rS, *map(float, argv[-6:]))
404 exit()
406 r = rS.Direct(40.6, -73.8, 51, 5.5e6)
407 printf('Direct: %r', r, nl=1)
408 printf('Direct3: %r', rS.Direct3(40.6, -73.8, 51, 5.5e6))
410 printf('Inverse: %r', rS.Inverse( 40.6, -73.8, 51.6, -0.5), nl=1)
411 printf('Inverse1: %r', rS.Inverse1(40.6, -73.8, 51.6, -0.5))
412 printf('Inverse3: %r', rS.Inverse3(40.6, -73.8, 51.6, -0.5))
414 printf('Inverse: %r', rS.Inverse( 40.6, -73.8, 35.8, 140.3), nl=1)
415 printf('Inverse1: %r', rS.Inverse1(40.6, -73.8, 35.8, 140.3))
416 printf('Inverse3: %r', rS.Inverse3(40.6, -73.8, 35.8, 140.3))
418 rlS = RhumbLineSolve(rS, 40.6, -73.8, 51, name='LineTest')
419 p = rlS.Position(5.5e6)
420 printf('Position: %s %r', p == r, p, nl=1)
421# p = rlS.ArcPosition(49.475527)
422# printf('ArcPosition: %s %r', p == r, p)
424# % python3 -m pygeodesy.rhumbsolve
426# version: /opt/local/bin/RhumbSolve: GeographicLib version 1.51
427#
428# Direct: GDict(S12=44095641862956.148438, azi12=51, lat1=40.6, lat2=71.6889, lon1=-73.8, lon2=0.25552, s12=5500000.0)
429# Direct3: Destination3Tuple(lat=71.6889, lon=0.25552, final=51.0)
430#
431# 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)
432# Inverse1: 51.92954250756195
433# Inverse3: Distance3Tuple(distance=5771083.383328, initial=77.76839, final=77.76839)
434#
435# 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)
436# Inverse1: 115.02061966879258
437# Inverse3: Distance3Tuple(distance=12782581.067684, initial=267.611112, final=267.611112)
438#
439# Position: True GDict(S12=44095641862956.148438, azi12=51, lat1=40.6, lat2=71.6889, lon1=-73.8, lon2=0.25552, s12=5500000.0)
442# % python3 -m pygeodesy.rhumbsolve --verbose
444# RhumbSolve 'Test' 1: /opt/local/bin/RhumbSolve --version (invoke)
445# RhumbSolve 'Test' 1: /opt/local/bin/RhumbSolve: GeographicLib version 1.51 (0)
446# version: /opt/local/bin/RhumbSolve: GeographicLib version 1.51
447# RhumbSolve 'Test' 2: /opt/local/bin/RhumbSolve -p 10 \ 40.600000000000001 -73.799999999999997 51.0 5500000.0 (Direct)
448# RhumbSolve 'Test' 2: lat2=71.688899882813047, lon2=0.255519824423445, S12=44095641862956.148 (0)
450# Direct: GDict(S12=44095641862956.148438, azi12=51, lat1=40.6, lat2=71.6889, lon1=-73.8, lon2=0.25552, s12=5500000.0)
451# RhumbSolve 'Test' 3: /opt/local/bin/RhumbSolve -p 10 \ 40.600000000000001 -73.799999999999997 51.0 5500000.0 (Direct3)
452# RhumbSolve 'Test' 3: lat2=71.688899882813047, lon2=0.255519824423445, S12=44095641862956.148 (0)
453# Direct3: Destination3Tuple(lat=71.6889, lon=0.25552, final=51.0)
454# RhumbSolve 'Test' 4: /opt/local/bin/RhumbSolve -p 10 -i \ 40.600000000000001 -73.799999999999997 51.600000000000001 -0.5 (Inverse)
455# RhumbSolve 'Test' 4: azi12=77.768389710255661, s12=5771083.3833280317, S12=37395209100030.367 (0)
457# 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)
458# RhumbSolve 'Test' 5: /opt/local/bin/RhumbSolve -p 10 -i \ 40.600000000000001 -73.799999999999997 51.600000000000001 -0.5 (Inverse1)
459# RhumbSolve 'Test' 5: azi12=77.768389710255661, s12=5771083.3833280317, S12=37395209100030.367 (0)
460# Inverse1: 51.92954250756195
461# RhumbSolve 'Test' 6: /opt/local/bin/RhumbSolve -p 10 -i \ 40.600000000000001 -73.799999999999997 51.600000000000001 -0.5 (Inverse3)
462# RhumbSolve 'Test' 6: azi12=77.768389710255661, s12=5771083.3833280317, S12=37395209100030.367 (0)
463# Inverse3: Distance3Tuple(distance=5771083.383328, initial=77.76839, final=77.76839)
464# RhumbSolve 'Test' 7: /opt/local/bin/RhumbSolve -p 10 -i \ 40.600000000000001 -73.799999999999997 35.799999999999997 140.300000000000011 (Inverse)
465# RhumbSolve 'Test' 7: azi12=-92.388887981699639, s12=12782581.0676841792, S12=-63760642939072.492 (0)
467# 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)
468# RhumbSolve 'Test' 8: /opt/local/bin/RhumbSolve -p 10 -i \ 40.600000000000001 -73.799999999999997 35.799999999999997 140.300000000000011 (Inverse1)
469# RhumbSolve 'Test' 8: azi12=-92.388887981699639, s12=12782581.0676841792, S12=-63760642939072.492 (0)
470# Inverse1: 115.02061966879258
471# RhumbSolve 'Test' 9: /opt/local/bin/RhumbSolve -p 10 -i \ 40.600000000000001 -73.799999999999997 35.799999999999997 140.300000000000011 (Inverse3)
472# RhumbSolve 'Test' 9: azi12=-92.388887981699639, s12=12782581.0676841792, S12=-63760642939072.492 (0)
473# Inverse3: Distance3Tuple(distance=12782581.067684, initial=267.611112, final=267.611112)
475# Position: True GDict(S12=44095641862956.148438, azi12=51, lat1=40.6, lat2=71.6889, lon1=-73.8, lon2=0.25552, s12=5500000.0)
478# % python3 -m pygeodesy.rhumbsolve
480# version: /opt/local/bin/RhumbSolve: GeographicLib version 2.2
482# Direct: GDict(azi12=51, lat1=40.6, lat2=71.6889, lon1=-73.8, lon2=0.25552, s12=5500000.0, S12=44095641862956.109375)
483# Direct3: Destination3Tuple(lat=71.6889, lon=0.25552, final=51.0)
485# 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)
486# Inverse1: 51.92954250756191
487# Inverse3: Distance3Tuple(distance=5771083.383328, initial=77.76839, final=77.76839)
489# 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)
490# Inverse1: 115.02061966879249
491# Inverse3: Distance3Tuple(distance=12782581.067684, initial=267.611112, final=267.611112)
493# Position: True GDict(azi12=51, lat1=40.6, lat2=71.6889, lon1=-73.8, lon2=0.25552, s12=5500000.0, S12=44095641862956.109375)
496# % python3 -m pygeodesy.rhumbsolve --verbose
498# RhumbSolve 'Test' 1: /opt/local/bin/RhumbSolve --version (invoke)
499# RhumbSolve 'Test' 1: /opt/local/bin/RhumbSolve: GeographicLib version 2.2 (0)
500# version: /opt/local/bin/RhumbSolve: GeographicLib version 2.2
501# RhumbSolve 'Test' 2: /opt/local/bin/RhumbSolve -p 10 \ 40.600000000000001 -73.799999999999997 51.0 5500000.0 (Direct)
502# RhumbSolve 'Test' 2: lat2=71.688899882813018, lon2=0.255519824423402, S12=44095641862956.109 (0)
504# Direct: GDict(azi12=51, lat1=40.6, lat2=71.6889, lon1=-73.8, lon2=0.25552, s12=5500000.0, S12=44095641862956.109375)
505# RhumbSolve 'Test' 3: /opt/local/bin/RhumbSolve -p 10 \ 40.600000000000001 -73.799999999999997 51.0 5500000.0 (Direct3)
506# RhumbSolve 'Test' 3: lat2=71.688899882813018, lon2=0.255519824423402, S12=44095641862956.109 (0)
507# Direct3: Destination3Tuple(lat=71.6889, lon=0.25552, final=51.0)
508# RhumbSolve 'Test' 4: /opt/local/bin/RhumbSolve -p 10 -i \ 40.600000000000001 -73.799999999999997 51.600000000000001 -0.5 (Inverse)
509# RhumbSolve 'Test' 4: azi12=77.768389710255661, s12=5771083.383328028, S12=37395209100030.391 (0)
511# 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)
512# RhumbSolve 'Test' 5: /opt/local/bin/RhumbSolve -p 10 -i \ 40.600000000000001 -73.799999999999997 51.600000000000001 -0.5 (Inverse1)
513# RhumbSolve 'Test' 5: azi12=77.768389710255661, s12=5771083.383328028, S12=37395209100030.391 (0)
514# Inverse1: 51.92954250756191
515# RhumbSolve 'Test' 6: /opt/local/bin/RhumbSolve -p 10 -i \ 40.600000000000001 -73.799999999999997 51.600000000000001 -0.5 (Inverse3)
516# RhumbSolve 'Test' 6: azi12=77.768389710255661, s12=5771083.383328028, S12=37395209100030.391 (0)
517# Inverse3: Distance3Tuple(distance=5771083.383328, initial=77.76839, final=77.76839)
518# RhumbSolve 'Test' 7: /opt/local/bin/RhumbSolve -p 10 -i \ 40.600000000000001 -73.799999999999997 35.799999999999997 140.300000000000011 (Inverse)
519# RhumbSolve 'Test' 7: azi12=-92.388887981699654, s12=12782581.0676841699, S12=-63760642939072.5 (0)
521# 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)
522# RhumbSolve 'Test' 8: /opt/local/bin/RhumbSolve -p 10 -i \ 40.600000000000001 -73.799999999999997 35.799999999999997 140.300000000000011 (Inverse1)
523# RhumbSolve 'Test' 8: azi12=-92.388887981699654, s12=12782581.0676841699, S12=-63760642939072.5 (0)
524# Inverse1: 115.02061966879249
525# RhumbSolve 'Test' 9: /opt/local/bin/RhumbSolve -p 10 -i \ 40.600000000000001 -73.799999999999997 35.799999999999997 140.300000000000011 (Inverse3)
526# RhumbSolve 'Test' 9: azi12=-92.388887981699654, s12=12782581.0676841699, S12=-63760642939072.5 (0)
527# Inverse3: Distance3Tuple(distance=12782581.067684, initial=267.611112, final=267.611112)
529# Position: True GDict(azi12=51, lat1=40.6, lat2=71.6889, lon1=-73.8, lon2=0.25552, s12=5500000.0, S12=44095641862956.109375)
532# % python3 -m pygeodesy.rhumbsolve 60 0 30 0 45 1e6
534# version: /opt/local/bin/RhumbSolve: GeographicLib version 2.2
535# s23 lat3 lon3 cos()
536# 1 1000000.000 36.37559999, 7.58982303, -5.83098638e-01
537# 2 4532573.097 58.84251798, 41.57078946, 4.05349594e-01
538# 3 2233216.895 44.22871762, 17.86660260, -2.91432608e-01
539# 4 3168401.173 50.17678842, 26.60741388, 3.00555188e-02
540# 5 3082690.347 49.63189746, 25.76374255, -1.49446251e-04
541# 6 3083112.629 49.63458216, 25.76787599, -2.59865190e-09
542# 7 3083112.636 49.63458221, 25.76787606, 4.96052409e-16
543# 8 3083112.636 49.63458221, 25.76787606, -4.96052409e-16
544# 9 3083112.636 49.63458221, 25.76787606, 4.96052409e-16
545# 10 3083112.636 49.63458221, 25.76787606, -4.96052409e-16
546# 11 3083112.636 49.63458221, 25.76787606, 4.96052409e-16
547# 12 3083112.636 49.63458221, 25.76787606, -4.96052409e-16
548# 13 3083112.636 49.63458221, 25.76787606, 4.96052409e-16
549# 14 3083112.636 49.63458221, 25.76787606, -4.96052409e-16
550# 15 3083112.636 49.63458221, 25.76787606, 4.96052409e-16
552# **) MIT License
553#
554# Copyright (C) 2022-2023 -- mrJean1 at Gmail -- All Rights Reserved.
555#
556# Permission is hereby granted, free of charge, to any person obtaining a
557# copy of this software and associated documentation files (the "Software"),
558# to deal in the Software without restriction, including without limitation
559# the rights to use, copy, modify, merge, publish, distribute, sublicense,
560# and/or sell copies of the Software, and to permit persons to whom the
561# Software is furnished to do so, subject to the following conditions:
562#
563# The above copyright notice and this permission notice shall be included
564# in all copies or substantial portions of the Software.
565#
566# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
567# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
568# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
569# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
570# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
571# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
572# OTHER DEALINGS IN THE SOFTWARE.