pygeodesy 24.3.24__py2.py3-none-any.whl

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