pygeodesy 25.11.5__py2.py3-none-any.whl → 25.12.12__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 (125) hide show
  1. pygeodesy/__init__.py +25 -12
  2. pygeodesy/__main__.py +1 -1
  3. pygeodesy/albers.py +1 -1
  4. pygeodesy/angles.py +960 -0
  5. pygeodesy/auxilats/_CX_4.py +1 -1
  6. pygeodesy/auxilats/_CX_6.py +1 -1
  7. pygeodesy/auxilats/_CX_8.py +1 -1
  8. pygeodesy/auxilats/_CX_Rs.py +1 -1
  9. pygeodesy/auxilats/__init__.py +2 -2
  10. pygeodesy/auxilats/__main__.py +1 -1
  11. pygeodesy/auxilats/auxAngle.py +7 -8
  12. pygeodesy/auxilats/auxDLat.py +1 -1
  13. pygeodesy/auxilats/auxDST.py +1 -1
  14. pygeodesy/auxilats/auxLat.py +1 -1
  15. pygeodesy/auxilats/auxily.py +1 -1
  16. pygeodesy/azimuthal.py +6 -5
  17. pygeodesy/basics.py +14 -10
  18. pygeodesy/booleans.py +1 -1
  19. pygeodesy/cartesianBase.py +7 -7
  20. pygeodesy/clipy.py +1 -1
  21. pygeodesy/constants.py +27 -24
  22. pygeodesy/css.py +1 -1
  23. pygeodesy/datums.py +1 -1
  24. pygeodesy/deprecated/__init__.py +1 -1
  25. pygeodesy/deprecated/bases.py +1 -1
  26. pygeodesy/deprecated/classes.py +14 -7
  27. pygeodesy/deprecated/consterns.py +1 -1
  28. pygeodesy/deprecated/datum.py +1 -1
  29. pygeodesy/deprecated/functions.py +1 -1
  30. pygeodesy/deprecated/nvector.py +1 -1
  31. pygeodesy/deprecated/rhumbBase.py +1 -1
  32. pygeodesy/deprecated/rhumbaux.py +1 -1
  33. pygeodesy/deprecated/rhumbsolve.py +1 -1
  34. pygeodesy/deprecated/rhumbx.py +1 -1
  35. pygeodesy/dms.py +1 -1
  36. pygeodesy/ecef.py +1 -1
  37. pygeodesy/ecefLocals.py +1 -1
  38. pygeodesy/elevations.py +1 -1
  39. pygeodesy/ellipsoidalBase.py +1 -1
  40. pygeodesy/ellipsoidalBaseDI.py +1 -1
  41. pygeodesy/ellipsoidalExact.py +1 -1
  42. pygeodesy/ellipsoidalGeodSolve.py +1 -1
  43. pygeodesy/ellipsoidalKarney.py +1 -1
  44. pygeodesy/ellipsoidalNvector.py +1 -1
  45. pygeodesy/ellipsoidalVincenty.py +1 -1
  46. pygeodesy/ellipsoids.py +7 -6
  47. pygeodesy/elliptic.py +1 -1
  48. pygeodesy/epsg.py +1 -1
  49. pygeodesy/errors.py +8 -4
  50. pygeodesy/etm.py +1 -1
  51. pygeodesy/fmath.py +15 -8
  52. pygeodesy/formy.py +107 -5
  53. pygeodesy/frechet.py +1 -1
  54. pygeodesy/fstats.py +1 -1
  55. pygeodesy/fsums.py +1 -1
  56. pygeodesy/gars.py +1 -1
  57. pygeodesy/geod3solve.py +488 -0
  58. pygeodesy/geodesici.py +4 -4
  59. pygeodesy/geodesicw.py +1 -1
  60. pygeodesy/geodesicx/_C4_24.py +1 -1
  61. pygeodesy/geodesicx/_C4_27.py +1 -1
  62. pygeodesy/geodesicx/_C4_30.py +1 -1
  63. pygeodesy/geodesicx/__init__.py +1 -1
  64. pygeodesy/geodesicx/__main__.py +1 -1
  65. pygeodesy/geodesicx/gx.py +1 -1
  66. pygeodesy/geodesicx/gxarea.py +1 -1
  67. pygeodesy/geodesicx/gxbases.py +1 -1
  68. pygeodesy/geodesicx/gxline.py +1 -1
  69. pygeodesy/geodsolve.py +70 -102
  70. pygeodesy/geohash.py +1 -1
  71. pygeodesy/geoids.py +1 -1
  72. pygeodesy/hausdorff.py +1 -1
  73. pygeodesy/heights.py +1 -1
  74. pygeodesy/internals.py +1 -1
  75. pygeodesy/interns.py +3 -3
  76. pygeodesy/iters.py +1 -1
  77. pygeodesy/karney.py +132 -116
  78. pygeodesy/ktm.py +1 -1
  79. pygeodesy/latlonBase.py +1 -1
  80. pygeodesy/lazily.py +23 -12
  81. pygeodesy/lcc.py +1 -1
  82. pygeodesy/ltp.py +1 -1
  83. pygeodesy/ltpTuples.py +1 -1
  84. pygeodesy/mgrs.py +3 -3
  85. pygeodesy/named.py +14 -9
  86. pygeodesy/namedTuples.py +1 -1
  87. pygeodesy/nvectorBase.py +1 -1
  88. pygeodesy/osgr.py +1 -1
  89. pygeodesy/points.py +1 -1
  90. pygeodesy/props.py +1 -1
  91. pygeodesy/resections.py +1 -1
  92. pygeodesy/rhumb/__init__.py +8 -6
  93. pygeodesy/rhumb/aux_.py +1 -1
  94. pygeodesy/rhumb/bases.py +1 -1
  95. pygeodesy/rhumb/ekx.py +1 -1
  96. pygeodesy/rhumb/solve.py +91 -84
  97. pygeodesy/simplify.py +1 -1
  98. pygeodesy/solveBase.py +72 -49
  99. pygeodesy/sphericalBase.py +1 -1
  100. pygeodesy/sphericalNvector.py +1 -1
  101. pygeodesy/sphericalTrigonometry.py +1 -1
  102. pygeodesy/streprs.py +6 -4
  103. pygeodesy/trf.py +1 -1
  104. pygeodesy/triaxials/__init__.py +70 -0
  105. pygeodesy/triaxials/bases.py +935 -0
  106. pygeodesy/triaxials/conformal3.py +617 -0
  107. pygeodesy/triaxials/triaxial3.py +969 -0
  108. pygeodesy/{triaxials.py → triaxials/triaxial5.py} +353 -781
  109. pygeodesy/units.py +1 -1
  110. pygeodesy/unitsBase.py +1 -1
  111. pygeodesy/ups.py +2 -3
  112. pygeodesy/utily.py +17 -14
  113. pygeodesy/utm.py +1 -1
  114. pygeodesy/utmups.py +1 -1
  115. pygeodesy/utmupsBase.py +1 -1
  116. pygeodesy/vector2d.py +1 -1
  117. pygeodesy/vector3d.py +1 -1
  118. pygeodesy/vector3dBase.py +1 -1
  119. pygeodesy/webmercator.py +1 -1
  120. pygeodesy/wgrs.py +1 -1
  121. {pygeodesy-25.11.5.dist-info → pygeodesy-25.12.12.dist-info}/METADATA +12 -12
  122. pygeodesy-25.12.12.dist-info/RECORD +125 -0
  123. pygeodesy-25.11.5.dist-info/RECORD +0 -119
  124. {pygeodesy-25.11.5.dist-info → pygeodesy-25.12.12.dist-info}/WHEEL +0 -0
  125. {pygeodesy-25.11.5.dist-info → pygeodesy-25.12.12.dist-info}/top_level.txt +0 -0
pygeodesy/angles.py ADDED
@@ -0,0 +1,960 @@
1
+
2
+ # -*- coding: utf-8 -*-
3
+
4
+ u'''Classes L{Ang}, L{Deg}, L{Rad} and L{Lambertian} accurately representing an angle
5
+ as a 3-tuple C{(sine, cosine, turns)}, with C{turns} the number of full turns.
6
+
7
+ Transcoded to pure Python from I{Karney}'s GeographicLib 2.7 C++ class U{AngleT
8
+ <https://GeographicLib.SourceForge.io/C++/doc/classGeographicLib_1_1AngleT.html>}.
9
+
10
+ Copyright (C) U{Charles Karney <mailto:Karney@Alum.MIT.edu>} (2024-2025) and licensed
11
+ under the MIT/X11 License. For more information, see the U{GeographicLib 2.7
12
+ <https://GeographicLib.SourceForge.io/>} documentation.
13
+ '''
14
+ # make sure int/int division yields float quotient, see .basics
15
+ from __future__ import division as _; del _ # noqa: E702 ;
16
+
17
+ from pygeodesy.basics import _copysign, map1, signBit, _signOf
18
+ from pygeodesy.constants import EPS, EPS0, NAN, PI2, _0_0, _N_0_0, \
19
+ _0_25, _1_0, _N_1_0, _4_0, _360_0, \
20
+ _copysign_0_0, _copysign_1_0, \
21
+ _flipsign, float_, _isfinite, \
22
+ _over, _pos_self, remainder
23
+ from pygeodesy.errors import _xkwds, _xkwds_get, _xkwds_pop2
24
+ from pygeodesy.fmath import hypot, _ALL_LAZY, _MODS
25
+ # from pygeodesy.interns import _COMMASPACE_ # from .streprs
26
+ # from pygeodesy.lazily import _ALL_LAZY, _ALL_MODS as _MODS # from .fmath
27
+ from pygeodesy.named import _Named, _NamedTuple, _Pass
28
+ from pygeodesy.props import Property_RO, property_doc_, property_RO, \
29
+ _allPropertiesOf_n, _update_all
30
+ from pygeodesy.streprs import Fmt, fstr, unstr, _COMMASPACE_
31
+ from pygeodesy.units import Degrees, _isDegrees, _isRadians, Radians
32
+ from pygeodesy.utily import atan2, atan2d, sincos2, sincos2d, SinCos2
33
+
34
+ from math import asinh, ceil as _ceil, fabs, floor as _floor, \
35
+ isinf, isnan, sinh
36
+
37
+ __all__ = _ALL_LAZY.angles
38
+ __version__ = '25.12.02'
39
+
40
+ _EPS03 = EPS / (1 << 20)
41
+ # _HD = _180_0
42
+ # _QD = _90_0
43
+ # _TD = _360_0
44
+ # _DM = _SM = _60_0
45
+ # _DS = _3600_0
46
+ _ZRND = _1_0 / 1024
47
+
48
+ _CARDINAL2 = {-2: (_N_0_0, _N_1_0),
49
+ -1: (_N_1_0, _0_0),
50
+ 1: ( _1_0, _0_0),
51
+ 2: ( _0_0, _N_1_0)}.get
52
+
53
+
54
+ def _fint(f):
55
+ # float of C{int(f)} preserving signed C{0}.
56
+ i = int(f)
57
+ return float_(i) if i else _copysign_0_0(f)
58
+
59
+
60
+ def _ncardinal(s, c, n):
61
+ if n:
62
+ n *= _4_0
63
+ i = (1 if (-c) < fabs(s) else 2) if signBit(c) else \
64
+ (1 if c < fabs(s) else 0)
65
+ if i:
66
+ n += _copysign(i, s)
67
+ return n
68
+
69
+
70
+ def _normalize2(s, c):
71
+ h = hypot(s, c)
72
+ if _isfinite(h):
73
+ sc = ((s / h), (c / h)) if h else (
74
+ # If y is +/-0 and x = -0, +/-pi is returned,
75
+ # or y is +/-0 and x = +0, +/-0 is returned,
76
+ # so, retain the sign of s = +/-0
77
+ _orthogonal2(False, s, c))
78
+ elif isnan(h) or (isinf(s) and isinf(c)):
79
+ sc = NAN, NAN
80
+ else:
81
+ sc = _orthogonal2(isinf(s), s, c)
82
+ return sc
83
+
84
+
85
+ def _other(x, unit=Radians, **unused):
86
+ # get C{x} as C{Ang} from C{Degrees}, C{Radians} or C{Lambertian}
87
+ return Ang.fromLambertian(x) if unit is Lambertian else (
88
+ Ang.fromRadians(x) if _isRadians(x, iscalar=unit is Radians) else (
89
+ Ang.fromDegrees(x) if _isDegrees(x, iscalar=unit is Degrees) else
90
+ _raiseError(unit, x))) # PYCHOK indent
91
+
92
+
93
+ def _orthogonal2(pred, s, c):
94
+ return (_copysign_1_0(s), _copysign_0_0(c)) if pred else \
95
+ (_copysign_0_0(s), _copysign_1_0(c))
96
+
97
+
98
+ def _raiseError(unit, arg, **kwds):
99
+ raise TypeError(unstr(unit, arg, **kwds))
100
+
101
+
102
+ def _rnd(x):
103
+ w = _ZRND - fabs(x)
104
+ if w > 0:
105
+ x = _copysign(_ZRND - w, x)
106
+ return x
107
+
108
+
109
+ def _scnu4(s, c, n, unit=Radians, **unused): # unit=Ang._unit
110
+ s, c, n = map1(float, s, c, n)
111
+ return _normalize2(s, c) + (n, unit)
112
+
113
+
114
+ class Ang(_Named):
115
+ '''An accurate representation of angles, as 3-tuple C{(s, c, n)}.
116
+
117
+ This class represents an angle via its sine C{s}, cosine C{c} and
118
+ the number of full turns C{n}. The angle is then C{atan2(s, c) +
119
+ n * PI2}. This representation offers several advantages:
120
+
121
+ - cardinal directions (multiples of 90 degrees) are exactly represented
122
+ (a benefit shared by representing angles as degrees)
123
+
124
+ - angles very close to any cardinal direction are accurately represented
125
+
126
+ - there's no loss of precision with large angles (outside the "normal"
127
+ range [-180, +180])
128
+
129
+ - various operations, such as adding a multiple of 90 degrees to an
130
+ angle are performed exactly.
131
+
132
+ @note: B{C{n}} is a C{float}, this allows it to be NAN, INF or NINF.
133
+ '''
134
+ _unit = Radians # see _scnu4
135
+
136
+ def __init__(self, s_ang=0, c=None, n=0, normal=True, **unit_name):
137
+ '''New L{Ang}.
138
+
139
+ @kwarg s_ang: A previous L{Ang}, C{Degrees}, C{Radians} if C{B{c}
140
+ is None}, otherwise the sine component (C{float}).
141
+ @kwarg c: The cosine component (C{float}) iff C{not None}.
142
+ @kwarg n: The number of L{PI2} turns (C{float}).
143
+ @kwarg normal: If C{True}, B{C{s}} and B{C{c}} are normalized, i.e.
144
+ on the unit circle (C{boo}).
145
+ @kwarg unit_name: Type C{B{unit}=}L{Radians} or L{Degrees} of scalar
146
+ scalar values (L{Degrees} or L{Radians}).
147
+
148
+ @note: Either B{C{s}} or B{C{c}} can be INF or NINF, but not both.
149
+
150
+ @note: By default, the point B{C{(s, c)}} is scaled to lie on the
151
+ unit circle.
152
+ '''
153
+ s, c, n, u = s_ang.scnu4 if isAng(s_ang) else (
154
+ _other(s_ang, **unit_name).scnu4 if c is None else
155
+ _scnu4(s_ang, c, n, **unit_name))
156
+ if unit_name:
157
+ u, name = _xkwds_pop2(unit_name, unit=u)
158
+ if name:
159
+ self.name = name
160
+ self._n = _fint(n)
161
+ self._s, self._c = (s, c) if normal else _normalize2(s, c)
162
+ self.unit = u
163
+
164
+ def __abs__(self):
165
+ s, _ = self._float2()
166
+ return self._float1(fabs(s))
167
+
168
+ def __add__(self, other):
169
+ return self.copy().__iadd__(other)
170
+
171
+ def __bool__(self): # PYCHOK Python 3+
172
+ s, c, n = self.scn3
173
+ return bool(s or c or n)
174
+
175
+ # def __call__(self, *args, **kwds): # PYCHOK no cover
176
+ # return self._NotImplemented(*args, **kwds)
177
+
178
+ def __ceil__(self): # PYCHOK not special in Python 2-
179
+ s, _ = self._float2()
180
+ return self._float1(_ceil(s))
181
+
182
+ def __cmp__(self, other): # PYCHOK no cover
183
+ s, r = self._float2(other)
184
+ return _signOf(s, r) # -1, 0, +1
185
+
186
+ def __divmod__(self, other):
187
+ s, r = self._float2(other)
188
+ q, r = divmod(s, r)
189
+ return q, self._float1(r)
190
+
191
+ def __eq__(self, other):
192
+ s, r = self._float2(other)
193
+ return fabs(s - r) < EPS0
194
+
195
+ def __float__(self):
196
+ u = self.unit
197
+ return self.radians if u is Radians else (
198
+ self.degrees if u is Degrees else (
199
+ self.lambertian if u is Lambertian else
200
+ _raiseError(float, u))) # PYCHOK indent
201
+
202
+ def __floor__(self): # PYCHOK not special in Python 2-
203
+ s, _ = self._float2()
204
+ return self._float1(_floor(s))
205
+
206
+ def __floordiv__(self, other):
207
+ return self.copy().__ifloordiv__(other)
208
+
209
+ # def __format__(self, *other): # PYCHOK no cover
210
+ # return self._NotImplemented(self, *other)
211
+
212
+ def __ge__(self, other):
213
+ s, r = self._float2(other)
214
+ return s >= r
215
+
216
+ def __gt__(self, other):
217
+ s, r = self._float2(other)
218
+ return s > r
219
+
220
+ def __hash__(self): # PYCHOK no cover
221
+ # @see: U{Notes for type implementors<https://docs.Python.org/
222
+ # 3/library/numbers.html#numbers.Rational>}
223
+ return hash(self.scn3) # tuple.__hash__()
224
+
225
+ def __iadd__(self, other):
226
+ p = self._other(other)
227
+ q = p.ncardinal + self.ncardinal
228
+ s, c, n = self.scn3
229
+ s, c = _normalize2(s * p.c + c * p.s,
230
+ c * p.c - s * p.s)
231
+ q -= _ncardinal(s, c, n)
232
+ n = _fint(q * _0_25) + p.n
233
+ if n:
234
+ self._n += n
235
+ self._s = s
236
+ self._c = c
237
+ _update_all(self)
238
+ return self._update(s, c)
239
+
240
+ def __ifloordiv__(self, other):
241
+ s, r = self._float2(other)
242
+ return self._ifloat(s // r)
243
+
244
+ def __imatmul__(self, other): # PYCHOK no cover
245
+ return self._notImplemented()
246
+
247
+ def __imod__(self, other):
248
+ s, r = self._float2(other)
249
+ return self._ifloat(s % r)
250
+
251
+ def __imul__(self, other):
252
+ s, r = self._float2(other)
253
+ return self._ifloat(s * r)
254
+
255
+ def __int__(self):
256
+ s, _ = self._float2(0)
257
+ return int(s)
258
+
259
+ def __invert__(self): # PYCHOK no cover
260
+ # Luciano Ramalho, "Fluent Python", O'Reilly, 2nd Ed, 2022 p. 567
261
+ return self._notImplemented()
262
+
263
+ def __ipow__(self, other, *mod): # PYCHOK 2 vs 3 args
264
+ s, r = self._float2(other)
265
+ return self._ifloat(pow(s, r, *mod))
266
+
267
+ def __isub__(self, other):
268
+ return self.__iadd__(-other)
269
+
270
+ # def __iter__(self):
271
+ # '''
272
+ # return self._NotImplemented()
273
+
274
+ def __itruediv__(self, other):
275
+ s, r = self._float2(other)
276
+ return self._ifloat(s / r)
277
+
278
+ def __le__(self, other):
279
+ s, r = self._float2(other)
280
+ return s <= r
281
+
282
+ def __lt__(self, other):
283
+ s, r = self._float2(other)
284
+ return s < r
285
+
286
+ def __matmul__(self, other): # PYCHOK no cover
287
+ return self._notImplemented(other)
288
+
289
+ def __mod__(self, other):
290
+ s, r = self._float2(other)
291
+ return self._float1(s % r)
292
+
293
+ def __mul__(self, other):
294
+ return self.copy().__imul__(other)
295
+
296
+ def __ne__(self, other):
297
+ return not self.__eq__(other)
298
+
299
+ def __neg__(self):
300
+ s, c, n = self.scn3
301
+ s, n = _flipsign(s), _flipsign(n)
302
+ return self._Ang(s, c, n) # normal=True
303
+
304
+ def __pos__(self):
305
+ return self if _pos_self else self.copy()
306
+
307
+ def __pow__(self, other, *mod): # PYCHOK 2 vs 3 args
308
+ return self.copy().__ipow__(other, *mod)
309
+
310
+ def __radd__(self, other):
311
+ return self._other(other) + self
312
+
313
+ def __rdivmod__(self, other):
314
+ return divmod(self._other(other), self)
315
+
316
+ def __repr__(self):
317
+ return self.toRepr()
318
+
319
+ def __rfloordiv__(self, other):
320
+ return self._other(other) // self
321
+
322
+ def __rmatmul__(self, other): # PYCHOK no cover
323
+ return self._notImplemented(self, other)
324
+
325
+ def __rmod__(self, other):
326
+ return self._other(other) % self
327
+
328
+ def __rmul__(self, other):
329
+ return self._other(other) * self
330
+
331
+ def __round__(self, *ndigits): # PYCHOK Python 3+
332
+ return self.round(*ndigits)
333
+
334
+ def __rpow__(self, other, *mod):
335
+ return pow(self._other(other), self, *mod)
336
+
337
+ def __rsub__(self, other):
338
+ return self._other(other) - self
339
+
340
+ def __rtruediv__(self, other):
341
+ return self._other(other) / self
342
+
343
+ def __str__(self):
344
+ return self.toStr(0) # ignore turns
345
+
346
+ def __sub__(self, other):
347
+ return self.copy().__isub__(other)
348
+
349
+ def __truediv__(self, other):
350
+ return self.copy().__itruediv__(other)
351
+
352
+ __trunc__ = __int__
353
+
354
+ if _MODS.sys_version_info2 < (3, 0): # PYCHOK no cover
355
+ # <https://docs.Python.org/2/library/operator.html#mapping-operators-to-functions>
356
+ __div__ = __truediv__
357
+ __idiv__ = __itruediv__
358
+ __long__ = __int__
359
+ __nonzero__ = __bool__
360
+ __rdiv__ = __rtruediv__
361
+
362
+ def _Ang(self, s, *cn, **normal_unit_name):
363
+ # return an C{Ang} like C{self}
364
+ return Ang(s, *cn, **self._kwds(normal_unit_name))
365
+
366
+ def base(self, *center):
367
+ '''Return this C{Angle}'s base, optionally centered.
368
+ '''
369
+ r = self.copy()
370
+ if center:
371
+ c = self._other(center[0])
372
+ b = self - c
373
+ b = b.base()
374
+ b += c
375
+ r.n0 = b.n0
376
+ else:
377
+ r.n = 0
378
+ return r
379
+
380
+ @property_RO
381
+ def c(self):
382
+ '''Get the cosine of this C{Angle} (C{float}).
383
+ '''
384
+ return self._c
385
+
386
+ @staticmethod
387
+ def cardinal(q=0, **unit_name):
388
+ '''A cardinal direction.
389
+
390
+ @kwarg q: The number of I{quarter} turns (C{scalar}).
391
+
392
+ @return: An C{Ang} equivalent to B{C{q}} quarter turns.
393
+
394
+ @note: B{C{q}} is truncated to an integer and signed
395
+ C{0} is distinguished. C{Ang.NAN} is returned
396
+ if B{C{q}} is not finite.
397
+ '''
398
+ if _isfinite(q):
399
+ if q:
400
+ q = _fint(q)
401
+ i = int(remainder(q, _4_0)) # i is in [-2, 2]
402
+ n = _fint((q - i) * _0_25)
403
+ s, c = _CARDINAL2(i, ((_0_0 if q else q), _1_0))
404
+ t = s is not q
405
+ else:
406
+ s, c, n, t = _copysign_0_0(q), 1, 0, True
407
+ r = Ang(s, c, n, normal=t, **unit_name)
408
+ else:
409
+ r = Ang.NAN(**unit_name)
410
+ return r
411
+
412
+ def copy(self, **unit_name): # PYCHOK signature
413
+ '''Return a copy of this C{Ang}.
414
+ '''
415
+ return self._Ang(self, **self._kwds(unit_name))
416
+
417
+ @Property_RO
418
+ def degrees(self):
419
+ '''Get this C{Ang} in C{degrees}.
420
+ '''
421
+ d = self.degrees0
422
+ if self.n:
423
+ d += self.n * _360_0
424
+ return d # XXX Degrees(d, self.name)
425
+
426
+ @Property_RO
427
+ def degrees0(self):
428
+ '''Get this C{Ang} in C{degrees} ignoring the turns.
429
+ '''
430
+ return atan2d(*self.sc2) # XXX Degrees(d, self.name)
431
+
432
+ divmod = __divmod__
433
+
434
+ @staticmethod
435
+ def EPS0(**unit_name):
436
+ '''Get a tiny C{Ang}.
437
+
438
+ @note: This allows angles extremely close to the cardinal
439
+ directions to be generated. The C{.round} method
440
+ will flush this angle to C{0}.
441
+ '''
442
+ return Ang(_EPS03, 1, **unit_name)
443
+
444
+ @staticmethod
445
+ def _flip(bet, omg, alp=None):
446
+ '''(INTERNAL) Reflect C{bet}, C{omg} and C{alp} inplace.
447
+ ''' # Ellipsoid3.Flip
448
+ bet.reflect(flipc=True)
449
+ omg.reflect(flips=True)
450
+ if alp:
451
+ alp.reflect(flips=True, flipc=True)
452
+
453
+ def flipsign(self, mul=-1, **name):
454
+ '''Copy this C{Ang} with sign flipped.
455
+ '''
456
+ r = (-self) if signBit(mul) else self
457
+ return self._Ang(r, **name) if name else r
458
+
459
+ def _float1(self, f, **name):
460
+ # return C{f} as C{Ang} in this C{unit}
461
+ return _Ang_from[self.unit](f, **name)
462
+
463
+ def _float2(self, other=None):
464
+ # get self and C{other} as floats
465
+ r = other if other is None or isinstance(other, int) else \
466
+ float(_Ang_from[self.unit](other))
467
+ return float(self), r
468
+
469
+ def _ifloat(self, f): # PYCHOK expected
470
+ # set self to C{f} degrees or radians
471
+ scn = self._float1(f).scn3
472
+ self._s, self._c, self._n = scn
473
+ return self._update()
474
+
475
+ @staticmethod
476
+ def fromDegrees(deg, **unit_name):
477
+ '''Get an C{Ang} from degrees.
478
+ '''
479
+ if isAng(deg):
480
+ s, c, n = deg.scn3
481
+ d = deg.degrees0
482
+ elif _isDegrees(deg, iscalar=True):
483
+ s, c = sincos2d(deg)
484
+ d = atan2d(s, c)
485
+ n = round((deg - d) / _360_0)
486
+ else:
487
+ _raiseError(Ang.fromDegrees, deg, **unit_name)
488
+ a = Ang(s, c, n, **_xkwds(unit_name, unit=Degrees))
489
+ a.__dict__.update(degrees0=d) # Property_RO
490
+ return a
491
+
492
+ @staticmethod
493
+ def fromLambertian(psi, **unit_name):
494
+ '''Get an C{Ang} from C{lamberterian} radians.
495
+ '''
496
+ s = psi.lambertian if isAng(psi) else sinh(psi)
497
+ return Ang(s, 1, normal=False, **_xkwds(unit_name, unit=Lambertian))
498
+
499
+ @staticmethod
500
+ def fromRadians(rad, **unit_name):
501
+ '''Get an C{Ang} from radians.
502
+ '''
503
+ if isAng(rad):
504
+ s, c, n = rad.scn3
505
+ r = rad.radians0
506
+ elif _isRadians(rad, iscalar=True):
507
+ s, c = sincos2(rad)
508
+ r = atan2(s, c)
509
+ n = round((rad - r) / PI2)
510
+ else:
511
+ _raiseError(Ang.fromRadians, rad, **unit_name)
512
+ a = Ang(s, c, n, **_xkwds(unit_name, unit=Radians))
513
+ a.__dict__.update(radians0=r) # Property_RO
514
+ return a
515
+
516
+ @staticmethod
517
+ def fromScalar(ang, **unit_name):
518
+ '''Get an C{Ang} from C{Degrees}, C{Radians} or another C{Ang}.
519
+ '''
520
+ if isAng(ang):
521
+ r = Ang(ang, **_xkwds(unit_name, unit=ang.unit))
522
+ else:
523
+ u = _xkwds_get(unit_name, unit=None)
524
+ if u is Lambertian:
525
+ r = Ang.fromLambertian(ang, **unit_name)
526
+ elif _isDegrees(ang, iscalar=u is Degrees):
527
+ r = Ang.fromDegrees(ang, **unit_name)
528
+ elif _isRadians(ang, iscalar=u is Radians):
529
+ r = Ang.fromRadians(ang, **unit_name)
530
+ else:
531
+ _raiseError(Ang.fromScalar, ang, **unit_name)
532
+ return r
533
+
534
+ def is_integer(self, *n):
535
+ '''Is this C{Ang}'s degrees C{integer}? (C{bool}).
536
+ '''
537
+ return self.toDegrees(*n).is_integer()
538
+
539
+ def isnear0(self, eps0=EPS0): # aka zerop
540
+ '''Is this C{Ang} near C{0} within a tolerance?
541
+ '''
542
+ s, c, n = self.scn3
543
+ return bool(n == 0 and c > 0 and fabs(s) <= eps0)
544
+
545
+ def _kwds(self, kwds, **dflt):
546
+ return _xkwds(kwds, **_xkwds(dflt, unit=self.unit,
547
+ name=self.name))
548
+
549
+ @Property_RO
550
+ def lambertian(self):
551
+ '''Get this C{Ang}'s Lambertian, C{asinh(tan(radians))}.
552
+ '''
553
+ return asinh(self.t) # XXX Lambertian(self.t)
554
+
555
+ def mod(self, mul=_1_0, **unit_name):
556
+ '''Return the I{reduced latitude} C{atan(B{mul} *
557
+ tan(B{this}))} as an C{Ang}.
558
+
559
+ @arg mul: Factor (C{scalar}, positive).
560
+
561
+ @note: The quadrant of the result tracks that of
562
+ this C{Ang} through multiples turns.
563
+ '''
564
+ kwds = self._kwds(unit_name)
565
+ if signBit(mul):
566
+ r = self._Ang(Ang.NAN(), **kwds)
567
+ else:
568
+ s, c, n = self.scn3
569
+ if mul > 1:
570
+ c = c / mul # /= chokes PyChecker
571
+ else: # mul <= 1
572
+ s *= mul
573
+ r = self._Ang(s, c, n, normal=False, **kwds)
574
+ return r
575
+
576
+ @staticmethod
577
+ def N(**unit_name):
578
+ '''Get North C{Ang}.
579
+ '''
580
+ return Ang(0, 1, **unit_name)
581
+
582
+ @property
583
+ def n(self):
584
+ '''Return the number of turns (C{float}) or C{0.0}.
585
+ '''
586
+ return self._n or _0_0
587
+
588
+ @n.setter # PYCHOK setter!
589
+ def n(self, n):
590
+ self._n_0(_fint(n))
591
+
592
+ def _n_0(self, n):
593
+ '''(INTERNAL) Set C{n} or C{n0}.
594
+ '''
595
+ if self._n != n:
596
+ self._n, n = n, self._n
597
+ self._update()
598
+ return n
599
+
600
+ @property
601
+ def n0(self):
602
+ '''Return the number of turns, treating C{-180} as C{180 - 1 turn} (C{float}).
603
+ '''
604
+ return (self.n - self._n01) or _0_0
605
+
606
+ @n0.setter # PYCHOK setter!
607
+ def n0(self, n):
608
+ self._n_0(_fint(n) + self._n01)
609
+
610
+ @Property_RO
611
+ def _n01(self):
612
+ s, c = self.sc2
613
+ return int(c < 0 and s == 0 and signBit(s))
614
+
615
+ @staticmethod
616
+ def NAN(**unit_name):
617
+ '''Get an invalid C{Ang}.
618
+ '''
619
+ return Ang(NAN, NAN, **unit_name)
620
+
621
+ @Property_RO
622
+ def ncardinal(self):
623
+ '''Get the nearest cardinal direction (C{float_int}).
624
+
625
+ @note: This is the reverse of C{cardinal}.
626
+ '''
627
+ return _ncardinal(*self.scn3)
628
+
629
+ def nearest(self, ind=0, **name):
630
+ '''Return the closest cardinal direction (C{Ang}).
631
+
632
+ @arg ind: An indicator, if C{B{ind}=0} the closest cardinal
633
+ direction, otherwise, if B{C{ind}} is even, the
634
+ closest even (N/S) cardinal direction or if B{C{ind}}
635
+ is odd, the closest odd (E/W) cardinal direction.
636
+ '''
637
+ s, c, n = self.scn3
638
+ p = (ind == 0 and fabs(s) > fabs(c)) or (ind & 1)
639
+ s, c = _orthogonal2(p, s, c)
640
+ return self._Ang(s, c, n, **self._kwds(name))
641
+
642
+ @staticmethod
643
+ def _norm(bet, omg, alp=None, alt=False):
644
+ '''(INTERNAL) Put C{bet}, C{ong} and C{alp} in range.
645
+ ''' # Ellipsoid3.AngNorm
646
+ flip = signBit(omg.s if alt else bet.c)
647
+ if flip:
648
+ Ang._flip(bet, omg, alp)
649
+ return flip
650
+
651
+ def normalize(self, *n):
652
+ '''Re-normalize this C{Ang}, optionally replacing turns.
653
+ '''
654
+ sc = _normalize2(*self.sc2)
655
+ if n:
656
+ self.n, n = n[0], self.n
657
+ if self.n != n: # updated
658
+ self._s, self._c = sc
659
+ return self
660
+ return self._update(*sc)
661
+
662
+ def _other(self, other):
663
+ # get C{other} as C{Ang} from C{unit}
664
+ return other if isAng(other) else _other(other, self.unit)
665
+
666
+ pow = __pow__
667
+
668
+ @Property_RO
669
+ def _quadrant(self):
670
+ s, c = map(int, map(signBit, self.sc2))
671
+ return s + s + (c ^ s)
672
+
673
+ @property_doc_("this C{Ang}'s quadrant (C{int} 0..3)")
674
+ def quadrant(self):
675
+ return self._quadrant
676
+
677
+ @quadrant.setter # PYCHOK setter!
678
+ def quadrant(self, quadrant):
679
+ s, c = map(fabs, self.sc2)
680
+ q = int(quadrant)
681
+ if (q & 2):
682
+ s = -s # _copysign(self.s, -1 if (q & 2) else 1)
683
+ if (((q >> 1) ^ q) & 1):
684
+ c = -c # _copysign(self.c, -1 if (((q >> 1) ^ q) & 1) else 1)
685
+ self._update(s, c)
686
+
687
+ @Property_RO
688
+ def radians(self):
689
+ '''Get this C{Ang} in C{radians}.
690
+ '''
691
+ r = self.radians0
692
+ if self.n:
693
+ r += self.n * PI2
694
+ return r # XXX Radians(r, self.name)
695
+
696
+ @Property_RO
697
+ def radians0(self):
698
+ '''Get this C{Ang} in C{radians} ignoring the turns.
699
+ '''
700
+ return atan2(*self.sc2) # XXX Radians(r, self.name)
701
+
702
+ def reflect(self, flips=False, flipc=False, swapsc=False):
703
+ '''Reflect this C{Ang} in various ways.
704
+
705
+ @kwarg flips: Flip the sign of C{s}.
706
+ @kwarg flipc: Flip the sign of C{c}.
707
+ @kwarg swapsc: Swap C{s} and C{c}.
708
+
709
+ @note: The operations are carried out in the order
710
+ of the arguments.
711
+ '''
712
+ s, c = self.sc2
713
+ if flips:
714
+ s = -s
715
+ if flipc:
716
+ c = -c
717
+ if swapsc:
718
+ s, c = c, s
719
+ return self._update(s, c)
720
+
721
+ def round(self, *ndigits, **name):
722
+ '''Return this C{Ang}, optionally rounded to C{ndigits} (C{Ang}).
723
+ '''
724
+ s, c, n = self.scn3
725
+ if ndigits:
726
+ s = round(s, *ndigits)
727
+ c = round(c, *ndigits)
728
+ else:
729
+ s, c = map1(_rnd, s, c)
730
+ return self._Ang(s, c, n, **self._kwds(name))
731
+
732
+ @property_RO
733
+ def s(self):
734
+ '''Get the sine of this C{Ang} (C{float}).
735
+ '''
736
+ return self._s
737
+
738
+ @property_RO
739
+ def sc2(self):
740
+ '''Get the 2-tuple C{(s, c)}.
741
+ '''
742
+ return self.s, self.c
743
+
744
+ @Property_RO
745
+ def scn3(self):
746
+ '''Get the 3-tuple C{(s, c, n)}.
747
+ '''
748
+ return self.s, self.c, self.n
749
+
750
+ @property_RO
751
+ def scnu4(self):
752
+ '''Get the 4-tuple C{(s, c, n, unit)}.
753
+ '''
754
+ return self.s, self.c, self.n, self.unit
755
+
756
+ def shift(self, q=0, **unit_name):
757
+ '''Shift this C{Ang} by C{q} I{quarter} turns (C{scalar}).
758
+ '''
759
+ kwds = self._kwds(unit_name)
760
+ if _isfinite(q):
761
+ s = self.copy(**kwds)
762
+ if q:
763
+ s -= Ang.cardinal(q)
764
+ else:
765
+ s = Ang.NAN(**kwds)
766
+ return s
767
+
768
+ def signOf(self, *n):
769
+ '''Determine this C{Ang}'s sign, optionally replacing the turns.
770
+
771
+ @return: The sign (C{int}, -1, 0 or +1).
772
+ '''
773
+ return _signOf(self.toDegrees(*n), 0)
774
+
775
+ @Property_RO
776
+ def t(self):
777
+ '''Get the tangent of this C{Ang} (C{float}).
778
+ '''
779
+ return _over(*self.sc2)
780
+
781
+ def toDegrees(self, *n):
782
+ '''Return this C{Ang} as C{Degrees}, optionally replacing the turns.
783
+ '''
784
+ if n:
785
+ d = self.degrees0
786
+ n = float(n[0])
787
+ if n:
788
+ d += n * _360_0
789
+ else:
790
+ d = self.degrees
791
+ return Degrees(d, self.name)
792
+
793
+ def toLambertian(self, **name):
794
+ '''Return this C{Ang} as L{Lambertian}.
795
+ '''
796
+ name = _xkwds(name, name=self.name)
797
+ return Lambertian(self.lambertian, **name)
798
+
799
+ def toRadians(self, *n):
800
+ '''Return this C{Ang} as C{Radians}, optionally replacing the turns.
801
+ '''
802
+ if n:
803
+ r = self.radians0
804
+ n = float(n[0])
805
+ if n:
806
+ r += n * PI2
807
+ else:
808
+ r = self.radians
809
+ return Radians(r, self.name)
810
+
811
+ def toRepr(self, *n, **prec_fmt): # PYCHOK signature
812
+ '''Return this C{Ang} as C{"<name>(<value>)"} with/out turns (C{str}).
813
+ '''
814
+ return self.toUnit(*n).toRepr(**prec_fmt)
815
+
816
+ def toStr(self, *n, **prec_fmt): # PYCHOK signature
817
+ '''Return this C{Ang} as C{"<value>"} with/out turns (C{str}).
818
+ '''
819
+ return self.toUnit(*n).toStr(**prec_fmt)
820
+
821
+ def toTuple(self, **prec_fmt_sep):
822
+ '''Return string C{"(s, c, n)"} or tuple C{('s', 'c', 'n')} if C{sep is None}.
823
+ '''
824
+ return fstr(self.scn3, **prec_fmt_sep)
825
+
826
+ def toUnit(self, *n):
827
+ '''Return this C{Ang} as C{self.unit}s, optionally replacing the turns.
828
+ '''
829
+ u = self.unit
830
+ return self.toRadians(*n) if u is Radians else (
831
+ self.toDegrees(*n) if u is Degrees else (
832
+ self.toLambertian() if u is Lambertian else
833
+ _raiseError(self.toUnit, u))) # PYCHOK indent
834
+
835
+ @property_doc_(' the scalar unit to L{Degrees} or L{Radians}')
836
+ def unit(self):
837
+ return self._unit
838
+
839
+ @unit.setter # PYCHOK setter!
840
+ def unit(self, unit):
841
+ if unit not in _Ang_types: # PYCHOK no cover
842
+ _raiseError(Ang.unit, unit)
843
+ if self._unit != unit:
844
+ self._unit = unit
845
+
846
+ def _update(self, *sc):
847
+ if sc:
848
+ if sc == self.sc2:
849
+ return self
850
+ self._s, self._c = sc
851
+ _update_all(self)
852
+ return self
853
+
854
+ _allPropertiesOf_n(14, Ang) # PYCHOK assert
855
+
856
+
857
+ class _Ang3Tuple(_NamedTuple):
858
+ '''(INTERNAL) Methods C{.toDegrees}, C{.toLambertian}, C{.toRadians} and C{.toUnit}.
859
+ '''
860
+ _Names_ = (Ang.__name__,) * 3 # needed for ...
861
+ _Units_ = Ang, Ang, _Pass # ...testNamedTuples
862
+
863
+ def toDegrees(self, *n, **fmt_prec_sep):
864
+ '''Change any C{Ang} to C{unit Degrees} or to C{Degrees.toStr} if any B{C{fmt_prec_sep}}.
865
+ '''
866
+ t = self.toUnit(Degrees, *n)
867
+ if fmt_prec_sep: # see C{Degrees.toStr}
868
+ sep, fmt_prec = _xkwds_pop2(fmt_prec_sep, sep=_COMMASPACE_)
869
+ s = self.toStr(sep=None) if sep else self
870
+ t = (a.toStr(**fmt_prec) if isAng(a) else s for a, s in zip(t, s))
871
+ t = Fmt.PAREN(sep.join(t)) if sep else tuple(t)
872
+ return t
873
+
874
+ def toLambertian(self):
875
+ '''Change any C{Ang} to C{unit Lambertian}.
876
+ '''
877
+ return self.toUnit(Lambertian)
878
+
879
+ def toRadians(self, *n):
880
+ '''Change any C{Ang} to C{unit Radians}.
881
+ '''
882
+ return self.toUnit(Radians, *n)
883
+
884
+ def toUnit(self, unit, *n):
885
+ '''Change any C{Ang} to C{unit}, .
886
+ '''
887
+ for a in self:
888
+ if isAng(a): # and a.unit is not unit:
889
+ a.unit = unit
890
+ if n:
891
+ a.n = n[0]
892
+ return self
893
+
894
+
895
+ class Lambertian(Radians):
896
+ '''A C{Lambertian} in C{radians}.
897
+ '''
898
+ def __new__(cls, *args, **kwds):
899
+ return Radians.__new__(cls, *args, **_xkwds(kwds, name='psi'))
900
+
901
+
902
+ _Ang_from = {Radians: Ang.fromRadians,
903
+ Degrees: Ang.fromDegrees,
904
+ Lambertian: Ang.fromLambertian}
905
+ _Ang_types = tuple(_Ang_from.keys()) # PYCHOK used!
906
+
907
+
908
+ def Ang_(s, c=None, n=1, **unit_name):
909
+ '''(INTERNAL) New, non-normal C{Ang}.
910
+ '''
911
+ return Ang(s, c, n, **_xkwds(unit_name, normal=False))
912
+
913
+
914
+ def Deg(deg, **name):
915
+ '''Return an L{Ang} from C{deg} degrees or an other L{Ang}.
916
+ '''
917
+ return Ang(deg, unit=Degrees, **name)
918
+
919
+
920
+ def isAng(ang):
921
+ '''Is C{ang} an L{Ang} instance?
922
+ '''
923
+ return isinstance(ang, Ang)
924
+
925
+
926
+ def Rad(rad, **name):
927
+ '''Return an L{Ang} from C{rad} radians or an other L{Ang}.
928
+ '''
929
+ return Ang(rad, unit=Radians, **name)
930
+
931
+
932
+ def _SinCos2(ang, *unit):
933
+ '''Get C{sin} and C{cos} of an L{Ang}, any I{typed} C{ang}le
934
+ or C{unit} if C{ang}le is scalar.
935
+
936
+ @see: Function L{SinCos2<pygeodesy.utily.SinCos2>}.
937
+ '''
938
+ return ang.sc2 if isAng(ang) else SinCos2(ang, *unit)
939
+
940
+ # **) MIT License
941
+ #
942
+ # Copyright (C) 2025-2026 -- mrJean1 at Gmail -- All Rights Reserved.
943
+ #
944
+ # Permission is hereby granted, free of charge, to any person obtaining a
945
+ # copy of this software and associated documentation files (the "Software"),
946
+ # to deal in the Software without restriction, including without limitation
947
+ # the rights to use, copy, modify, merge, publish, distribute, sublicense,
948
+ # and/or sell copies of the Software, and to permit persons to whom the
949
+ # Software is furnished to do so, subject to the following conditions:
950
+ #
951
+ # The above copyright notice and this permission notice shall be included
952
+ # in all copies or substantial portions of the Software.
953
+ #
954
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
955
+ # OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
956
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
957
+ # THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
958
+ # OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
959
+ # ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
960
+ # OTHER DEALINGS IN THE SOFTWARE.