Coverage for pygeodesy/auxilats/auxAngle.py: 83%
223 statements
« prev ^ index » next coverage.py v7.2.2, created at 2023-08-12 12:31 -0400
« prev ^ index » next coverage.py v7.2.2, created at 2023-08-12 12:31 -0400
2# -*- coding: utf-8 -*-
4u'''(INTERNAL) Private I{Auxiliary} 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:Charles@Karney.com>} (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, AuxError, _Aux2Greek
18from pygeodesy.basics import _xinstanceof, _xkwds_get
19from pygeodesy.constants import EPS, MAX, NAN, _INF_NAN_NINF, _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_, _isub_op_, _sub_op_, _Named
24from pygeodesy.interns import NN, _iadd_op_ # _near_
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__ = '23.08.09'
37_MAX_2 = MAX * _0_5 # PYCHOK used!
38# del 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=NN, **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: Optional name (C{str}).
58 @kwarg aux: I{Auxiliary} kind (C{Aux.KIND}), ignored if B{C{y_angle}}
59 is non-C{scalar}.
61 @raise AuxError: Invalid B{C{y_angle}}, B{C{x}} or B{C{aux}}.
62 '''
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 aux = _xkwds_get(aux, aux=self._AUX)
73 if self._AUX is not aux:
74 if aux not in _AUXClass:
75 raise AuxError(aux=aux)
76 self._AUX = aux
77 self._y, self._x = _yx2(yx)
78 if name:
79 self.name = name
81 def __abs__(self):
82 '''Return this angle' absolute value (L{AuxAngle}).
83 '''
84 a = self._copy_2(self.__abs__)
85 a._yx = map(fabs, self._yx)
86 return a
88 def __add__(self, other):
89 '''Return C{B{self} + B{other}} as an L{AuxAngle}.
91 @arg other: An L{AuxAngle}.
93 @return: The sum (L{AuxAngle}).
95 @raise TypeError: Invalid B{C{other}}.
96 '''
97 a = self._copy_2(self.__add__)
98 return a._iadd(other, _add_op_)
100 def __bool__(self): # PYCHOK not special in Python 2-
101 '''Return C{True} if this angle is non-zero.
102 '''
103 return bool(self.tan)
105 def __eq__(self, other):
106 '''Return C{B{self} == B{other}} as C{bool}.
107 '''
108 return not self.__ne__(other)
110 def __float__(self):
111 '''Return this angle's C{tan} (C{float}).
112 '''
113 return self.tan
115 def __iadd__(self, other):
116 '''Apply C{B{self} += B{other}} to this angle.
118 @arg other: An L{AuxAngle}.
120 @return: This angle, updated (L{AuxAngle}).
122 @raise TypeError: Invalid B{C{other}}.
123 '''
124 return self._iadd(other, _iadd_op_)
126 def __isub__(self, other):
127 '''Apply C{B{self} -= B{other}} to this angle.
129 @arg other: An L{AuxAngle}.
131 @return: This instance, updated (L{AuxAngle}).
133 @raise TypeError: Invalid B{C{other}} type.
134 '''
135 return self._iadd(-other, _isub_op_)
137 def __ne__(self, other):
138 '''Return C{B{self} != B{other}} as C{bool}.
139 '''
140 _xinstanceof(AuxAngle, other=other)
141 y, x, r = self._yxr_normalized
142 s, c, t = other._yxr_normalized
143 return fabs(y - s) > EPS or fabs(x - c) > EPS \
144 or fabs(r - t) > EPS
146 def __neg__(self):
147 '''Return I{a copy of} this angle, negated.
148 '''
149 a = self._copy_2(self.__neg__)
150 if a.y or not a.x:
151 a.y = -a.y
152 else:
153 a.x = -a.x
154 return a
156 def __pos__(self):
157 '''Return this angle I{as-is}, like C{float.__pos__()}.
158 '''
159 return self if _pos_self else self._copy_2(self.__pos__)
161 def __radd__(self, other):
162 '''Return C{B{other} + B{self}} as an L{AuxAngle}.
164 @see: Method L{AuxAngle.__add__}.
165 '''
166 a = self._copy_r2(other, self.__radd__)
167 return a._iadd(self, _add_op_)
169 def __rsub__(self, other):
170 '''Return C{B{other} - B{self}} as an L{AuxAngle}.
172 @see: Method L{AuxAngle.__sub__}.
173 '''
174 a = self._copy_r2(other, self.__rsub__)
175 return a._iadd(-self, _sub_op_)
177 def __str__(self):
178 n = _Aux2Greek.get(self._AUX, self.classname)
179 return unstr(n, y=self.y, x=self.x, tan=self.tan)
181 def __sub__(self, other):
182 '''Return C{B{self} - B{other}} as an L{AuxAngle}.
184 @arg other: An L{AuxAngle}.
186 @return: The difference (L{AuxAngle}).
188 @raise TypeError: Invalid B{C{other}} type.
189 '''
190 a = self._copy_2(self.__sub__)
191 return a._iadd(-other, _sub_op_)
193 def _iadd(self, other, *unused): # op
194 '''(INTERNAL) Apply C{B{self} += B{other}}.
195 '''
196 _xinstanceof(AuxAngle, other=other)
197 # ignore zero other to preserve signs of _y and _x
198 if other.tan:
199 s, c = other._yx
200 y, x = self._yx
201 self._yx = (y * c + x * s), \
202 (x * c - y * s)
203 return self
205 def _copy_2(self, which):
206 '''(INTERNAL) Copy for I{dyadic} operators.
207 '''
208 return _Named.copy(self, deep=False, name=which.__name__)
210 def _copy_r2(self, other, which):
211 '''(INTERNAL) Copy for I{reverse-dyadic} operators.
212 '''
213 _xinstanceof(AuxAngle, other=other)
214 return other._copy_2(which)
216 def copyquadrant(self, other):
217 '''Copy the B{C{other}}'s quadrant into this angle (L{auxAngle}).
218 '''
219 _xinstanceof(AuxAngle, other=other)
220 self._yx = copysign(self.y, other.y), \
221 copysign(self.x, other.x)
222 return self
224 @Property_RO
225 def diff(self):
226 '''Get derivative C{dtan(Eta) / dtan(Phi)} (C{float} or C{NAN}).
227 '''
228 return self._diff
230 @staticmethod
231 def fromDegrees(deg, **name_aux):
232 '''Get an L{AuxAngle} from degrees.
233 '''
234 return _AuxClass(**name_aux)(*sincos2d(deg), **name_aux)
236 @staticmethod
237 def fromLambertianDegrees(psi, **name_aux):
238 '''Get an L{AuxAngle} from I{Lambertian} degrees.
239 '''
240 return _AuxClass(**name_aux)(sinh(radians(psi)), **name_aux)
242 @staticmethod
243 def fromLambertianRadians(psi, **name_aux):
244 '''Get an L{AuxAngle} from I{Lambertian} radians.
245 '''
246 return _AuxClass(**name_aux)(sinh(psi), **name_aux)
248 @staticmethod
249 def fromRadians(rad, **name_aux):
250 '''Get an L{AuxAngle} from radians.
251 '''
252 return _AuxClass(**name_aux)(*sincos2(rad), **name_aux)
254 @Property_RO
255 def iteration(self):
256 '''Get the iteration (C{int} or C{None}).
257 '''
258 return self._iter
260 def normal(self):
261 '''Normalize this angle I{in-place}.
263 @return: This angle, normalized (L{AuxAngle}).
264 '''
265 self._yx = self._yx_normalized
266 return self
268 @Property_RO
269 def normalized(self):
270 '''Get a normalized copy of this angle (L{AuxAngle}).
271 '''
272 y, x = self._yx_normalized
273 return self.classof(y, x, name=self.name, aux=self._AUX)
275 @property_RO
276 def _RhumbAux(self):
277 '''(INTERNAL) Import the L{RhumbAux} class, I{once}.
278 '''
279 AuxAngle._RhumbAux = R = _MODS.rhumbaux.RhumbAux # overwrite property_RO
280 return R
282 @Property_RO
283 def tan(self):
284 '''Get this angle's C{tan} (C{float}).
285 '''
286 y, x = self._yx
287 return _over(y, x) if isfinite(y) and y else y
289 def toBeta(self, rhumb):
290 '''Short for C{rhumb.auxDLat.convert(Aux.BETA, self, exact=rhumb.exact)}
291 '''
292 return self._toRhumbAux(rhumb, Aux.BETA)
294 def toChi(self, rhumb):
295 '''Short for C{rhumb.auxDLat.convert(Aux.CHI, self, exact=rhumb.exact)}
296 '''
297 return self._toRhumbAux(rhumb, Aux.CHI)
299 @Property_RO
300 def toDegrees(self):
301 '''Get this angle as L{Degrees}.
302 '''
303 return Degrees(atan2d(*self._yx), name=self.name)
305 @Property_RO
306 def toLambertianDegrees(self): # PYCHOK no cover
307 '''Get this angle's I{Lambertian} in L{Degrees}.
308 '''
309 r = self.toLambertianRadians
310 return Degrees(degrees(r), name=r.name)
312 @Property_RO
313 def toLambertianRadians(self):
314 '''Get this angle's I{Lambertian} in L{Radians}.
315 '''
316 return Radians(asinh(self.tan), name=self.name)
318 def toMu(self, rhumb):
319 '''Short for C{rhumb.auxDLat.convert(Aux.MU, self, exact=rhumb.exact)}
320 '''
321 return self._toRhumbAux(rhumb, Aux.MU)
323 def toPhi(self, rhumb):
324 '''Short for C{rhumb.auxDLat.convert(Aux.PHI, self, exact=rhumb.exact)}
325 '''
326 return self._toRhumbAux(rhumb, Aux.PHI)
328 @Property_RO
329 def toRadians(self):
330 '''Get this angle as L{Radians}.
331 '''
332 return Radians(atan2(*self._yx), name=self.name)
334 def _toRhumbAux(self, rhumb, aux):
335 '''(INTERNAL) Create an C{aux}-KIND angle from this angle.
336 '''
337 _xinstanceof(self._RhumbAux, rhumb=rhumb)
338 return rhumb._auxD.convert(aux, self, exact=rhumb.exact)
340 @Property
341 def x(self):
342 '''Get this angle's C{x} (C{float}).
343 '''
344 return self._x
346 @x.setter # PYCHOK setter!
347 def x(self, x): # PYCHOK no cover
348 '''Set this angle's C{x} (C{float}).
349 '''
350 x = float(x)
351 if self.x != x:
352 _update_all(self)
353 self._x = x
355 @property_RO
356 def _x_normalized(self):
357 '''(INTERNAL) Get the I{normalized} C{x}.
358 '''
359 _, x = self._yx_normalized
360 return x
362 @Property
363 def y(self):
364 '''Get this angle's C{y} (C{float}).
365 '''
366 return self._y
368 @y.setter # PYCHOK setter!
369 def y(self, y): # PYCHOK no cover
370 '''Set this angle's C{y} (C{float}).
371 '''
372 y = float(y)
373 if self.y != y:
374 _update_all(self)
375 self._y = y
377 @Property
378 def _yx(self):
379 '''(INTERNAL) Get this angle as 2-tuple C{(y, x)}.
380 '''
381 return self._y, self._x
383 @_yx.setter # PYCHOK setter!
384 def _yx(self, yx):
385 '''(INTERNAL) Set this angle's C{y} and C{x}.
386 '''
387 yx = _yx2(yx)
388 if self._yx != yx:
389 _update_all(self)
390 self._y, self._x = yx
392 @Property_RO
393 def _yx_normalized(self):
394 '''(INTERNAL) Get this angle as 2-tuple C{(y, x)}, I{normalized}.
395 '''
396 y, x = self._yx
397 if isfinite(y) and fabs(y) < _MAX_2 \
398 and fabs(x) < _MAX_2 \
399 and isfinite(self.tan):
400 h = hypot(y, x)
401 if h > 0:
402 y = y / h # /= chokes PyChecker
403 x = x / h
404 if isnan(y): # PYCHOK no cover
405 y = _copysign_1_0(self.y)
406 if isnan(x): # PYCHOK no cover
407 x = _copysign_1_0(self.x)
408 else: # scalar 0
409 y, x = _0_0, _copysign_1_0(y * x)
410 else: # scalar NAN
411 y, x = NAN, _copysign_1_0(y * x)
412 return y, x
414 def _yxr_normalized(self, abs_y=False):
415 '''(INTERNAL) Get 3-tuple C{(y, x, r)}, I{normalized}
416 with C{y} or C{abs(y)} and C{r} as C{.toRadians}.
417 '''
418 y, x = self._yx_normalized
419 if abs_y:
420 y = fabs(y) # only y, not x
421 return y, x, atan2(y, x) # .toRadians
424class AuxBeta(AuxAngle):
425 '''A I{Parametric, Auxiliary} latitude.
426 '''
427 _AUX = Aux.BETA
429 @staticmethod
430 def fromDegrees(deg, name=NN):
431 '''Get an L{AuxBeta} from degrees.
432 '''
433 return AuxBeta(*sincos2d(deg), name=name)
435 @staticmethod
436 def fromRadians(rad, name=NN):
437 '''Get an L{AuxBeta} from radians.
438 '''
439 return AuxBeta(*sincos2(rad), name=name)
442class AuxChi(AuxAngle):
443 '''A I{Conformal, Auxiliary} latitude.
444 '''
445 _AUX = Aux.CHI
447 @staticmethod
448 def fromDegrees(deg, name=NN):
449 '''Get an L{AuxChi} from degrees.
450 '''
451 return AuxChi(*sincos2d(deg), name=name)
454class AuxMu(AuxAngle):
455 '''A I{Rectifying, Auxiliary} latitude.
456 '''
457 _AUX = Aux.MU
459 @staticmethod
460 def fromDegrees(deg, name=NN):
461 '''Get an L{AuxMu} from degrees.
462 '''
463 return AuxMu(*sincos2d(deg), name=name)
466class AuxPhi(AuxAngle):
467 '''A I{Geodetic or Geographic, Auxiliary} latitude.
468 '''
469 _AUX = Aux.PHI
470 _diff = _1_0 # see .auxLat._Newton
472 @staticmethod
473 def fromDegrees(deg, name=NN):
474 '''Get an L{AuxPhi} from degrees.
475 '''
476 return AuxPhi(*sincos2d(deg), name=name)
479class AuxTheta(AuxAngle):
480 '''A I{Geocentric, Auxiliary} latitude.
481 '''
482 _AUX = Aux.THETA
484 @staticmethod
485 def fromDegrees(deg, name=NN):
486 '''Get an L{AuxTheta} from degrees.
487 '''
488 return AuxTheta(*sincos2d(deg), name=name)
491class AuxXi(AuxAngle):
492 '''An I{Authalic, Auxiliary} latitude.
493 '''
494 _AUX = Aux.XI
496 @staticmethod
497 def fromDegrees(deg, name=NN):
498 '''Get an L{AuxXi} from degrees.
499 '''
500 return AuxXi(*sincos2d(deg), name=name)
503_AUXClass = {Aux.BETA: AuxBeta,
504 Aux.CHI: AuxChi,
505 Aux.MU: AuxMu,
506 Aux.PHI: AuxPhi,
507 Aux.THETA: AuxTheta,
508 Aux.XI: AuxXi}
510def _AuxClass(aux=None, **unused): # PYCHOK C{classof(aux)}
511 return _AUXClass.get(aux, AuxAngle)
514def _yx2(yx):
515 try:
516 y, x = map(float, yx)
517 if y in _INF_NAN_NINF: # PYCHOK no cover
518 x = _copysign_1_0(x)
519 except (TypeError, ValueError) as e:
520 y, x = yx
521 raise AuxError(y=y, x=x, cause=e)
522 return y, x
525__all__ += _ALL_DOCS(AuxAngle, AuxBeta, AuxMu, AuxPhi)
527# **) MIT License
528#
529# Copyright (C) 2023-2023 -- mrJean1 at Gmail -- All Rights Reserved.
530#
531# Permission is hereby granted, free of charge, to any person obtaining a
532# copy of this software and associated documentation files (the "Software"),
533# to deal in the Software without restriction, including without limitation
534# the rights to use, copy, modify, merge, publish, distribute, sublicense,
535# and/or sell copies of the Software, and to permit persons to whom the
536# Software is furnished to do so, subject to the following conditions:
537#
538# The above copyright notice and this permission notice shall be included
539# in all copies or substantial portions of the Software.
540#
541# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
542# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
543# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
544# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
545# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
546# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
547# OTHER DEALINGS IN THE SOFTWARE.