Coverage for pygeodesy/auxilats/auxAngle.py: 96%
225 statements
« prev ^ index » next coverage.py v7.2.2, created at 2024-05-06 16:50 -0400
« prev ^ index » next coverage.py v7.2.2, created at 2024-05-06 16:50 -0400
2# -*- coding: utf-8 -*-
4u'''(INTERNAL) I{Auxiliary} latitudes' base classes, constants and functions.
6Class L{AuxAngle} transcoded to Python from I{Karney}'s C++ class U{AuxAngle
7<https://GeographicLib.SourceForge.io/C++/doc/classGeographicLib_1_1AuxAngle.html>}
8in I{GeographicLib version 2.2+}.
10Copyright (C) U{Charles Karney<mailto:Karney@Alum.MIT.edu>} (2022-2023) and licensed
11under the MIT/X11 License. For more information, see the U{GeographicLib
12<https://GeographicLib.SourceForge.io>} documentation.
13'''
14# make sure int/int division yields float quotient, see .basics
15from __future__ import division as _; del _ # PYCHOK semicolon
17from pygeodesy.auxilats.auxily import Aux, _Aux2Greek, AuxError
18from pygeodesy.basics import _xinstanceof, _xkwds_get
19from pygeodesy.constants import EPS, _INF_NAN_NINF, MAX, NAN, _0_0, _0_5, _1_0, \
20 _copysign_1_0, _over, _pos_self, isfinite, isnan
21# from pygeodesy.errors import AuxError, _xkwds_get # from .auxily, .basics
22from pygeodesy.fmath import hypot, unstr
23from pygeodesy.fsums import _add_op_, _iadd_op_, _isub_op_, _sub_op_, _Named, NN
24# from pygeodesy.interns import NN # from .fsums
25# from pygeodesy.named import _Named # from .fsums
26from pygeodesy.lazily import _ALL_DOCS, _ALL_MODS as _MODS
27from pygeodesy.props import Property, Property_RO, property_RO, _update_all
28# from pygeodesy.streprs import unstr # from .fmath
29from pygeodesy.units import Degrees, Radians
30from pygeodesy.utily import atan2d, sincos2, sincos2d
32from math import asinh, atan2, copysign, degrees, fabs, radians, sinh
34__all__ = ()
35__version__ = '24.04.26'
37_0_INF_NAN_NINF = (0, _0_0) + _INF_NAN_NINF
38_MAX_2 = MAX * _0_5 # PYCHOK used!
39# del _INF_NAN_NINF, MAX
42class AuxAngle(_Named):
43 '''U{An accurate representation of angles
44 <https://GeographicLib.SourceForge.io/C++/doc/classGeographicLib_1_1AuxAngle.html>}
45 '''
46 _AUX = None # overloaded/-ridden
47 _diff = NAN # default
48 _iter = None # like .Named._NamedBase
49 _y = _0_0
50 _x = _1_0
52 def __init__(self, y_angle=_0_0, x=_1_0, name=NN, **aux):
53 '''New L{AuxAngle}.
55 @kwarg y_angle: The Y component (C{scalar}, including C{INF}, C{NAN}
56 and C{NINF}) or a previous L{AuxAngle} instance.
57 @kwarg x: The X component, ignored if C{B{y_angle}} is non-C{scalar}.
58 @kwarg name: Optional name (C{str}).
59 @kwarg aux: I{Auxiliary} kind (C{Aux.KIND}), ignored if B{C{y_angle}}
60 is non-C{scalar}.
62 @raise AuxError: Invalid B{C{y_angle}}, B{C{x}} or B{C{aux}}.
63 '''
64 try:
65 yx = y_angle._yx
66 if self._AUX is None:
67 self._AUX = y_angle._AUX
68 if self._diff != y_angle._diff:
69 self._diff = y_angle._diff
70 except AttributeError:
71 yx = y_angle, x
72 if aux:
73 aux = _xkwds_get(aux, aux=self._AUX)
74 if self._AUX is not aux:
75 if aux not in _AUXClass:
76 raise AuxError(aux=aux)
77 self._AUX = aux
78 self._y, self._x = _yx2(yx)
79 if name:
80 self.name = name
82 def __abs__(self):
83 '''Return this angle's absolute value (L{AuxAngle}).
84 '''
85 a = self._copy_2(self.__abs__)
86 a._yx = map(fabs, self._yx)
87 return a
89 def __add__(self, other):
90 '''Return C{B{self} + B{other}} as an L{AuxAngle}.
92 @arg other: An L{AuxAngle}.
94 @return: The sum (L{AuxAngle}).
96 @raise TypeError: Invalid B{C{other}}.
97 '''
98 a = self._copy_2(self.__add__)
99 return a._iadd(other, _add_op_)
101 def __bool__(self): # PYCHOK not special in Python 2-
102 '''Return C{True} if this angle is non-zero.
103 '''
104 return bool(self.tan)
106 def __eq__(self, other):
107 '''Return C{B{self} == B{other}} as C{bool}.
108 '''
109 return not self.__ne__(other)
111 def __float__(self):
112 '''Return this angle's C{tan} (C{float}).
113 '''
114 return self.tan
116 def __iadd__(self, other):
117 '''Apply C{B{self} += B{other}} to this angle.
119 @arg other: An L{AuxAngle}.
121 @return: This angle, updated (L{AuxAngle}).
123 @raise TypeError: Invalid B{C{other}}.
124 '''
125 return self._iadd(other, _iadd_op_)
127 def __isub__(self, other):
128 '''Apply C{B{self} -= B{other}} to this angle.
130 @arg other: An L{AuxAngle}.
132 @return: This instance, updated (L{AuxAngle}).
134 @raise TypeError: Invalid B{C{other}} type.
135 '''
136 return self._iadd(-other, _isub_op_)
138 def __ne__(self, other):
139 '''Return C{B{self} != B{other}} as C{bool}.
140 '''
141 _xinstanceof(AuxAngle, other=other)
142 y, x, r = self._yxr_normalized()
143 s, c, t = other._yxr_normalized()
144 return fabs(y - s) > EPS or fabs(x - c) > EPS \
145 or fabs(r - t) > EPS
147 def __neg__(self):
148 '''Return I{a copy of} this angle, negated.
149 '''
150 a = self._copy_2(self.__neg__)
151 if a.y or not a.x:
152 a.y = -a.y
153 else:
154 a.x = -a.x
155 return a
157 def __pos__(self):
158 '''Return this angle I{as-is}, like C{float.__pos__()}.
159 '''
160 return self if _pos_self else self._copy_2(self.__pos__)
162 def __radd__(self, other):
163 '''Return C{B{other} + B{self}} as an L{AuxAngle}.
165 @see: Method L{AuxAngle.__add__}.
166 '''
167 a = self._copy_r2(other, self.__radd__)
168 return a._iadd(self, _add_op_)
170 def __rsub__(self, other):
171 '''Return C{B{other} - B{self}} as an L{AuxAngle}.
173 @see: Method L{AuxAngle.__sub__}.
174 '''
175 a = self._copy_r2(other, self.__rsub__)
176 return a._iadd(-self, _sub_op_)
178 def __str__(self):
179 n = _Aux2Greek.get(self._AUX, self.classname)
180 return unstr(n, y=self.y, x=self.x, tan=self.tan)
182 def __sub__(self, other):
183 '''Return C{B{self} - B{other}} as an L{AuxAngle}.
185 @arg other: An L{AuxAngle}.
187 @return: The difference (L{AuxAngle}).
189 @raise TypeError: Invalid B{C{other}} type.
190 '''
191 a = self._copy_2(self.__sub__)
192 return a._iadd(-other, _sub_op_)
194 def _iadd(self, other, *unused): # op
195 '''(INTERNAL) Apply C{B{self} += B{other}}.
196 '''
197 _xinstanceof(AuxAngle, other=other)
198 # ignore zero other to preserve signs of _y and _x
199 if other.tan:
200 s, c = other._yx
201 y, x = self._yx
202 self._yx = (y * c + x * s), \
203 (x * c - y * s)
204 return self
206 def _copy_2(self, which):
207 '''(INTERNAL) Copy for I{dyadic} operators.
208 '''
209 return _Named.copy(self, deep=False, name=which.__name__)
211 def _copy_r2(self, other, which):
212 '''(INTERNAL) Copy for I{reverse-dyadic} operators.
213 '''
214 _xinstanceof(AuxAngle, other=other)
215 return other._copy_2(which)
217 def copyquadrant(self, other):
218 '''Copy an B{C{other}} angle's quadrant into this angle (L{auxAngle}).
219 '''
220 _xinstanceof(AuxAngle, other=other)
221 self._yx = copysign(self.y, other.y), \
222 copysign(self.x, other.x)
223 return self
225 @Property_RO
226 def diff(self):
227 '''Get derivative C{dtan(Eta) / dtan(Phi)} (C{float} or C{NAN}).
228 '''
229 return self._diff
231 @staticmethod
232 def fromDegrees(deg, **name_aux):
233 '''Get an L{AuxAngle} from degrees.
234 '''
235 return _AuxClass(**name_aux)(*sincos2d(deg), **name_aux)
237 @staticmethod
238 def fromLambertianDegrees(psi, **name_aux):
239 '''Get an L{AuxAngle} from I{Lambertian} degrees.
240 '''
241 return _AuxClass(**name_aux)(sinh(radians(psi)), **name_aux)
243 @staticmethod
244 def fromLambertianRadians(psi, **name_aux):
245 '''Get an L{AuxAngle} from I{Lambertian} radians.
246 '''
247 return _AuxClass(**name_aux)(sinh(psi), **name_aux)
249 @staticmethod
250 def fromRadians(rad, **name_aux):
251 '''Get an L{AuxAngle} from radians.
252 '''
253 return _AuxClass(**name_aux)(*sincos2(rad), **name_aux)
255 @Property_RO
256 def iteration(self):
257 '''Get the iteration (C{int} or C{None}).
258 '''
259 return self._iter
261 def normal(self):
262 '''Normalize this angle I{in-place}.
264 @return: This angle, normalized (L{AuxAngle}).
265 '''
266 self._yx = self._yx_normalized
267 return self
269 @Property_RO
270 def normalized(self):
271 '''Get a normalized copy of this angle (L{AuxAngle}).
272 '''
273 y, x = self._yx_normalized
274 return self.classof(y, x, name=self.name, aux=self._AUX)
276 @property_RO
277 def _RhumbAux(self):
278 '''(INTERNAL) Import the L{RhumbAux} class, I{once}.
279 '''
280 AuxAngle._RhumbAux = R = _MODS.rhumb.aux_.RhumbAux # overwrite property_RO
281 return R
283 @Property_RO
284 def tan(self):
285 '''Get this angle's C{tan} (C{float}).
286 '''
287 y, x = self._yx
288 return _over(y, x) if isfinite(y) and y else y
290 def toBeta(self, rhumb):
291 '''Short for C{rhumb.auxDLat.convert(Aux.BETA, self, exact=rhumb.exact)}
292 '''
293 return self._toRhumbAux(rhumb, Aux.BETA)
295 def toChi(self, rhumb):
296 '''Short for C{rhumb.auxDLat.convert(Aux.CHI, self, exact=rhumb.exact)}
297 '''
298 return self._toRhumbAux(rhumb, Aux.CHI)
300 @Property_RO
301 def toDegrees(self):
302 '''Get this angle as L{Degrees}.
303 '''
304 return Degrees(atan2d(*self._yx), name=self.name)
306 @Property_RO
307 def toLambertianDegrees(self): # PYCHOK no cover
308 '''Get this angle's I{Lambertian} in L{Degrees}.
309 '''
310 r = self.toLambertianRadians
311 return Degrees(degrees(r), name=r.name)
313 @Property_RO
314 def toLambertianRadians(self):
315 '''Get this angle's I{Lambertian} in L{Radians}.
316 '''
317 return Radians(asinh(self.tan), name=self.name)
319 def toMu(self, rhumb):
320 '''Short for C{rhumb.auxDLat.convert(Aux.MU, self, exact=rhumb.exact)}
321 '''
322 return self._toRhumbAux(rhumb, Aux.MU)
324 def toPhi(self, rhumb):
325 '''Short for C{rhumb.auxDLat.convert(Aux.PHI, self, exact=rhumb.exact)}
326 '''
327 return self._toRhumbAux(rhumb, Aux.PHI)
329 @Property_RO
330 def toRadians(self):
331 '''Get this angle as L{Radians}.
332 '''
333 return Radians(atan2(*self._yx), name=self.name)
335 def _toRhumbAux(self, rhumb, aux):
336 '''(INTERNAL) Create an C{aux}-KIND angle from this angle.
337 '''
338 _xinstanceof(self._RhumbAux, rhumb=rhumb)
339 return rhumb._auxD.convert(aux, self, exact=rhumb.exact)
341 @Property
342 def x(self):
343 '''Get this angle's C{x} (C{float}).
344 '''
345 return self._x
347 @x.setter # PYCHOK setter!
348 def x(self, x): # PYCHOK no cover
349 '''Set this angle's C{x} (C{float}).
350 '''
351 x = float(x)
352 if self._x != x:
353 _update_all(self)
354 self._x = x
356 @property_RO
357 def _x_normalized(self):
358 '''(INTERNAL) Get the I{normalized} C{x}.
359 '''
360 _, x = self._yx_normalized
361 return x
363 @Property
364 def y(self):
365 '''Get this angle's C{y} (C{float}).
366 '''
367 return self._y
369 @y.setter # PYCHOK setter!
370 def y(self, y): # PYCHOK no cover
371 '''Set this angle's C{y} (C{float}).
372 '''
373 y = float(y)
374 if self.y != y:
375 _update_all(self)
376 self._y = y
378 @Property
379 def _yx(self):
380 '''(INTERNAL) Get this angle as 2-tuple C{(y, x)}.
381 '''
382 return self._y, self._x
384 @_yx.setter # PYCHOK setter!
385 def _yx(self, yx):
386 '''(INTERNAL) Set this angle's C{y} and C{x}.
387 '''
388 yx = _yx2(yx)
389 if self._yx != yx:
390 _update_all(self)
391 self._y, self._x = yx
393 @Property_RO
394 def _yx_normalized(self):
395 '''(INTERNAL) Get this angle as 2-tuple C{(y, x)}, I{normalized}.
396 '''
397 y, x = self._yx
398 if isfinite(y) and fabs(y) < _MAX_2 \
399 and fabs(x) < _MAX_2 \
400 and isfinite(self.tan):
401 h = hypot(y, x)
402 if h > 0 and y:
403 y = y / h # /= chokes PyChecker
404 x = x / h
405 if isnan(y): # PYCHOK no cover
406 y = _copysign_1_0(self.y)
407 if isnan(x): # PYCHOK no cover
408 x = _copysign_1_0(self.x)
409 else: # scalar 0
410 y, x = _0_0, _copysign_1_0(y * x)
411 else: # scalar NAN
412 y, x = NAN, _copysign_1_0(y * x)
413 return y, x
415 def _yxr_normalized(self, abs_y=False):
416 '''(INTERNAL) Get 3-tuple C{(y, x, r)}, I{normalized}
417 with C{y} or C{abs(y)} and C{r} as C{.toRadians}.
418 '''
419 y, x = self._yx_normalized
420 if abs_y:
421 y = fabs(y) # only y, not x
422 return y, x, atan2(y, x) # .toRadians
425class AuxBeta(AuxAngle):
426 '''A I{Parametric, Auxiliary} latitude.
427 '''
428 _AUX = Aux.BETA
430 @staticmethod
431 def fromDegrees(deg, name=NN):
432 '''Get an L{AuxBeta} from degrees.
433 '''
434 return AuxBeta(*sincos2d(deg), name=name)
436 @staticmethod
437 def fromRadians(rad, name=NN):
438 '''Get an L{AuxBeta} from radians.
439 '''
440 return AuxBeta(*sincos2(rad), name=name)
443class AuxChi(AuxAngle):
444 '''A I{Conformal, Auxiliary} latitude.
445 '''
446 _AUX = Aux.CHI
448 @staticmethod
449 def fromDegrees(deg, name=NN):
450 '''Get an L{AuxChi} from degrees.
451 '''
452 return AuxChi(*sincos2d(deg), name=name)
455class AuxMu(AuxAngle):
456 '''A I{Rectifying, Auxiliary} latitude.
457 '''
458 _AUX = Aux.MU
460 @staticmethod
461 def fromDegrees(deg, name=NN):
462 '''Get an L{AuxMu} from degrees.
463 '''
464 return AuxMu(*sincos2d(deg), name=name)
467class AuxPhi(AuxAngle):
468 '''A I{Geodetic or Geographic, Auxiliary} latitude.
469 '''
470 _AUX = Aux.PHI
471 _diff = _1_0 # see .auxLat._Newton
473 @staticmethod
474 def fromDegrees(deg, name=NN):
475 '''Get an L{AuxPhi} from degrees.
476 '''
477 return AuxPhi(*sincos2d(deg), name=name)
480class AuxTheta(AuxAngle):
481 '''A I{Geocentric, Auxiliary} latitude.
482 '''
483 _AUX = Aux.THETA
485 @staticmethod
486 def fromDegrees(deg, name=NN):
487 '''Get an L{AuxTheta} from degrees.
488 '''
489 return AuxTheta(*sincos2d(deg), name=name)
492class AuxXi(AuxAngle):
493 '''An I{Authalic, Auxiliary} latitude.
494 '''
495 _AUX = Aux.XI
497 @staticmethod
498 def fromDegrees(deg, name=NN):
499 '''Get an L{AuxXi} from degrees.
500 '''
501 return AuxXi(*sincos2d(deg), name=name)
504_AUXClass = {Aux.BETA: AuxBeta,
505 Aux.CHI: AuxChi,
506 Aux.MU: AuxMu,
507 Aux.PHI: AuxPhi,
508 Aux.THETA: AuxTheta,
509 Aux.XI: AuxXi}
511def _AuxClass(aux=None, **unused): # PYCHOK C{classof(aux)}
512 return _AUXClass.get(aux, AuxAngle)
515def _yx2(yx):
516 try:
517 y, x = map(float, yx)
518 if y in _0_INF_NAN_NINF:
519 x = _copysign_1_0(x)
520 except (TypeError, ValueError) as e:
521 y, x = yx
522 raise AuxError(y=y, x=x, cause=e)
523 return y, x
526__all__ += _ALL_DOCS(AuxAngle, AuxBeta, AuxChi, AuxMu, AuxPhi, AuxTheta, AuxXi)
528# **) MIT License
529#
530# Copyright (C) 2023-2024 -- mrJean1 at Gmail -- All Rights Reserved.
531#
532# Permission is hereby granted, free of charge, to any person obtaining a
533# copy of this software and associated documentation files (the "Software"),
534# to deal in the Software without restriction, including without limitation
535# the rights to use, copy, modify, merge, publish, distribute, sublicense,
536# and/or sell copies of the Software, and to permit persons to whom the
537# Software is furnished to do so, subject to the following conditions:
538#
539# The above copyright notice and this permission notice shall be included
540# in all copies or substantial portions of the Software.
541#
542# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
543# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
544# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
545# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
546# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
547# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
548# OTHER DEALINGS IN THE SOFTWARE.