Coverage for pygeodesy/rhumb/bases.py: 94%
373 statements
« prev ^ index » next coverage.py v7.6.1, created at 2025-04-09 11:05 -0400
« prev ^ index » next coverage.py v7.6.1, created at 2025-04-09 11:05 -0400
2# -*- coding: utf-8 -*-
4u'''(INTERNAL) base classes C{RhumbBase} and C{RhumbLineBase}, pure Python version of I{Karney}'s
5C++ classes U{Rhumb<https://GeographicLib.SourceForge.io/C++/doc/classGeographicLib_1_1Rhumb.html>}
6and U{RhumbLine<https://GeographicLib.SourceForge.io/C++/doc/classGeographicLib_1_1RhumbLine.html>}
7from I{GeographicLib versions 2.0} and I{2.2} and I{Karney}'s C++ example U{Rhumb intersect
8<https://SourceForge.net/p/geographiclib/discussion/1026620/thread/2ddc295e/>}.
10Class L{RhumbLineBase} has been enhanced with methods C{Intersecant2}, C{Intersection} and C{PlumbTo}
11to iteratively find the intersection of a rhumb line and a circle or an other rhumb line, respectively
12a perpendicular geodesic or other rhumb line.
14For more details, see the C++ U{GeographicLib<https://GeographicLib.SourceForge.io/C++/doc/index.html>}
15documentation, especially the U{Class List<https://GeographicLib.SourceForge.io/C++/doc/annotated.html>},
16the background information on U{Rhumb lines<https://GeographicLib.SourceForge.io/C++/doc/rhumb.html>},
17the utily U{RhumbSolve<https://GeographicLib.SourceForge.io/C++/doc/RhumbSolve.1.html>} and U{Online
18rhumb line calculations<https://GeographicLib.SourceForge.io/cgi-bin/RhumbSolve>}.
20Copyright (C) U{Charles Karney<mailto:Karney@Alum.MIT.edu>} (2014-2024) and licensed under the MIT/X11
21License. For more information, see the U{GeographicLib<https://GeographicLib.SourceForge.io>} documentation.
22'''
23# make sure int/int division yields float quotient
24from __future__ import division as _; del _ # PYCHOK semicolon
26from pygeodesy.basics import _copysign, itemsorted, unsigned0, _xinstanceof
27from pygeodesy.constants import EPS, EPS0, EPS1, INT0, NAN, _over, \
28 _EPSqrt as _TOL, _0_0, _0_01, _1_0, _90_0
29from pygeodesy.datums import Datum, _earth_datum, _spherical_datum, _WGS84
30from pygeodesy.errors import IntersectionError, RhumbError, _xdatum, \
31 _xkwds, _xkwds_pop2, _Xorder
32# from pygeodesy.etm import ExactTransverseMercator # _MODS
33from pygeodesy.fmath import euclid, favg, sqrt_a, Fsum
34# from pygeodesy.formy import opposing # _MODS
35# from pygeodesy.fsums import Fsum # from .fmath
36from pygeodesy.internals import _DUNDER_nameof, _under
37from pygeodesy.interns import NN, _coincident_, _COMMASPACE_, _Dash, \
38 _parallel_, _too_
39from pygeodesy.karney import _atan2d, Caps, _CapsBase, _diff182, _fix90, \
40 _norm180, GDict
41# from pygeodesy.ktm import KTransverseMercator, _AlpCoeffs # _MODS
42from pygeodesy.lazily import _ALL_DOCS, _ALL_MODS as _MODS
43from pygeodesy.namedTuples import Distance2Tuple, LatLon2Tuple
44from pygeodesy.props import deprecated_method, Property, Property_RO, \
45 property_RO, _update_all
46from pygeodesy.streprs import Fmt, pairs
47from pygeodesy.units import Float_, Lat, Lon, Meter, Radius_, Int # PYCHOK shared
48from pygeodesy.utily import acos1, _azireversed, _loneg, sincos2d, sincos2d_, \
49 _unrollon, _Wrap
50from pygeodesy.vector3d import _intersect3d3, Vector3d # in .Intersection below
52from math import cos, fabs
54__all__ = ()
55__version__ = '24.10.14'
57_anti_ = _Dash('anti')
58_rls = [] # instances of C{RbumbLine...} to be updated
59_TRIPS = 129 # .Intersection, .PlumbTo, 19+
62class _Lat(Lat):
63 '''(INTERNAL) Latitude B{C{lat}}.
64 '''
65 def __init__(self, *lat, **Error_name):
66 kwds = _xkwds(Error_name, clip=0, Error=RhumbError)
67 Lat.__new__(_Lat, *lat, **kwds)
70class _Lon(Lon):
71 '''(INTERNAL) Longitude B{C{lon}}.
72 '''
73 def __init__(self, *lon, **Error_name):
74 kwds = _xkwds(Error_name, clip=0, Error=RhumbError)
75 Lon.__new__(_Lon, *lon, **kwds)
78def _update_all_rls(r):
79 '''(INTERNAL) Zap cached/memoized C{Property[_RO]}s
80 of any C{RhumbLine} instances tied to the given
81 C{Rhumb} instance B{C{r}}.
82 '''
83 # _xinstanceof(_MODS.rhumb.aux_.RhumbAux, _MODS.rhumb.ekx.Rhumb, r=r)
84 _update_all(r)
85 for rl in _rls: # PYCHOK use weakref?
86 if rl._rhumb is r:
87 _update_all(rl)
90class RhumbBase(_CapsBase):
91 '''(INTERNAL) Base class for C{rhumb.aux_.RhumbAux} and C{rhumb.ekx.Rhumb}.
92 '''
93 _datum = _WGS84
94 _exact = True
95 _f_max = _0_01
96 _mTM = 6 # see .TMorder
98 def __init__(self, a_earth, f, exact, TMorder_name):
99 '''New C{RhumbAux} or C{Rhumb}.
100 '''
101 if TMorder_name:
102 M = self._mTM
103 m, name = _xkwds_pop2(TMorder_name, TMorder=M)
104 if m != M:
105 self.TMorder = m
106 else:
107 name = {}
108 _earth_datum(self, a_earth, f=f, **name)
109 if not exact:
110 self.exact = False
111 if name:
112 self.name = name
114 @Property_RO
115 def a(self):
116 '''Get the C{ellipsoid}'s equatorial radius, semi-axis (C{meter}).
117 '''
118 return self.ellipsoid.a
120 equatoradius = a
122 def ArcDirect(self, lat1, lon1, azi12, a12, outmask=Caps.LATITUDE_LONGITUDE):
123 '''Solve the I{direct rhumb} problem, optionally with area.
125 @arg lat1: Latitude of the first point (C{degrees90}).
126 @arg lon1: Longitude of the first point (C{degrees180}).
127 @arg azi12: Azimuth of the rhumb line (compass C{degrees}).
128 @arg a12: Angle along the rhumb line from the given to the
129 destination point (C{degrees}), can be negative.
131 @return: L{GDict} with 2 up to 8 items C{lat2, lon2, a12, S12,
132 lat1, lon1, azi12, s12} with the destination point's
133 latitude C{lat2} and longitude C{lon2} in C{degrees},
134 the rhumb angle C{a12} in C{degrees} and area C{S12}
135 under the rhumb line in C{meter} I{squared}.
137 @raise ImportError: Package C{numpy} not found or not installed,
138 only required for area C{S12} when C{B{exact}
139 is True} and L{RhumbAux}.
141 @note: If B{C{a12}} is large enough that the rhumb line crosses
142 a pole, the longitude of the second point is indeterminate
143 and C{NAN} is returned for C{lon2} and area C{S12}.
145 @note: If the given point is a pole, the cosine of its latitude is
146 taken to be C{sqrt(L{EPS})}. This position is extremely
147 close to the actual pole and allows the calculation to be
148 carried out in finite terms.
149 '''
150 s12 = a12 * self._mpd
151 return self._DirectRhumb(lat1, lon1, azi12, a12, s12, outmask)
153 @Property_RO
154 def b(self):
155 '''Get the C{ellipsoid}'s polar radius, semi-axis (C{meter}).
156 '''
157 return self.ellipsoid.b
159 polaradius = b
161 @property
162 def datum(self):
163 '''Get this rhumb's datum (L{Datum}).
164 '''
165 return self._datum
167 @datum.setter # PYCHOK setter!
168 def datum(self, datum):
169 '''Set this rhumb's datum (L{Datum}).
171 @raise RhumbError: If C{abs(B{f}} exceeds non-zero C{f_max} and C{exact=False}.
172 '''
173 _xinstanceof(Datum, datum=datum)
174 if self._datum != datum:
175 self._exactest(self.exact, datum.ellipsoid, self.f_max)
176 _update_all_rls(self)
177 self._datum = datum
179 def _Direct(self, ll1, azi12, s12, **outmask):
180 '''(INTERNAL) Short-cut version, see .latlonBase.rhumb....
181 '''
182 return self.Direct(ll1.lat, ll1.lon, azi12, s12, **outmask)
184 def Direct(self, lat1, lon1, azi12, s12, outmask=Caps.LATITUDE_LONGITUDE):
185 '''Solve the I{direct rhumb} problem, optionally with area.
187 @arg lat1: Latitude of the first point (C{degrees90}).
188 @arg lon1: Longitude of the first point (C{degrees180}).
189 @arg azi12: Azimuth of the rhumb line (compass C{degrees}).
190 @arg s12: Distance along the rhumb line from the given to
191 the destination point (C{meter}), can be negative.
193 @return: L{GDict} with 2 up to 8 items C{lat2, lon2, a12, S12,
194 lat1, lon1, azi12, s12} with the destination point's
195 latitude C{lat2} and longitude C{lon2} in C{degrees},
196 the rhumb angle C{a12} in C{degrees} and area C{S12}
197 under the rhumb line in C{meter} I{squared}.
199 @raise ImportError: Package C{numpy} not found or not installed,
200 only required for area C{S12} when C{B{exact}
201 is True} and L{RhumbAux}.
203 @note: If B{C{s12}} is large enough that the rhumb line crosses
204 a pole, the longitude of the second point is indeterminate
205 and C{NAN} is returned for C{lon2} and area C{S12}.
207 @note: If the given point is a pole, the cosine of its latitude is
208 taken to be C{sqrt(L{EPS})}. This position is extremely
209 close to the actual pole and allows the calculation to be
210 carried out in finite terms.
211 '''
212 a12 = _over(s12, self._mpd)
213 return self._DirectRhumb(lat1, lon1, azi12, a12, s12, outmask)
215 def Direct8(self, lat1, lon1, azi12, s12, outmask=Caps.LATITUDE_LONGITUDE_AREA):
216 '''Like method L{Rhumb.Direct} but returning a L{Rhumb8Tuple} with area C{S12}.
217 '''
218 return self.Direct(lat1, lon1, azi12, s12, outmask=outmask).toRhumb8Tuple()
220 def _DirectLine(self, ll1, azi12, **caps_name):
221 '''(INTERNAL) Short-cut version, see .latlonBase.
222 '''
223 return self.DirectLine(ll1.lat, ll1.lon, azi12, **caps_name)
225 def DirectLine(self, lat1, lon1, azi12, **caps_name):
226 '''Define a C{RhumbLine} in terms of the I{direct} rhumb
227 problem to compute several points on a single rhumb line.
229 @arg lat1: Latitude of the first point (C{degrees90}).
230 @arg lon1: Longitude of the first point (C{degrees180}).
231 @arg azi12: Azimuth of the rhumb line (compass C{degrees}).
232 @kwarg caps_name: Optional keyword arguments C{B{name}=NN} and
233 C{B{caps}=Caps.STANDARD}, a bit-or'ed combination of
234 L{Caps} values specifying the required capabilities.
235 Include C{Caps.LINE_OFF} if updates to the B{C{rhumb}}
236 should I{not} be reflected in this rhumb line.
238 @return: A C{RhumbLine...} instance and invoke its method
239 C{.Position} to compute each point.
241 @note: Updates to this rhumb are reflected in the returned
242 rhumb line, unless C{B{caps} |= Caps.LINE_OFF}.
243 '''
244 return self._RhumbLine(self, lat1, lon1, azi12, **caps_name)
246 Line = DirectLine # synonyms
248 def _DirectRhumb(self, lat1, lon1, azi12, a12, s12, outmask):
249 '''(INTERNAL) See methods C{.ArcDirect} and C{.Direct}.
250 '''
251 rl = self._RhumbLine(self, lat1, lon1, azi12, caps=Caps.LINE_OFF,
252 name=self.name)
253 return rl._Position(a12, s12, outmask | self._debug) # lat2, lon2, S12
255 @Property
256 def ellipsoid(self):
257 '''Get this rhumb's ellipsoid (L{Ellipsoid}).
258 '''
259 return self.datum.ellipsoid
261 @ellipsoid.setter # PYCHOK setter!
262 def ellipsoid(self, a_earth_f):
263 '''Set this rhumb's ellipsoid (L{Ellipsoid}, L{Ellipsoid2}, L{Datum} or
264 L{a_f2Tuple}) or (equatorial) radius and flattening (2-tuple C{(a, f)}).
266 @raise RhumbError: If C{abs(B{f}} exceeds non-zero C{f_max} and C{exact=False}.
267 '''
268 self.datum = _spherical_datum(a_earth_f, Error=RhumbError)
270 @Property
271 def exact(self):
272 '''Get the I{exact} option (C{bool}).
273 '''
274 return self._exact
276 @exact.setter # PYCHOK setter!
277 def exact(self, exact):
278 '''Set the I{exact} option (C{bool}). If C{True}, use I{exact} rhumb
279 expressions, otherwise a series expansion (accurate for oblate or
280 prolate ellipsoids with C{abs(flattening)} below C{f_max}.
282 @raise RhumbError: If C{B{exact}=False} and C{abs(flattening})
283 exceeds non-zero C{f_max}.
285 @see: Option U{B{-s}<https://GeographicLib.SourceForge.io/C++/doc/RhumbSolve.1.html>}
286 and U{ACCURACY<https://GeographicLib.SourceForge.io/C++/doc/RhumbSolve.1.html#ACCURACY>}.
287 '''
288 x = bool(exact)
289 if self._exact != x:
290 self._exactest(x, self.ellipsoid, self.f_max)
291 _update_all_rls(self)
292 self._exact = x
294 def _exactest(self, exact, ellipsoid, f_max):
295 # Helper for property setters C{ellipsoid}, C{exact} and C{f_max}
296 if fabs(ellipsoid.f) > f_max > 0 and not exact:
297 raise RhumbError(exact=exact, f=ellipsoid.f, f_max=f_max)
299 @Property_RO
300 def f(self):
301 '''Get the C{ellipsoid}'s flattening (C{float}).
302 '''
303 return self.ellipsoid.f
305 flattening = f
307 @property
308 def f_max(self):
309 '''Get the I{max.} flattening (C{float}).
310 '''
311 return self._f_max
313 @f_max.setter # PYCHOK setter!
314 def f_max(self, f_max): # PYCHOK no cover
315 '''Set the I{max.} flattening, not to exceed (C{float}).
317 @raise RhumbError: If C{exact=False} and C{abs(flattening})
318 exceeds non-zero C{f_max}.
319 '''
320 f = Float_(f_max=f_max, low=_0_0, high=EPS1)
321 if self._f_max != f:
322 self._exactest(self.exact, self.ellipsoid, f)
323 self._f_max = f
325 def _Inverse(self, ll1, ll2, wrap, **outmask):
326 '''(INTERNAL) Short-cut version, see .latlonBase.rhumb....
327 '''
328 if wrap:
329 ll2 = _unrollon(ll1, _Wrap.point(ll2))
330 return self.Inverse(ll1.lat, ll1.lon, ll2.lat, ll2.lon, **outmask)
332 def Inverse(self, lat1, lon1, lat2, lon2, outmask=Caps.AZIMUTH_DISTANCE):
333 '''Solve the I{inverse rhumb} problem.
335 @arg lat1: Latitude of the first point (C{degrees90}).
336 @arg lon1: Longitude of the first point (C{degrees180}).
337 @arg lat2: Latitude of the second point (C{degrees90}).
338 @arg lon2: Longitude of the second point (C{degrees180}).
340 @return: L{GDict} with 4 to 9 items C{lat1, lon1, lat2, lon2,
341 azi12, azi21, s12, a12, S12}, the rhumb line's azimuth
342 C{azi12} and I{reverse} azimuth C{azi21}, both in
343 compass C{degrees} between C{-180} and C{+180}, the
344 rhumb distance C{s12} and rhumb angle C{a12} between
345 both points in C{meter} respectively C{degrees} and
346 the area C{S12} under the rhumb line in C{meter}
347 I{squared}.
349 @raise ImportError: Package C{numpy} not found or not installed,
350 only required for L{RhumbAux} area C{S12}
351 when C{B{exact} is True}.
353 @note: The shortest rhumb line is found. If the end points are
354 on opposite meridians, there are two shortest rhumb lines
355 and the East-going one is chosen.
357 @note: If either point is a pole, the cosine of its latitude is
358 taken to be C{sqrt(L{EPS})}. This position is extremely
359 close to the actual pole and allows the calculation to be
360 carried out in finite terms.
361 '''
362 r = GDict(lat1=lat1, lon1=lon1, lat2=lat2, lon2=lon2, name=self.name)
363 Cs = Caps
364 if (outmask & Cs.AZIMUTH_DISTANCE_AREA):
365 lon12, _ = _diff182(lon1, lon2, K_2_0=True)
366 y, x, s1, s2 = self._Inverse4(lon12, r, outmask)
367 if (outmask & Cs.AZIMUTH):
368 z = _atan2d(y, x)
369 r.set_(azi12=z, azi21=_azireversed(z))
370 if (outmask & Cs.AREA):
371 S12 = self._S12d(s1, s2, lon12)
372 r.set_(S12=unsigned0(S12)) # like .gx
373 return r
375 def _Inverse4(self, lon12, r, outmask): # PYCHOK no cover
376 '''(INTERNAL) I{Must be overloaded}.'''
377 self._notOverloaded(lon12, r, Caps.toStr(outmask)) # underOK=True
379 def Inverse8(self, lat1, lon1, azi12, s12, outmask=Caps.AZIMUTH_DISTANCE_AREA):
380 '''Like method L{Rhumb.Inverse} but returning a L{Rhumb8Tuple} with area C{S12}.
381 '''
382 return self.Inverse(lat1, lon1, azi12, s12, outmask=outmask).toRhumb8Tuple()
384 def _InverseLine(self, ll1, ll2, wrap, **caps_name):
385 '''(INTERNAL) Short-cut version, see .latlonBase.
386 '''
387 if wrap:
388 ll2 = _unrollon(ll1, _Wrap.point(ll2))
389 return self.InverseLine(ll1.lat, ll1.lon, ll2.lat, ll2.lon, **caps_name)
391 def InverseLine(self, lat1, lon1, lat2, lon2, **caps_name):
392 '''Define a C{RhumbLine} in terms of the I{inverse} rhumb problem.
394 @arg lat1: Latitude of the first point (C{degrees90}).
395 @arg lon1: Longitude of the first point (C{degrees180}).
396 @arg lat2: Latitude of the second point (C{degrees90}).
397 @arg lon2: Longitude of the second point (C{degrees180}).
398 @kwarg caps_name: Optional keyword arguments C{B{name}=NN} and
399 C{B{caps}=Caps.STANDARD}, a bit-or'ed combination of
400 L{Caps} values specifying the required capabilities.
401 Include C{Caps.LINE_OFF} if updates to the B{C{rhumb}}
402 should I{not} be reflected in this rhumb line.
404 @return: A C{RhumbLine...} instance and invoke its method
405 C{ArcPosition} or C{Position} to compute points.
407 @note: Updates to this rhumb are reflected in the returned
408 rhumb line, unless C{B{caps} |= Caps.LINE_OFF}.
409 '''
410 r = self.Inverse(lat1, lon1, lat2, lon2, outmask=Caps.AZIMUTH)
411 return self._RhumbLine(self, lat1, lon1, r.azi12, **caps_name)
413 @Property_RO
414 def _mpd(self): # PYCHOK no cover
415 '''(INTERNAL) I{Must be overloaded}.'''
416 _MODS.named.notOverloaded(self)
418 @property_RO
419 def RAorder(self):
420 '''Get the I{Rhumb Area} order, C{None} always.
421 '''
422 return None
424 @property_RO
425 def _RhumbLine(self): # PYCHOK no cover
426 '''(INTERNAL) I{Must be overloaded}.'''
427 self._notOverloaded(underOK=True)
429 def _S12d(self, s1, s2, lon): # PYCHOK no cover
430 '''(INTERNAL) I{Must be overloaded}.'''
431 self._notOverloaded(s1, s2, lon) # underOK=True
433 @Property
434 def TMorder(self):
435 '''Get the L{KTransverseMercator} order (C{int}, 4, 5, 6, 7 or 8).
436 '''
437 return self._mTM
439 @TMorder.setter # PYCHOK setter!
440 def TMorder(self, order):
441 '''Set the L{KTransverseMercator} order (C{int}, 4, 5, 6, 7 or 8).
443 @note: Setting C{TMorder} turns property C{exact} off, but only
444 for L{Rhumb} instances.
445 '''
446 m = _Xorder(_MODS.ktm._AlpCoeffs, RhumbError, TMorder=order)
447 if self._mTM != m:
448 _update_all_rls(self)
449 self._mTM = m
450 if self.exact and isinstance(self, _MODS.rhumb.ekx.Rhumb):
451 self.exact = False
453 def toStr(self, prec=6, sep=_COMMASPACE_, **unused): # PYCHOK signature
454 '''Return this C{Rhumb} as string.
456 @kwarg prec: The C{float} precision, number of decimal digits (0..9).
457 Trailing zero decimals are stripped for B{C{prec}} values
458 of 1 and above, but kept for negative B{C{prec}} values.
459 @kwarg sep: Separator to join (C{str}).
461 @return: Tuple items (C{str}).
462 '''
463 d = dict(ellipsoid=self.ellipsoid, RAorder=self.RAorder,
464 exact=self.exact, TMorder=self.TMorder)
465 return sep.join(pairs(itemsorted(d, asorted=False), prec=prec))
468class RhumbLineBase(_CapsBase):
469 '''(INTERNAL) Base class for C{rhumb.aux_.RhumbLineAux} and C{rhumb.ekx.RhumbLine}.
470 '''
471 _azi12 = _0_0
472 _calp = _1_0
473# _caps = \
474# _debug = 0
475# _lat1 = \
476# _lon1 = \
477# _lon12 = _0_0
478 _Rhumb = RhumbBase # compatible C{Rhumb} class
479 _rhumb = None # C{Rhumb} instance
480 _salp = \
481 _talp = _0_0
483 def __init__(self, rhumb, lat1, lon1, azi12, caps=Caps.STANDARD, name=NN):
484 '''New C{RhumbLine} or C{RhumbLineAux}.
485 '''
486 _xinstanceof(self._Rhumb, rhumb=rhumb)
488 self._lat1 = _Lat(lat1=_fix90(lat1))
489 self._lon1 = _Lon(lon1= lon1)
490 self._lon12 = _norm180(self._lon1)
491 if azi12: # non-zero, non-None
492 self.azi12 = _norm180(azi12)
494 n = name or rhumb.name
495 if n:
496 self.name=n
498 self._caps = caps
499 self._debug |= (caps | rhumb._debug) & Caps._DEBUG_DIRECT_LINE
500 if (caps & Caps.LINE_OFF): # copy to avoid updates
501 self._rhumb = rhumb.copy(deep=False, name=_under(rhumb.name))
502 else:
503 self._rhumb = rhumb
504 _rls.append(self)
506 def __del__(self): # XXX use weakref?
507 if _rls: # may be empty or None
508 try: # PYCHOK no cover
509 _rls.remove(self)
510 except (TypeError, ValueError):
511 pass
512 self._rhumb = None
513 # _update_all(self) # throws TypeError during Python 2 cleanup
515 def ArcPosition(self, a12, outmask=Caps.LATITUDE_LONGITUDE):
516 '''Compute a point at a given angular distance on this rhumb line.
518 @arg a12: The angle along this rhumb line from its origin to the
519 point (C{degrees}), can be negative.
520 @kwarg outmask: Bit-or'ed combination of L{Caps} values specifying
521 the quantities to be returned.
523 @return: L{GDict} with 4 to 8 items C{azi12, a12, s12, S12, lat2,
524 lon2, lat1, lon1} with latitude C{lat2} and longitude
525 C{lon2} of the point in C{degrees}, the rhumb distance
526 C{s12} in C{meter} from the start point of and the area
527 C{S12} under this rhumb line in C{meter} I{squared}.
529 @raise ImportError: Package C{numpy} not found or not installed,
530 only required for L{RhumbLineAux} area C{S12}
531 when C{B{exact} is True}.
533 @note: If B{C{a12}} is large enough that the rhumb line crosses a
534 pole, the longitude of the second point is indeterminate and
535 C{NAN} is returned for C{lon2} and area C{S12}.
537 If the first point is a pole, the cosine of its latitude is
538 taken to be C{sqrt(L{EPS})}. This position is extremely
539 close to the actual pole and allows the calculation to be
540 carried out in finite terms.
541 '''
542 return self._Position(a12, self.degrees2m(a12), outmask)
544 @Property
545 def azi12(self):
546 '''Get this rhumb line's I{azimuth} (compass C{degrees}).
547 '''
548 return self._azi12
550 @azi12.setter # PYCHOK setter!
551 def azi12(self, azi12):
552 '''Set this rhumb line's I{azimuth} (compass C{degrees}).
553 '''
554 z = _norm180(azi12)
555 if self._azi12 != z:
556 if self._rhumb:
557 _update_all(self)
558 self._azi12 = z
559 self._salp, self._calp = t = sincos2d(z) # no NEG0
560 self._talp = _over(*t)
562 @property_RO
563 def azi12_sincos2(self): # PYCHOK no cover
564 '''Get the sine and cosine of this rhumb line's I{azimuth} (2-tuple C{(sin, cos)}).
565 '''
566 return self._scalp, self._calp
568 @property_RO
569 def datum(self):
570 '''Get this rhumb line's datum (L{Datum}).
571 '''
572 return self.rhumb.datum
574 def degrees2m(self, angle):
575 '''Convert an angular distance along this rhumb line to C{meter}.
577 @arg angle: Angular distance (C{degrees}).
579 @return: Distance (C{meter}).
580 '''
581 return float(angle) * self.rhumb._mpd
583 @deprecated_method
584 def distance2(self, lat, lon): # PYCHOK no cover
585 '''DEPRECATED on 23.09.23, use method L{RhumbLineAux.Inverse} or L{RhumbLine.Inverse}.
587 @return: A L{Distance2Tuple}C{(distance, initial)} with the C{distance}
588 in C{meter} and C{initial} bearing (azimuth) in C{degrees}.
589 '''
590 r = self.Inverse(lat, lon)
591 return Distance2Tuple(r.s12, r.azi12)
593 @property_RO
594 def ellipsoid(self):
595 '''Get this rhumb line's ellipsoid (L{Ellipsoid}).
596 '''
597 return self.rhumb.ellipsoid
599 @property_RO
600 def exact(self):
601 '''Get this rhumb line's I{exact} option (C{bool}).
602 '''
603 return self.rhumb.exact
605 def Intersecant2(self, lat0, lon0, radius, napier=True, **tol_eps):
606 '''Compute the intersection(s) of this rhumb line and a circle.
608 @arg lat0: Latitude of the circle center (C{degrees}).
609 @arg lon0: Longitude of the circle center (C{degrees}).
610 @arg radius: Radius of the circle (C{meter}, conventionally).
611 @kwarg napier: If C{True}, apply I{Napier}'s spherical triangle
612 instead of planar trigonometry (C{bool}).
613 @kwarg tol_eps: Optional keyword arguments, see method
614 method L{Intersection} for further details.
616 @return: 2-Tuple C{(P, Q)} with both intersections (representing
617 a rhumb chord), each a L{GDict} from method L{Intersection}
618 extended to 18 items by C{lat3, lon3, azi03, a03, s03}
619 with azimuth C{azi03} of, distance C{a03} in C{degrees}
620 and C{s03} in C{meter} along the rhumb line from the circle
621 C{lat0, lon0} to the chord center C{lat3, lon3}. If this
622 rhumb line is tangential to the circle, both points
623 are the same L{GDict} instance with distances C{s02} and
624 C{s03} near-equal to the B{C{radius}}.
626 @raise IntersectionError: The circle and this rhumb line
627 do not intersect.
629 @raise UnitError: Invalid B{C{radius}}.
630 '''
631 r = Radius_(radius)
632 p = q = self.PlumbTo(lat0, lon0, exact=None, **tol_eps)
633 a = q.s02
634 t = dict(lat3=q.lat2, lon3=q.lon2, azi03=q.azi02, a03=q.a02, s03=a)
635 if a < r:
636 t.update(iteration=q.iteration, lat0=q.lat1, lon0=q.lon1, # or lat0, lon0
637 name=_DUNDER_nameof(self.Intersecant2, self.name))
638 if fabs(a) < EPS0: # coincident centers
639 d, h = _0_0, r
640 else:
641 d = q.s12
642 if napier: # Napier rule (R1) cos(b) = cos(c) / cos(a)
643 # <https://WikiPedia.org/wiki/Spherical_trigonometry>
644 m = self.rhumb._mpr
645 h = (acos1(cos(r / m) / cos(a / m)) * m) if m else _0_0
646 else:
647 h = _copysign(sqrt_a(r, a), a)
648 p = q = self.Position(d + h).set_(**t)
649 if h:
650 q = self.Position(d - h).set_(**t)
651 elif a > r:
652 t = _too_(Fmt.distant(a))
653 raise IntersectionError(self, lat0, lon0, radius,
654 txt=t, **tol_eps)
655 else: # tangential
656 q.set_(**t) # == p.set(_**t)
657 return p, q
659 @deprecated_method
660 def intersection2(self, other, **tol_eps): # PYCHOK no cover
661 '''DEPRECATED on 23.10.10, use method L{Intersection}.'''
662 p = self.Intersection(other, **tol_eps)
663 r = LatLon2Tuple(p.lat2, p.lon2, name=self.intersection2.__name__)
664 r._iteration = p.iteration
665 return r
667 def Intersection(self, other, tol=_TOL, **eps):
668 '''I{Iteratively} find the intersection of this and an other rhumb line.
670 @arg other: The other rhumb line (C{RhumbLine}).
671 @kwarg tol: Tolerance for longitudinal convergence and parallel
672 error (C{degrees}).
673 @kwarg eps: Tolerance for L{pygeodesy.intersection3d3} (C{EPS}).
675 @return: The intersection point, a L{Position}-like L{GDict} with
676 13 items C{lat1, lon1, azi12, a12, s12, lat2, lon2, lat0,
677 lon0, azi02, a02, s02, at} with the rhumb angle C{a02}
678 and rhumb distance C{s02} between the start point C{lat0,
679 lon0} of the B{C{other}} rhumb line and the intersection
680 C{lat2, lon2}, the azimuth C{azi02} of the B{C{other}}
681 rhumb line and the angle C{at} between both rhumb lines.
682 See method L{Position} for further details.
684 @raise IntersectionError: No convergence for this B{C{tol}} or
685 no intersection for an other reason.
687 @see: Methods C{distance2} and C{PlumbTo} and function
688 L{pygeodesy.intersection3d3}.
690 @note: Each iteration involves a round trip to this rhumb line's
691 L{ExactTransverseMercator} or L{KTransverseMercator}
692 projection and function L{pygeodesy.intersection3d3} in
693 that domain.
694 '''
695 _xinstanceof(RhumbLineBase, other=other)
696 _xdatum(self.rhumb, other.rhumb, Error=RhumbError)
697 try:
698 if self.others(other) is self:
699 raise ValueError(_coincident_)
700 # make invariants and globals locals
701 _s_3d, s_az = self._xTM3d, self.azi12
702 _o_3d, o_az = other._xTM3d, other.azi12
703 p = _MODS.formy.opposing(s_az, o_az, margin=tol)
704 if p is not None: # == p in (True, False)
705 raise ValueError(_anti_(_parallel_) if p else _parallel_)
706 _diff = euclid # approximate length
707 _i3d3 = _intersect3d3 # NOT .vector3d.intersection3d3
708 _LL2T = LatLon2Tuple
709 _xTMr = self.xTM.reverse # ellipsoidal or spherical
710 # use halfway point as initial estimate
711 p = _LL2T(favg(self.lat1, other.lat1),
712 favg(self.lon1, other.lon1))
713 for i in range(1, _TRIPS):
714 v = _i3d3(_s_3d(p), s_az, # point + bearing
715 _o_3d(p), o_az, useZ=False, **eps)[0]
716 t = _xTMr(v.x, v.y, lon0=p.lon) # PYCHOK Reverse4Tuple
717 d = _diff(t.lon - p.lon, t.lat) # PYCHOK t.lat + p.lat - p.lat
718 p = _LL2T(t.lat + p.lat, t.lon) # PYCHOK t.lon + p.lon = lon0
719 if d < tol: # 19+ trips
720 break
721 else:
722 raise ValueError(Fmt.no_convergence(d, tol))
724 P = GDict(lat1=self.lat1, lat2=p.lat, lat0=other.lat1,
725 lon1=self.lon1, lon2=p.lon, lon0=other.lon1,
726 name=_DUNDER_nameof(self.Intersection, self.name))
727 r = self.Inverse(p.lat, p.lon, outmask=Caps.DISTANCE)
728 t = other.Inverse(p.lat, p.lon, outmask=Caps.DISTANCE)
729 P.set_(azi12= self.azi12, a12=r.a12, s12=r.s12,
730 azi02=other.azi12, a02=t.a12, s02=t.s12,
731 at=other.azi12 - self.azi12, iteration=i)
732 except Exception as x:
733 raise IntersectionError(self, other, tol=tol,
734 eps=eps, cause=x)
735 return P
737 def Inverse(self, lat2, lon2, wrap=False, **outmask):
738 '''Return the rhumb angle, distance, azimuth, I{reverse} azimuth, etc. of
739 a rhumb line between the given point and this rhumb line's start point.
741 @arg lat2: Latitude of the point (C{degrees}).
742 @arg lon2: Longitude of the points (C{degrees}).
743 @kwarg wrap: If C{True}, wrap or I{normalize} and unroll B{C{lat2}}
744 and B{C{lon2}} (C{bool}).
746 @return: L{GDict} with 8 items C{a12, s12, azi12, azi21, lat1, lon1,
747 lat2, lon2}, the rhumb angle C{a12} and rhumb distance C{s12}
748 between both points in C{degrees} respectively C{meter}, the
749 rhumb line's azimuth C{azi12} and I{reverse} azimuth C{azi21}
750 both in compass C{degrees} between C{-180} and C{+180}.
751 '''
752 if wrap:
753 _, lat2, lon2 = _Wrap.latlon3(self.lon1, _fix90(lat2), lon2, wrap)
754 r = self.rhumb.Inverse(self.lat1, self.lon1, lat2, lon2, **outmask)
755 return r
757 @Property_RO
758 def isLoxodrome(self):
759 '''Is this rhumb line a meridional (C{None}), a parallel
760 (C{False}) or a C{True} loxodrome?
762 @see: I{Osborne's} U{2.5 Rumb lines and loxodromes
763 <https://Zenodo.org/record/35392>}, page 37.
764 '''
765 return bool(self._salp) if self._calp else None
767 @Property_RO
768 def lat1(self):
769 '''Get this rhumb line's latitude (C{degrees90}).
770 '''
771 return self._lat1
773 @Property_RO
774 def lon1(self):
775 '''Get this rhumb line's longitude (C{degrees180}).
776 '''
777 return self._lon1
779 @Property_RO
780 def latlon1(self):
781 '''Get this rhumb line's lat- and longitude (L{LatLon2Tuple}C{(lat, lon)}).
782 '''
783 return LatLon2Tuple(self.lat1, self.lon1)
785 def m2degrees(self, distance):
786 '''Convert a distance along this rhumb line to an angular distance.
788 @arg distance: Distance (C{meter}).
790 @return: Angular distance (C{degrees}).
791 '''
792 return _over(float(distance), self.rhumb._mpd)
794 @property_RO
795 def _mu1(self): # PYCHOK no cover
796 '''(INTERNAL) I{Must be overloaded}.'''
797 self._notOverloaded(underOK=True)
799 def _mu2lat(self, mu2): # PYCHOK no cover
800 '''(INTERNAL) I{Must be overloaded}.'''
801 self._notOverloaded(mu2) # underOK=True
803 @deprecated_method
804 def nearestOn4(self, lat0, lon0, **exact_eps_est_tol): # PYCHOK no cover
805 '''DEPRECATED on 23.10.10, use method L{PlumbTo}.'''
806 P = self.PlumbTo(lat0, lon0, **exact_eps_est_tol)
807 r = _MODS.deprecated.classes.NearestOn4Tuple(P.lat2, P.lon2, P.s12, P.azi02,
808 name=self.nearestOn4.__name__)
809 r._iteration = P.iteration
810 return r
812 @deprecated_method
813 def NearestOn(self, lat0, lon0, **exact_eps_est_tol): # PYCHOK no cover
814 '''DEPRECATED on 23.10.30, use method L{PlumbTo}.'''
815 return self.PlumbTo(lat0, lon0, **exact_eps_est_tol)
817 def PlumbTo(self, lat0, lon0, exact=None, eps=EPS, est=None, tol=_TOL):
818 '''Compute the I{perpendicular} intersection of this rhumb line with a geodesic
819 from the given point (transcoded from I{Karney}'s C++ U{rhumb-intercept
820 <https://SourceForge.net/p/geographiclib/discussion/1026620/thread/2ddc295e/>}).
822 @arg lat0: Latitude of the point on the geodesic (C{degrees}).
823 @arg lon0: Longitude of the point on the geodesic (C{degrees}).
824 @kwarg exact: If C{None}, use a rhumb line perpendicular to this rhumb line,
825 otherwise use an I{exact} C{Geodesic...} from the given point
826 perpendicular to this rhumb line (C{bool} or C{Geodesic...}),
827 see method L{geodesic_<pygeodesy.Ellipsoid.geodesic_>}.
828 @kwarg eps: Optional tolerance (C{EPS}), used only if C{B{exact} is None},
829 see function L{intersection3d3<pygeodesy.intersection3d3>}.
830 @kwarg est: Optionally, an initial estimate for the distance C{s12} of the
831 intersection I{along} this rhumb line (C{meter}), used only if
832 C{B{exact} is not None}.
833 @kwarg tol: Longitudinal convergence tolerance (C{degrees}) or distance
834 tolerance (C(meter)) when C{B{exact} is None}, respectively
835 C{not None}.
837 @return: The intersection point on this rhumb line, a L{GDict} from method
838 L{Intersection} if B{C{exact}=None}. If C{B{exact} is not None},
839 a L{Position}-like L{GDict} of 13 items C{azi12, a12, s12, lat2,
840 lat1, lat0, lon2, lon1, lon0, azi0, a02, s02, at} with distance
841 C{a02} in C{degrees} and C{s02} in C{meter} between the given point
842 C{lat0, lon0} and the intersection C{lat2, lon2}, geodesic azimuth
843 C{azi0} at the given point and the (perpendicular) angle C{at}
844 between the geodesic and this rhumb line at the intersection. The
845 I{geodesic} azimuth at the intersection is C{(at + azi12)}. See
846 method L{Position} for further details.
848 @raise ImportError: I{Karney}'s U{geographiclib
849 <https://PyPI.org/project/geographiclib>}
850 package not found or not installed.
852 @raise IntersectionError: No convergence for this B{C{eps}} or B{C{tol}} or
853 no intersection for some other reason.
855 @see: Methods C{distance2}, C{Intersecant2} and C{Intersection} and function
856 L{intersection3d3<pygeodesy.intersection3d3>}.
857 '''
858 Cs, tol = Caps, Float_(tol=tol, low=EPS, high=None)
860# def _over(p, q): # see @note at method C{.Position}
861# if p:
862# p = (p / (q or _copysign(tol, q))) if isfinite(q) else NAN
863# return p
865 if exact is None:
866 z = _norm180(self.azi12 + _90_0) # perpendicular azimuth
867 rl = RhumbLineBase(self.rhumb, lat0, lon0, z, caps=Cs.LINE_OFF)
868 P = self.Intersection(rl, tol=tol, eps=eps)
870 else: # C{rhumb-intercept}
871 E = self.ellipsoid
872 _gI = E.geodesic_(exact=exact).Inverse
873 gm = Cs.STANDARD | Cs._REDUCEDLENGTH_GEODESICSCALE # ^ Cs.DISTANCE_IN
874 if est is None: # get an estimate from the "perpendicular" geodesic
875 r = _gI(self.lat1, self.lon1, lat0, lon0, outmask=Cs.AZIMUTH_DISTANCE)
876 d, _ = _diff182(r.azi2, self.azi12, K_2_0=True)
877 _, s12 = sincos2d(d)
878 s12 *= r.s12 # signed
879 else:
880 s12 = Meter(est=est)
881 try:
882 _abs = fabs
883 _d2 = _diff182
884 _ErT = E.rocPrimeVertical # aka rocTransverse
885 _ovr = _over
886 _S12 = Fsum(s12).fsum2f_
887 _scd = sincos2d_
888 for i in range(1, _TRIPS): # 9+, suffix 1 == C++ 2, 2 == C++ 3
889 P = self.Position(s12) # outmask=Cs.LATITUDE_LONGITUDE
890 r = _gI(lat0, lon0, P.lat2, P.lon2, outmask=gm)
891 d, _ = _d2(self.azi12, r.azi2, K_2_0=True)
892 s, c, s2, c2 = _scd(d, r.lat2)
893 c2 *= _ErT(r.lat2)
894 s *= _ovr(s2 * self._salp, c2) - _ovr(s * r.M21, r.m12)
895 s12, t = _S12(c / s) # XXX _ovr?
896 if _abs(t) < tol: # or _abs(c) < EPS
897 break
898 P.set_(azi0=r.azi1, a02=r.a12, s02=r.s12, # azi2=r.azi2,
899 lat0=lat0, lon0=lon0, iteration=i, at=r.azi2 - self.azi12,
900 name=_DUNDER_nameof(self.PlumbTo, self.name))
901 except Exception as x: # Fsum(NAN) Value-, ZeroDivisionError
902 raise IntersectionError(lat0=lat0, lon0=lon0, tol=tol, exact=exact,
903 eps=eps, est=est, iteration=i, cause=x)
905 return P
907 def Position(self, s12, outmask=Caps.LATITUDE_LONGITUDE):
908 '''Compute a point at a given distance on this rhumb line.
910 @arg s12: The distance along this rhumb line from its origin to the point
911 (C{meters}), can be negative.
912 @kwarg outmask: Bit-or'ed combination of L{Caps} values specifying the
913 quantities to be returned.
915 @return: L{GDict} with 4 to 8 items C{azi12, a12, s12, S12, lat2, lat1,
916 lon2, lon1} with latitude C{lat2} and longitude C{lon2} of the
917 point in C{degrees}, the rhumb angle C{a12} in C{degrees} from
918 the start point of and the area C{S12} under this rhumb line
919 in C{meter} I{squared}.
921 @raise ImportError: Package C{numpy} not found or not installed, required
922 only for L{RhumbLineAux} area C{S12} when C{B{exact}
923 is True}.
925 @note: If B{C{s12}} is large enough that the rhumb line crosses a pole, the
926 longitude of the second point is indeterminate and C{NAN} is returned
927 for C{lon2} and area C{S12}.
929 If the first point is a pole, the cosine of its latitude is taken to
930 be C{sqrt(L{EPS})}. This position is extremely close to the actual
931 pole and allows the calculation to be carried out in finite terms.
932 '''
933 return self._Position(self.m2degrees(s12), s12, outmask)
935 def _Position(self, a12, s12, outmask):
936 '''(INTERNAL) C{Arc-/Position} helper.
937 '''
938 r = GDict(azi12=self.azi12, a12=a12, s12=s12, name=self.name)
939 Cs = Caps
940 if (outmask & Cs.LATITUDE_LONGITUDE_AREA):
941 if a12 or s12:
942 mu12 = self._calp * a12
943 mu2 = self._mu1 + mu12
944 if fabs(mu2) > 90: # past pole
945 mu2 = _norm180(mu2) # reduce to [-180, 180)
946 if fabs(mu2) > 90: # point on anti-meridian
947 mu2 = _norm180(_loneg(mu2))
948 lat2 = self._mu2lat(mu2)
949 lon2 = S12 = NAN
950 else:
951 lat2, lon2, S1, S2 = self._Position4(a12, mu2, s12, mu12)
952 if (outmask & Cs.AREA):
953 S12 = self.rhumb._S12d(S1, S2, lon2)
954 S12 = unsigned0(S12) # like .gx
955# else:
956# S12 = None # unused
957 if (outmask & Cs.LONGITUDE):
958 if (outmask & Cs.LONG_UNROLL):
959 lon2 += self.lon1
960 else:
961 lon2 = _norm180(self._lon12 + lon2)
962 else: # coincident
963 lat2, lon2 = self.latlon1
964 S12 = _0_0
966 if (outmask & Cs.AREA):
967 r.set_(S12=S12)
968 if (outmask & Cs.LATITUDE):
969 r.set_(lat2=lat2, lat1=self.lat1)
970 if (outmask & Cs.LONGITUDE):
971 r.set_(lon2=lon2, lon1=self.lon1)
972 return r
974 def _Position4(self, a12, mu2, s12, mu12): # PYCHOK no cover
975 '''(INTERNAL) I{Must be overloaded}.'''
976 self._notOverloaded(a12, s12, mu2, mu12) # underOK=True
978 @Property_RO
979 def rhumb(self):
980 '''Get this rhumb line's rhumb (L{RhumbAux} or L{Rhumb}).
981 '''
982 return self._rhumb
984 def toStr(self, prec=6, sep=_COMMASPACE_, **unused): # PYCHOK signature
985 '''Return this C{RhumbLine} as string.
987 @kwarg prec: The C{float} precision, number of decimal digits (0..9).
988 Trailing zero decimals are stripped for B{C{prec}} values
989 of 1 and above, but kept for negative B{C{prec}} values.
990 @kwarg sep: Separator to join (C{str}).
992 @return: C{RhumbLine} (C{str}).
993 '''
994 d = dict(rhumb=self.rhumb, lat1=self.lat1, lon1=self.lon1,
995 azi12=self.azi12, exact=self.exact,
996 TMorder=self.TMorder, xTM=self.xTM)
997 return sep.join(pairs(itemsorted(d, asorted=False), prec=prec))
999 @property_RO
1000 def TMorder(self):
1001 '''Get this rhumb line's I{Transverse Mercator} order (C{int}, 4, 5, 6, 7 or 8).
1002 '''
1003 return self.rhumb.TMorder
1005 @Property_RO
1006 def xTM(self):
1007 '''Get this rhumb line's I{Transverse Mercator} projection (L{ExactTransverseMercator}
1008 if I{exact} and I{ellipsoidal}, otherwise L{KTransverseMercator} for C{TMorder}).
1009 '''
1010 E = self.ellipsoid
1011 # ExactTransverseMercator doesn't handle spherical earth models
1012 return _MODS.etm.ExactTransverseMercator(E) if self.exact and E.isEllipsoidal else \
1013 _MODS.ktm.KTransverseMercator(E, TMorder=self.TMorder)
1015 def _xTM3d(self, latlon0, z=INT0, V3d=Vector3d):
1016 '''(INTERNAL) C{xTM.forward} this C{latlon1} to C{V3d} with B{C{latlon0}}
1017 as current intersection estimate and central meridian.
1018 '''
1019 t = self.xTM.forward(self.lat1 - latlon0.lat, self.lon1, lon0=latlon0.lon)
1020 return V3d(t.easting, t.northing, z)
1023class _PseudoRhumbLine(RhumbLineBase):
1024 '''(INTERNAL) Pseudo-rhumb line for a geodesic (line), see C{geodesicw._PlumbTo}.
1025 '''
1026 def __init__(self, gl, name=NN):
1027 R = RhumbBase(gl.geodesic.ellipsoid, None, True, name)
1028 RhumbLineBase.__init__(self, R, gl.lat1, gl.lon1, 0, caps=Caps.LINE_OFF)
1029 self._azi1 = self.azi12 = gl.azi1
1030 self._gl = gl
1031 self._gD = gl.geodesic.Direct
1033 def PlumbTo(self, lat0, lon0, **exact_eps_est_tol): # PYCHOK signature
1034 P = RhumbLineBase.PlumbTo(self, lat0, lon0, **exact_eps_est_tol)
1035 z, P = _xkwds_pop2(P, azi12=None)
1036 P.set_(azi1=self._gl.azi1, azi2=z)
1037 return P # geodesic L{Position}
1039 def Position(self, s12, **unused): # PYCHOK signature
1040 r = self._gD(self.lat1, self.lon1, self._azi1, s12)
1041 self._azi1 = r.azi1
1042 self.azi12 = z = r.azi2
1043 self._salp, _ = sincos2d(z)
1044 return r.set_(azi12=z)
1047__all__ += _ALL_DOCS(RhumbBase, RhumbLineBase)
1049if __name__ == '__main__':
1051 from pygeodesy import printf, Rhumb as Rh, RhumbAux as Ah
1052 from pygeodesy.basics import _zip
1053 from pygeodesy.ellipsoids import _EWGS84
1055 Al = Ah(_EWGS84).Line(30, 0, 45)
1056 Rl = Rh(_EWGS84).Line(30, 0, 45)
1058 for i in range(1, 10):
1059 s = .5e6 + 1e6 / i
1060 a = Al.Position(s).lon2
1061 r = Rl.Position(s).lon2
1062 e = (fabs(a - r) / a) if a else 0
1063 printf('# Position.lon2 %.14f vs %.14f, diff %g', r, a, e)
1065 for exact in (None, False, True):
1066 for est in (None, 1e6):
1067 a = Al.PlumbTo(60, 0, exact=exact, est=est)
1068 r = Rl.PlumbTo(60, 0, exact=exact, est=est)
1069 printf('# %s, iteration=%s, exact=%s, est=%s\n# %s, iteration=%s',
1070 a.toRepr(), a.iteration, exact, est,
1071 r.toRepr(), r.iteration, nl=1)
1073 NE_=(71.688899882813, 0.2555198244234, 44095641862956.11)
1074 LHR=(77.7683897102557, 5771083.38332803, 37395209100030.39)
1075 NRT=(-92.38888798169965, 12782581.067684170, -63760642939072.50)
1077 def _ref(fmt, r3, x3):
1078 e3 = []
1079 for r, x in _zip(r3, x3): # strict=True
1080 e = fabs(r - x) / fabs(x)
1081 e3.append('%.g' % (e,))
1082 printf((fmt % r3) + ', rel errors: ' + ', '.join(e3))
1084 for R in (Ah, Rh): # <https://GeographicLib.SourceForge.io/cgi-bin/RhumbSolve -p 9> version 2.2
1085 rh = R(exact=True) # WGS84 default
1086 printf('# %r', rh, nl=1)
1087 r = rh.Direct8(40.6, -73.8, 51, 5.5e6) # from JFK about NE
1088 _ref('# JFK NE lat2=%.12f, lon2=%.12f, S12=%.1f', (r.lat2, r.lon2, r.S12), NE_)
1089 r = rh.Inverse8(40.6, -73.8, 51.6, -0.5) # JFK to LHR
1090 _ref('# JFK-LHR azi12=%.12f, s12=%.3f S12=%.1f', (r.azi12, r.s12, r.S12), LHR)
1091 r = rh.Inverse8(40.6, -73.8, 35.8, 140.3) # JFK to Tokyo Narita
1092 _ref('# JFK-NRT azi12=%.12f, s12=%.3f S12=%.1f', (r.azi12, r.s12, r.S12), NRT)
1094# % python3.10 -m pygeodesy3.rhumb.Bases
1096# Position.lon2 11.61455846901637 vs 11.61455846901637, diff 3.05885e-16
1097# Position.lon2 7.58982302826842 vs 7.58982302826842, diff 2.34045e-16
1098# Position.lon2 6.28526067416369 vs 6.28526067416369, diff 2.82623e-16
1099# Position.lon2 5.63938995325146 vs 5.63938995325146, diff 1.57495e-16
1100# Position.lon2 5.25385527435707 vs 5.25385527435707, diff 0
1101# Position.lon2 4.99764604290380 vs 4.99764604290380, diff 8.88597e-16
1102# Position.lon2 4.81503363740473 vs 4.81503363740473, diff 1.84459e-16
1103# Position.lon2 4.67828821748836 vs 4.67828821748835, diff 5.69553e-16
1104# Position.lon2 4.57205667906283 vs 4.57205667906283, diff 5.82787e-16
1106# Intersection(a02=17.798332, a12=19.521356, at=90.0, azi02=135.0, azi12=45.0, lat0=60.0, lat1=30.0, lat2=45.0, lon0=0.0, lon1=0.0, lon2=15.830286, name='Intersection', s02=1977981.142985, s12=2169465.957531), iteration=9, exact=None, est=None
1107# Intersection(a02=17.798332, a12=19.521356, at=90.0, azi02=135.0, azi12=45.0, lat0=60.0, lat1=30.0, lat2=45.0, lon0=0.0, lon1=0.0, lon2=15.830286, name='Intersection', s02=1977981.142985, s12=2169465.957531), iteration=9
1109# Intersection(a02=17.798332, a12=19.521356, at=90.0, azi02=135.0, azi12=45.0, lat0=60.0, lat1=30.0, lat2=45.0, lon0=0.0, lon1=0.0, lon2=15.830286, name='Intersection', s02=1977981.142985, s12=2169465.957531), iteration=9, exact=None, est=1000000.0
1110# Intersection(a02=17.798332, a12=19.521356, at=90.0, azi02=135.0, azi12=45.0, lat0=60.0, lat1=30.0, lat2=45.0, lon0=0.0, lon1=0.0, lon2=15.830286, name='Intersection', s02=1977981.142985, s12=2169465.957531), iteration=9
1112# PlumbTo(a02=17.967658, a12=27.74256, at=90.0, azi0=113.73626, azi12=45.0, lat0=60, lat1=30.0, lat2=49.634582, lon0=0, lon1=0.0, lon2=25.767876, name='PlumbTo', s02=1997960.116871, s12=3083112.636236), iteration=5, exact=False, est=None
1113# PlumbTo(a02=17.967658, a12=27.74256, at=90.0, azi0=113.73626, azi12=45.0, lat0=60, lat1=30.0, lat2=49.634582, lon0=0, lon1=0.0, lon2=25.767876, name='PlumbTo', s02=1997960.116871, s12=3083112.636236), iteration=5
1115# PlumbTo(a02=17.967658, a12=27.74256, at=90.0, azi0=113.73626, azi12=45.0, lat0=60, lat1=30.0, lat2=49.634582, lon0=0, lon1=0.0, lon2=25.767876, name='PlumbTo', s02=1997960.116871, s12=3083112.636236), iteration=7, exact=False, est=1000000.0
1116# PlumbTo(a02=17.967658, a12=27.74256, at=90.0, azi0=113.73626, azi12=45.0, lat0=60, lat1=30.0, lat2=49.634582, lon0=0, lon1=0.0, lon2=25.767876, name='PlumbTo', s02=1997960.116871, s12=3083112.636236), iteration=7
1118# PlumbTo(a02=17.967658, a12=27.74256, at=90.0, azi0=113.73626, azi12=45.0, lat0=60, lat1=30.0, lat2=49.634582, lon0=0, lon1=0.0, lon2=25.767876, name='PlumbTo', s02=1997960.116871, s12=3083112.636236), iteration=5, exact=True, est=None
1119# PlumbTo(a02=17.967658, a12=27.74256, at=90.0, azi0=113.73626, azi12=45.0, lat0=60, lat1=30.0, lat2=49.634582, lon0=0, lon1=0.0, lon2=25.767876, name='PlumbTo', s02=1997960.116871, s12=3083112.636236), iteration=5
1121# PlumbTo(a02=17.967658, a12=27.74256, at=90.0, azi0=113.73626, azi12=45.0, lat0=60, lat1=30.0, lat2=49.634582, lon0=0, lon1=0.0, lon2=25.767876, name='PlumbTo', s02=1997960.116871, s12=3083112.636236), iteration=7, exact=True, est=1000000.0
1122# PlumbTo(a02=17.967658, a12=27.74256, at=90.0, azi0=113.73626, azi12=45.0, lat0=60, lat1=30.0, lat2=49.634582, lon0=0, lon1=0.0, lon2=25.767876, name='PlumbTo', s02=1997960.116871, s12=3083112.636236), iteration=7
1124# RhumbAux(RAorder=None, TMorder=6, ellipsoid=Ellipsoid(name='WGS84', a=6378137, b=6356752.31424518, f_=298.25722356, f=0.00335281, f2=0.00336409, n=0.00167922, e=0.08181919, e2=0.00669438, e21=0.99330562, e22=0.0067395, e32=0.00335843, A=6367449.14582341, L=10001965.72931272, R1=6371008.77141506, R2=6371007.18091847, R3=6371000.79000916, Rbiaxial=6367453.63451633, Rtriaxial=6372797.5559594), exact=True)
1125# JFK NE lat2=71.688899882813, lon2=0.255519824423, S12=44095641862956.1, rel errors: 4e-16, 2e-13, 4e-16
1126# JFK-LHR azi12=77.768389710256, s12=5771083.383 S12=37395209100030.4, rel errors: 5e-16, 3e-16, 8e-16
1127# JFK-NRT azi12=-92.388887981700, s12=12782581.068 S12=-63760642939072.5, rel errors: 0, 1e-16, 7e-16
1129# Rhumb(RAorder=6, TMorder=6, ellipsoid=Ellipsoid(name='WGS84', a=6378137, b=6356752.31424518, f_=298.25722356, f=0.00335281, f2=0.00336409, n=0.00167922, e=0.08181919, e2=0.00669438, e21=0.99330562, e22=0.0067395, e32=0.00335843, A=6367449.14582341, L=10001965.72931272, R1=6371008.77141506, R2=6371007.18091847, R3=6371000.79000916, Rbiaxial=6367453.63451633, Rtriaxial=6372797.5559594), exact=True)
1130# JFK NE lat2=71.688899882813, lon2=0.255519824423, S12=44095641862956.1, rel errors: 2e-16, 1e-13, 5e-16
1131# JFK-LHR azi12=77.768389710256, s12=5771083.383 S12=37395209100030.4, rel errors: 4e-16, 3e-16, 6e-16
1132# JFK-NRT azi12=-92.388887981700, s12=12782581.068 S12=-63760642939072.5, rel errors: 0, 1e-16, 1e-16
1134# **) MIT License
1135#
1136# Copyright (C) 2022-2025 -- mrJean1 at Gmail -- All Rights Reserved.
1137#
1138# Permission is hereby granted, free of charge, to any person obtaining a
1139# copy of this software and associated documentation files (the "Software"),
1140# to deal in the Software without restriction, including without limitation
1141# the rights to use, copy, modify, merge, publish, distribute, sublicense,
1142# and/or sell copies of the Software, and to permit persons to whom the
1143# Software is furnished to do so, subject to the following conditions:
1144#
1145# The above copyright notice and this permission notice shall be included
1146# in all copies or substantial portions of the Software.
1147#
1148# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
1149# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
1150# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
1151# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
1152# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
1153# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
1154# OTHER DEALINGS IN THE SOFTWARE.