Coverage for pygeodesy/auxilats/auxAngle.py: 83%
218 statements
« prev ^ index » next coverage.py v7.2.2, created at 2023-08-07 07:28 -0400
« prev ^ index » next coverage.py v7.2.2, created at 2023-08-07 07:28 -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.06'
37_MAX_2 = MAX * _0_5 # PYCHOK used!
38# del MAX
41class AuxAngle(_Named):
42 '''U{An accurate representation of angles
43 <https://GeographicLib.DourceForge.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} from C{degrees}, C{radians} or C{scalars}.
54 @kwarg y_angle: The Y component (C{scalar}, including C{INF},
55 C{NAN} and C{NINF}) or a previous L{AuxAngle}
56 instance.
57 @kwarg x: The X component, ignored if C{B{y_angle}} si non-scalar.
58 @kwarg name: Optional name (C{str}).
59 @kwarg aux: Optional I{Auxiliary} kind (C{Aux.KIND}), ignored if
60 B{C{y_angle}} is non-scalar.
62 @raise AuxError: Invalid B{C{y_angle}} or B{C{x}} or both.
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 self._AUX = aux
76 self._y, self._x = _yx2(yx)
77 if name:
78 self.name = name
80 def __abs__(self):
81 '''Return this angle' absolute value (L{AuxAngle}).
82 '''
83 a = self._copy_2(self.__abs__)
84 a._yx = map(fabs, self._yx)
85 return a
87 def __add__(self, other):
88 '''Return C{B{self} + B{other}} as an L{AuxAngle}.
90 @arg other: An L{AuxAngle}.
92 @return: The sum (L{AuxAngle}).
94 @raise TypeError: Invalid B{C{other}}.
95 '''
96 a = self._copy_2(self.__add__)
97 return a._iadd(other, _add_op_)
99 def __bool__(self): # PYCHOK not special in Python 2-
100 '''Return C{True} if this angle is non-zero.
101 '''
102 return bool(self.tan)
104 def __eq__(self, other):
105 '''Return C{B{self} == B{other}} as C{bool}.
106 '''
107 return not self.__ne__(other)
109 def __float__(self):
110 '''Return this angle's C{tan} (C{float}).
111 '''
112 return self.tan
114 def __iadd__(self, other):
115 '''Apply C{B{self} += B{other}} to this angle.
117 @arg other: An L{AuxAngle}.
119 @return: This angle, updated (L{AuxAngle}).
121 @raise TypeError: Invalid B{C{other}}.
122 '''
123 return self._iadd(other, _iadd_op_)
125 def __isub__(self, other):
126 '''Apply C{B{self} -= B{other}} to this angle.
128 @arg other: An L{AuxAngle}.
130 @return: This instance, updated (L{AuxAngle}).
132 @raise TypeError: Invalid B{C{other}} type.
133 '''
134 return self._iadd(-other, _isub_op_)
136 def __ne__(self, other):
137 '''Return C{B{self} != B{other}} as C{bool}.
138 '''
139 _xinstanceof(AuxAngle, other=other)
140 y, x, r = self._yxr_normalized
141 s, c, t = other._yxr_normalized
142 return fabs(y - s) > EPS or fabs(x - c) > EPS \
143 or fabs(r - t) > EPS
145 def __neg__(self):
146 '''Return I{a copy of} this angle, negated.
147 '''
148 a = self._copy_2(self.__neg__)
149 if a.y or not a.x:
150 a.y = -a.y
151 else:
152 a.x = -a.x
153 return a
155 def __pos__(self):
156 '''Return this angle I{as-is}, like C{float.__pos__()}.
157 '''
158 return self if _pos_self else self._copy_2(self.__pos__)
160 def __radd__(self, other):
161 '''Return C{B{other} + B{self}} as an L{AuxAngle}.
163 @see: Method L{AuxAngle.__add__}.
164 '''
165 a = self._copy_r2(other, self.__radd__)
166 return a._iadd(self, _add_op_)
168 def __rsub__(self, other):
169 '''Return C{B{other} - B{self}} as an L{AuxAngle}.
171 @see: Method L{AuxAngle.__sub__}.
172 '''
173 a = self._copy_r2(other, self.__rsub__)
174 return a._iadd(-self, _sub_op_)
176 def __str__(self):
177 n = _Aux2Greek.get(self._AUX, self.classname)
178 return unstr(n, y=self.y, x=self.x, tan=self.tan)
180 def __sub__(self, other):
181 '''Return C{B{self} - B{other}} as an L{AuxAngle}.
183 @arg other: An L{AuxAngle}.
185 @return: The difference (L{AuxAngle}).
187 @raise TypeError: Invalid B{C{other}} type.
188 '''
189 a = self._copy_2(self.__sub__)
190 return a._iadd(-other, _sub_op_)
192 def _iadd(self, other, *unused): # op
193 '''(INTERNAL) Apply C{B{self} += B{other}}.
194 '''
195 _xinstanceof(AuxAngle, other=other)
196 # ignore zero other to preserve signs of _y and _x
197 if other.tan:
198 s, c = other._yx
199 y, x = self._yx
200 self._yx = (y * c + x * s), \
201 (x * c - y * s)
202 return self
204 def _copy_2(self, which):
205 '''(INTERNAL) Copy for I{dyadic} operators.
206 '''
207 return _Named.copy(self, deep=False, name=which.__name__)
209 def _copy_r2(self, other, which):
210 '''(INTERNAL) Copy for I{reverse-dyadic} operators.
211 '''
212 _xinstanceof(AuxAngle, other=other)
213 return other._copy_2(which)
215 def copyquadrant(self, other):
216 '''Copy the B{C{other}}'s quadrant into this angle (L{auxAngle}).
217 '''
218 _xinstanceof(AuxAngle, other=other)
219 self._yx = copysign(self.y, other.y), \
220 copysign(self.x, other.x)
221 return self
223 @Property_RO
224 def diff(self):
225 '''Get derivative C{dtan(Eta) / dtan(Phi)} (C{float} or C{NAN}).
226 '''
227 return self._diff
229 @staticmethod
230 def fromDegrees(deg, **name_aux):
231 '''Get an L{AuxAngle} from degrees.
232 '''
233 return _AuxClass(**name_aux)(*sincos2d(deg), **name_aux)
235 @staticmethod
236 def fromLambertianDegrees(psi, **name_aux):
237 '''Get an L{AuxAngle} from I{Lambertian} degrees.
238 '''
239 return _AuxClass(**name_aux)(sinh(radians(psi)), **name_aux)
241 @staticmethod
242 def fromLambertianRadians(psi, **name_aux):
243 '''Get an L{AuxAngle} from I{Lambertian} radians.
244 '''
245 return _AuxClass(**name_aux)(sinh(psi), **name_aux)
247 @staticmethod
248 def fromRadians(rad, **name_aux):
249 '''Get an L{AuxAngle} from radians.
250 '''
251 return _AuxClass(**name_aux)(*sincos2(rad), **name_aux)
253 @Property_RO
254 def iteration(self):
255 '''Get the iteration (C{int} or C{None}).
256 '''
257 return self._iter
259 def normal(self):
260 '''Normalize this angle I{in-place}.
262 @return: This angle, normalized (L{AuxAngle}).
263 '''
264 self._yx = self._yx_normalized
265 return self
267 @Property_RO
268 def normalized(self):
269 '''Get a normalized copy of this angle (L{AuxAngle}).
270 '''
271 y, x = self._yx_normalized
272 return self.classof(y, x, name=self.name, aux=self._AUX)
274 @Property_RO
275 def tan(self):
276 '''Get this angle's C{tan} (C{float}).
277 '''
278 y, x = self._yx
279 return _over(y, x) if isfinite(y) and y else y
281 def toBeta(self, rhumb):
282 '''Short for C{rhumb.auxDLat.convert(Aux.BETA, self, exact=rhumb.exact)}
283 '''
284 return self._toRhumbAux(rhumb, Aux.BETA)
286 def toChi(self, rhumb):
287 '''Short for C{rhumb.auxDLat.convert(Aux.CHI, self, exact=rhumb.exact)}
288 '''
289 return self._toRhumbAux(rhumb, Aux.CHI)
291 @Property_RO
292 def toDegrees(self):
293 '''Get this angle as L{Degrees}.
294 '''
295 return Degrees(atan2d(*self._yx), name=self.name)
297 @Property_RO
298 def toLambertianDegrees(self): # PYCHOK no cover
299 '''Get this angle's I{Lambertian} in L{Degrees}.
300 '''
301 r = self.toLambertianRadians
302 return Degrees(degrees(r), name=r.name)
304 @Property_RO
305 def toLambertianRadians(self):
306 '''Get this angle's I{Lambertian} in L{Radians}.
307 '''
308 return Radians(asinh(self.tan), name=self.name)
310 def toMu(self, rhumb):
311 '''Short for C{rhumb.auxDLat.convert(Aux.MU, self, exact=rhumb.exact)}
312 '''
313 return self._toRhumbAux(rhumb, Aux.MU)
315 def toPhi(self, rhumb):
316 '''Short for C{rhumb.auxDLat.convert(Aux.PHI, self, exact=rhumb.exact)}
317 '''
318 return self._toRhumbAux(rhumb, Aux.PHI)
320 @Property_RO
321 def toRadians(self):
322 '''Get this angle as L{Radians}.
323 '''
324 return Radians(atan2(*self._yx), name=self.name)
326 def _toRhumbAux(self, rhumb, aux):
327 _xinstanceof(_MODS.rhumbaux.RhumbAux, rhumb=rhumb)
328 return rhumb._auxD.convert(aux, self, exact=rhumb.exact)
330 @Property
331 def x(self):
332 '''Get this angle's C{x} (C{float}).
333 '''
334 return self._x
336 @x.setter # PYCHOK setter!
337 def x(self, x): # PYCHOK no cover
338 '''Set this angle's C{x} (C{float}).
339 '''
340 x = float(x)
341 if self.x != x:
342 _update_all(self)
343 self._x = x
345 @property_RO
346 def _x_normalized(self):
347 '''(INTERNAL) Get the I{normalized} C{x}.
348 '''
349 _, x = self._yx_normalized
350 return x
352 @Property
353 def y(self):
354 '''Get this angle's C{y} (C{float}).
355 '''
356 return self._y
358 @y.setter # PYCHOK setter!
359 def y(self, y): # PYCHOK no cover
360 '''Set this angle's C{y} (C{float}).
361 '''
362 y = float(y)
363 if self.y != y:
364 _update_all(self)
365 self._y = y
367 @Property
368 def _yx(self):
369 '''(INTERNAL) Get this angle as 2-tuple C{(y, x)}.
370 '''
371 return self._y, self._x
373 @_yx.setter # PYCHOK setter!
374 def _yx(self, yx):
375 '''(INTERNAL) Set this angle's C{y} and C{x}.
376 '''
377 yx = _yx2(yx)
378 if self._yx != yx:
379 _update_all(self)
380 self._y, self._x = yx
382 @Property_RO
383 def _yx_normalized(self):
384 '''(INTERNAL) Get this angle as 2-tuple C{(y, x)}, I{normalized}.
385 '''
386 y, x = self._yx
387 if isfinite(y) and fabs(y) < _MAX_2 \
388 and fabs(x) < _MAX_2 \
389 and isfinite(self.tan):
390 h = hypot(y, x)
391 if h > 0:
392 y = y / h # /= chokes PyChecker
393 x = x / h
394 if isnan(y): # PYCHOK no cover
395 y = _copysign_1_0(self.y)
396 if isnan(x): # PYCHOK no cover
397 x = _copysign_1_0(self.x)
398 else: # scalar 0
399 y, x = _0_0, _copysign_1_0(y * x)
400 else: # scalar NAN
401 y, x = NAN, _copysign_1_0(y * x)
402 return y, x
404 def _yxr_normalized(self, abs_y=False):
405 '''(INTERNAL) Get 3-tuple C{(y, x, r)}, I{normalized}
406 with C{y} or C{abs(y)} and C{r} as C{.toRadians}.
407 '''
408 y, x = self._yx_normalized
409 if abs_y:
410 y = fabs(y) # only y, not x
411 return y, x, atan2(y, x) # .toRadians
414class AuxBeta(AuxAngle):
415 '''An I{Prametric} latitude.
416 '''
417 _AUX = Aux.BETA
419 @staticmethod
420 def fromDegrees(deg, name=NN):
421 '''Get an L{AuxBeta} from degrees.
422 '''
423 return AuxBeta(*sincos2d(deg), name=name)
425 @staticmethod
426 def fromRadians(rad, name=NN):
427 '''Get an L{AuxBeta} from radians.
428 '''
429 return AuxBeta(*sincos2(rad), name=name)
432class AuxChi(AuxAngle):
433 '''An I{Conformal} latitude.
434 '''
435 _AUX = Aux.CHI
437 @staticmethod
438 def fromDegrees(deg, name=NN):
439 '''Get an L{AuxChi} from degrees.
440 '''
441 return AuxChi(*sincos2d(deg), name=name)
444class AuxMu(AuxAngle):
445 '''An I{Rectifying, Auxiliary} latitude.
446 '''
447 _AUX = Aux.MU
449 @staticmethod
450 def fromDegrees(deg, name=NN):
451 '''Get an L{AuxMu} from degrees.
452 '''
453 return AuxMu(*sincos2d(deg), name=name)
456class AuxPhi(AuxAngle):
457 '''An I{Geographic, Auxiliary} latitude.
458 '''
459 _AUX = Aux.PHI
461 @staticmethod
462 def fromDegrees(deg, name=NN):
463 '''Get an L{AuxPhi} from degrees.
464 '''
465 return AuxPhi(*sincos2d(deg), name=name)
468class AuxTheta(AuxAngle):
469 '''An I{Geocentric} latitude.
470 '''
471 _AUX = Aux.THETA
473 @staticmethod
474 def fromDegrees(deg, name=NN):
475 '''Get an L{AuxTheta} from degrees.
476 '''
477 return AuxTheta(*sincos2d(deg), name=name)
480class AuxXi(AuxAngle):
481 '''An I{Authalic} latitude.
482 '''
483 _AUX = Aux.XI
485 @staticmethod
486 def fromDegrees(deg, name=NN):
487 '''Get an L{AuxXi} from degrees.
488 '''
489 return AuxXi(*sincos2d(deg), name=name)
492_AUXClass = {Aux.BETA: AuxBeta,
493 Aux.CHI: AuxChi,
494 Aux.MU: AuxMu,
495 Aux.PHI: AuxPhi,
496 Aux.THETA: AuxTheta,
497 Aux.XI: AuxXi}
499def _AuxClass(aux=None, **unused): # PYCHOK C{classof(aux)}
500 return _AUXClass.get(aux, AuxAngle)
503def _yx2(yx):
504 try:
505 y, x = map(float, yx)
506 if y in _INF_NAN_NINF: # PYCHOK no cover
507 x = _copysign_1_0(x)
508 except (TypeError, ValueError) as e:
509 y, x = yx
510 raise AuxError(y=y, x=x, cause=e)
511 return y, x
514__all__ += _ALL_DOCS(AuxAngle, AuxBeta, AuxMu, AuxPhi)
516# **) MIT License
517#
518# Copyright (C) 2023-2023 -- mrJean1 at Gmail -- All Rights Reserved.
519#
520# Permission is hereby granted, free of charge, to any person obtaining a
521# copy of this software and associated documentation files (the "Software"),
522# to deal in the Software without restriction, including without limitation
523# the rights to use, copy, modify, merge, publish, distribute, sublicense,
524# and/or sell copies of the Software, and to permit persons to whom the
525# Software is furnished to do so, subject to the following conditions:
526#
527# The above copyright notice and this permission notice shall be included
528# in all copies or substantial portions of the Software.
529#
530# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
531# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
532# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
533# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
534# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
535# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
536# OTHER DEALINGS IN THE SOFTWARE.