Coverage for pygeodesy/auxilats/auxAngle.py: 96%
227 statements
« prev ^ index » next coverage.py v7.2.2, created at 2024-06-01 11:43 -0400
« prev ^ index » next coverage.py v7.2.2, created at 2024-06-01 11:43 -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
18from pygeodesy.basics import _xinstanceof
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
21from pygeodesy.errors import AuxError, _UnexpectedError, _xkwds_pop2
22from pygeodesy.fmath import hypot, unstr
23from pygeodesy.fsums import _add_op_, _iadd_op_, _isub_op_, _sub_op_
24from pygeodesy.named import _name2__, _Named, _ALL_DOCS, _MODS
25# from pygeodesy.lazily import _ALL_DOCS, _ALL_MODS as _MODS # from .named
26from pygeodesy.props import Property, Property_RO, property_RO, _update_all
27# from pygeodesy.streprs import unstr # from .fmath
28from pygeodesy.units import Degrees, Radians
29from pygeodesy.utily import atan2d, sincos2, sincos2d
31from math import asinh, atan2, copysign, degrees, fabs, radians, sinh
33__all__ = ()
34__version__ = '24.05.25'
36_0_INF_NAN_NINF = (0, _0_0) + _INF_NAN_NINF
37_MAX_2 = MAX * _0_5 # PYCHOK used!
38# del _INF_NAN_NINF, MAX
41class AuxAngle(_Named):
42 '''U{An accurate representation of angles
43 <https://GeographicLib.SourceForge.io/C++/doc/classGeographicLib_1_1AuxAngle.html>}
44 '''
45 _AUX = None # overloaded/-ridden
46 _diff = NAN # default
47 _iter = None # like .Named._NamedBase
48 _y = _0_0
49 _x = _1_0
51 def __init__(self, y_angle=_0_0, x=_1_0, **name_aux):
52 '''New L{AuxAngle}.
54 @kwarg y_angle: The Y component (C{scalar}, including C{INF}, C{NAN}
55 and C{NINF}) or a previous L{AuxAngle} instance.
56 @kwarg x: The X component, ignored if C{B{y_angle}} is non-C{scalar}.
57 @kwarg name_aux: Optional C{B{name}=NN} (C{str}) and I{Auxiliary} kind
58 (C{B{aux}=Aux.KIND}), ignored if B{C{y_angle}} is L{AuxAngle}.
60 @raise AuxError: Invalid B{C{y_angle}}, B{C{x}} or B{C{aux}}.
61 '''
62 name, aux = _name2__(**name_aux)
63 try:
64 yx = y_angle._yx
65 if self._AUX is None:
66 self._AUX = y_angle._AUX
67 if self._diff != y_angle._diff:
68 self._diff = y_angle._diff
69 except AttributeError:
70 yx = y_angle, x
71 if aux:
72 a, kwds = _xkwds_pop2(aux, aux=self._AUX)
73 if kwds:
74 raise _UnexpectedError(**kwds)
75 if a is not self._AUX:
76 if a not in _AUXClass:
77 raise AuxError(aux=a)
78 self._AUX = a
79 self._y, self._x = _yx2(yx)
80 if name:
81 self.name = name
83 def __abs__(self):
84 '''Return this angle's absolute value (L{AuxAngle}).
85 '''
86 a = self._copy_2(self.__abs__)
87 a._yx = map(fabs, self._yx)
88 return a
90 def __add__(self, other):
91 '''Return C{B{self} + B{other}} as an L{AuxAngle}.
93 @arg other: An L{AuxAngle}.
95 @return: The sum (L{AuxAngle}).
97 @raise TypeError: Invalid B{C{other}}.
98 '''
99 a = self._copy_2(self.__add__)
100 return a._iadd(other, _add_op_)
102 def __bool__(self): # PYCHOK not special in Python 2-
103 '''Return C{True} if this angle is non-zero.
104 '''
105 return bool(self.tan)
107 def __eq__(self, other):
108 '''Return C{B{self} == B{other}} as C{bool}.
109 '''
110 return not self.__ne__(other)
112 def __float__(self):
113 '''Return this angle's C{tan} (C{float}).
114 '''
115 return self.tan
117 def __iadd__(self, other):
118 '''Apply C{B{self} += B{other}} to this angle.
120 @arg other: An L{AuxAngle}.
122 @return: This angle, updated (L{AuxAngle}).
124 @raise TypeError: Invalid B{C{other}}.
125 '''
126 return self._iadd(other, _iadd_op_)
128 def __isub__(self, other):
129 '''Apply C{B{self} -= B{other}} to this angle.
131 @arg other: An L{AuxAngle}.
133 @return: This instance, updated (L{AuxAngle}).
135 @raise TypeError: Invalid B{C{other}} type.
136 '''
137 return self._iadd(-other, _isub_op_)
139 def __ne__(self, other):
140 '''Return C{B{self} != B{other}} as C{bool}.
141 '''
142 _xinstanceof(AuxAngle, other=other)
143 y, x, r = self._yxr_normalized()
144 s, c, t = other._yxr_normalized()
145 return fabs(y - s) > EPS or fabs(x - c) > EPS \
146 or fabs(r - t) > EPS
148 def __neg__(self):
149 '''Return I{a copy of} this angle, negated.
150 '''
151 a = self._copy_2(self.__neg__)
152 if a.y or not a.x:
153 a.y = -a.y
154 else:
155 a.x = -a.x
156 return a
158 def __pos__(self):
159 '''Return this angle I{as-is}, like C{float.__pos__()}.
160 '''
161 return self if _pos_self else self._copy_2(self.__pos__)
163 def __radd__(self, other):
164 '''Return C{B{other} + B{self}} as an L{AuxAngle}.
166 @see: Method L{AuxAngle.__add__}.
167 '''
168 a = self._copy_r2(other, self.__radd__)
169 return a._iadd(self, _add_op_)
171 def __rsub__(self, other):
172 '''Return C{B{other} - B{self}} as an L{AuxAngle}.
174 @see: Method L{AuxAngle.__sub__}.
175 '''
176 a = self._copy_r2(other, self.__rsub__)
177 return a._iadd(-self, _sub_op_)
179 def __str__(self):
180 n = _Aux2Greek.get(self._AUX, self.classname)
181 return unstr(n, y=self.y, x=self.x, tan=self.tan)
183 def __sub__(self, other):
184 '''Return C{B{self} - B{other}} as an L{AuxAngle}.
186 @arg other: An L{AuxAngle}.
188 @return: The difference (L{AuxAngle}).
190 @raise TypeError: Invalid B{C{other}} type.
191 '''
192 a = self._copy_2(self.__sub__)
193 return a._iadd(-other, _sub_op_)
195 def _iadd(self, other, *unused): # op
196 '''(INTERNAL) Apply C{B{self} += B{other}}.
197 '''
198 _xinstanceof(AuxAngle, other=other)
199 # ignore zero other to preserve signs of _y and _x
200 if other.tan:
201 s, c = other._yx
202 y, x = self._yx
203 self._yx = (y * c + x * s), \
204 (x * c - y * s)
205 return self
207 def _copy_2(self, which):
208 '''(INTERNAL) Copy for I{dyadic} operators.
209 '''
210 return _Named.copy(self, deep=False, name=which.__name__)
212 def _copy_r2(self, other, which):
213 '''(INTERNAL) Copy for I{reverse-dyadic} operators.
214 '''
215 _xinstanceof(AuxAngle, other=other)
216 return other._copy_2(which)
218 def copyquadrant(self, other):
219 '''Copy an B{C{other}} angle's quadrant into this angle (L{auxAngle}).
220 '''
221 _xinstanceof(AuxAngle, other=other)
222 self._yx = copysign(self.y, other.y), \
223 copysign(self.x, other.x)
224 return self
226 @Property_RO
227 def diff(self):
228 '''Get derivative C{dtan(Eta) / dtan(Phi)} (C{float} or C{NAN}).
229 '''
230 return self._diff
232 @staticmethod
233 def fromDegrees(deg, **name_aux):
234 '''Get an L{AuxAngle} from degrees.
235 '''
236 return _AuxClass(**name_aux)(*sincos2d(deg), **name_aux)
238 @staticmethod
239 def fromLambertianDegrees(psi, **name_aux):
240 '''Get an L{AuxAngle} from I{Lambertian} degrees.
241 '''
242 return _AuxClass(**name_aux)(sinh(radians(psi)), **name_aux)
244 @staticmethod
245 def fromLambertianRadians(psi, **name_aux):
246 '''Get an L{AuxAngle} from I{Lambertian} radians.
247 '''
248 return _AuxClass(**name_aux)(sinh(psi), **name_aux)
250 @staticmethod
251 def fromRadians(rad, **name_aux):
252 '''Get an L{AuxAngle} from radians.
253 '''
254 return _AuxClass(**name_aux)(*sincos2(rad), **name_aux)
256 @Property_RO
257 def iteration(self):
258 '''Get the iteration (C{int} or C{None}).
259 '''
260 return self._iter
262 def normal(self):
263 '''Normalize this angle I{in-place}.
265 @return: This angle, normalized (L{AuxAngle}).
266 '''
267 self._yx = self._yx_normalized
268 return self
270 @Property_RO
271 def normalized(self):
272 '''Get a normalized copy of this angle (L{AuxAngle}).
273 '''
274 y, x = self._yx_normalized
275 return self.classof(y, x, name=self.name, aux=self._AUX)
277 @property_RO
278 def _RhumbAux(self):
279 '''(INTERNAL) Import the L{RhumbAux} class, I{once}.
280 '''
281 AuxAngle._RhumbAux = R = _MODS.rhumb.aux_.RhumbAux # overwrite property_RO
282 return R
284 @Property_RO
285 def tan(self):
286 '''Get this angle's C{tan} (C{float}).
287 '''
288 y, x = self._yx
289 return _over(y, x) if isfinite(y) and y else y
291 def toBeta(self, rhumb):
292 '''Short for C{rhumb.auxDLat.convert(Aux.BETA, self, exact=rhumb.exact)}
293 '''
294 return self._toRhumbAux(rhumb, Aux.BETA)
296 def toChi(self, rhumb):
297 '''Short for C{rhumb.auxDLat.convert(Aux.CHI, self, exact=rhumb.exact)}
298 '''
299 return self._toRhumbAux(rhumb, Aux.CHI)
301 @Property_RO
302 def toDegrees(self):
303 '''Get this angle as L{Degrees}.
304 '''
305 return Degrees(atan2d(*self._yx), name=self.name)
307 @Property_RO
308 def toLambertianDegrees(self): # PYCHOK no cover
309 '''Get this angle's I{Lambertian} in L{Degrees}.
310 '''
311 r = self.toLambertianRadians
312 return Degrees(degrees(r), name=r.name)
314 @Property_RO
315 def toLambertianRadians(self):
316 '''Get this angle's I{Lambertian} in L{Radians}.
317 '''
318 return Radians(asinh(self.tan), name=self.name)
320 def toMu(self, rhumb):
321 '''Short for C{rhumb.auxDLat.convert(Aux.MU, self, exact=rhumb.exact)}
322 '''
323 return self._toRhumbAux(rhumb, Aux.MU)
325 def toPhi(self, rhumb):
326 '''Short for C{rhumb.auxDLat.convert(Aux.PHI, self, exact=rhumb.exact)}
327 '''
328 return self._toRhumbAux(rhumb, Aux.PHI)
330 @Property_RO
331 def toRadians(self):
332 '''Get this angle as L{Radians}.
333 '''
334 return Radians(atan2(*self._yx), name=self.name)
336 def _toRhumbAux(self, rhumb, aux):
337 '''(INTERNAL) Create an C{aux}-KIND angle from this angle.
338 '''
339 _xinstanceof(self._RhumbAux, rhumb=rhumb)
340 return rhumb._auxD.convert(aux, self, exact=rhumb.exact)
342 @Property
343 def x(self):
344 '''Get this angle's C{x} (C{float}).
345 '''
346 return self._x
348 @x.setter # PYCHOK setter!
349 def x(self, x): # PYCHOK no cover
350 '''Set this angle's C{x} (C{float}).
351 '''
352 x = float(x)
353 if self._x != x:
354 _update_all(self)
355 self._x = x
357 @property_RO
358 def _x_normalized(self):
359 '''(INTERNAL) Get the I{normalized} C{x}.
360 '''
361 _, x = self._yx_normalized
362 return x
364 @Property
365 def y(self):
366 '''Get this angle's C{y} (C{float}).
367 '''
368 return self._y
370 @y.setter # PYCHOK setter!
371 def y(self, y): # PYCHOK no cover
372 '''Set this angle's C{y} (C{float}).
373 '''
374 y = float(y)
375 if self.y != y:
376 _update_all(self)
377 self._y = y
379 @Property
380 def _yx(self):
381 '''(INTERNAL) Get this angle as 2-tuple C{(y, x)}.
382 '''
383 return self._y, self._x
385 @_yx.setter # PYCHOK setter!
386 def _yx(self, yx):
387 '''(INTERNAL) Set this angle's C{y} and C{x}.
388 '''
389 yx = _yx2(yx)
390 if self._yx != yx:
391 _update_all(self)
392 self._y, self._x = yx
394 @Property_RO
395 def _yx_normalized(self):
396 '''(INTERNAL) Get this angle as 2-tuple C{(y, x)}, I{normalized}.
397 '''
398 y, x = self._yx
399 if isfinite(y) and fabs(y) < _MAX_2 \
400 and fabs(x) < _MAX_2 \
401 and isfinite(self.tan):
402 h = hypot(y, x)
403 if h > 0 and y:
404 y = y / h # /= chokes PyChecker
405 x = x / h
406 if isnan(y): # PYCHOK no cover
407 y = _copysign_1_0(self.y)
408 if isnan(x): # PYCHOK no cover
409 x = _copysign_1_0(self.x)
410 else: # scalar 0
411 y, x = _0_0, _copysign_1_0(y * x)
412 else: # scalar NAN
413 y, x = NAN, _copysign_1_0(y * x)
414 return y, x
416 def _yxr_normalized(self, abs_y=False):
417 '''(INTERNAL) Get 3-tuple C{(y, x, r)}, I{normalized}
418 with C{y} or C{abs(y)} and C{r} as C{.toRadians}.
419 '''
420 y, x = self._yx_normalized
421 if abs_y:
422 y = fabs(y) # only y, not x
423 return y, x, atan2(y, x) # .toRadians
426class AuxBeta(AuxAngle):
427 '''A I{Parametric, Auxiliary} latitude.
428 '''
429 _AUX = Aux.BETA
431 @staticmethod
432 def fromDegrees(deg, **name):
433 '''Get an L{AuxBeta} from degrees.
434 '''
435 return AuxBeta(*sincos2d(deg), **name)
437 @staticmethod
438 def fromRadians(rad, **name):
439 '''Get an L{AuxBeta} from radians.
440 '''
441 return AuxBeta(*sincos2(rad), **name)
444class AuxChi(AuxAngle):
445 '''A I{Conformal, Auxiliary} latitude.
446 '''
447 _AUX = Aux.CHI
449 @staticmethod
450 def fromDegrees(deg, **name):
451 '''Get an L{AuxChi} from degrees.
452 '''
453 return AuxChi(*sincos2d(deg), **name)
456class AuxMu(AuxAngle):
457 '''A I{Rectifying, Auxiliary} latitude.
458 '''
459 _AUX = Aux.MU
461 @staticmethod
462 def fromDegrees(deg, **name):
463 '''Get an L{AuxMu} from degrees.
464 '''
465 return AuxMu(*sincos2d(deg), **name)
468class AuxPhi(AuxAngle):
469 '''A I{Geodetic or Geographic, Auxiliary} latitude.
470 '''
471 _AUX = Aux.PHI
472 _diff = _1_0 # see .auxLat._Newton
474 @staticmethod
475 def fromDegrees(deg, **name):
476 '''Get an L{AuxPhi} from degrees.
477 '''
478 return AuxPhi(*sincos2d(deg), **name)
481class AuxTheta(AuxAngle):
482 '''A I{Geocentric, Auxiliary} latitude.
483 '''
484 _AUX = Aux.THETA
486 @staticmethod
487 def fromDegrees(deg, **name):
488 '''Get an L{AuxTheta} from degrees.
489 '''
490 return AuxTheta(*sincos2d(deg), **name)
493class AuxXi(AuxAngle):
494 '''An I{Authalic, Auxiliary} latitude.
495 '''
496 _AUX = Aux.XI
498 @staticmethod
499 def fromDegrees(deg, **name):
500 '''Get an L{AuxXi} from degrees.
501 '''
502 return AuxXi(*sincos2d(deg), **name)
505_AUXClass = {Aux.BETA: AuxBeta,
506 Aux.CHI: AuxChi,
507 Aux.MU: AuxMu,
508 Aux.PHI: AuxPhi,
509 Aux.THETA: AuxTheta,
510 Aux.XI: AuxXi}
512def _AuxClass(aux=None, **unused): # PYCHOK C{classof(aux)}
513 return _AUXClass.get(aux, AuxAngle)
516def _yx2(yx):
517 try:
518 y, x = map(float, yx)
519 if y in _0_INF_NAN_NINF:
520 x = _copysign_1_0(x)
521 except (TypeError, ValueError) as e:
522 y, x = yx
523 raise AuxError(y=y, x=x, cause=e)
524 return y, x
527__all__ += _ALL_DOCS(AuxAngle, AuxBeta, AuxChi, AuxMu, AuxPhi, AuxTheta, AuxXi)
529# **) MIT License
530#
531# Copyright (C) 2023-2024 -- mrJean1 at Gmail -- All Rights Reserved.
532#
533# Permission is hereby granted, free of charge, to any person obtaining a
534# copy of this software and associated documentation files (the "Software"),
535# to deal in the Software without restriction, including without limitation
536# the rights to use, copy, modify, merge, publish, distribute, sublicense,
537# and/or sell copies of the Software, and to permit persons to whom the
538# Software is furnished to do so, subject to the following conditions:
539#
540# The above copyright notice and this permission notice shall be included
541# in all copies or substantial portions of the Software.
542#
543# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
544# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
545# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
546# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
547# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
548# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
549# OTHER DEALINGS IN THE SOFTWARE.