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