Coverage for pygeodesy/angles.py: 95%

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.