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

1 

2# -*- coding: utf-8 -*- 

3 

4u'''(INTERNAL) Private I{Auxiliary} base classes, constants and functions. 

5 

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+}. 

9 

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 

16 

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 

31 

32from math import asinh, atan2, copysign, degrees, fabs, radians, sinh 

33 

34__all__ = () 

35__version__ = '23.08.09' 

36 

37_MAX_2 = MAX * _0_5 # PYCHOK used! 

38# del MAX 

39 

40 

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 

50 

51 def __init__(self, y_angle=_0_0, x=_1_0, name=NN, **aux): 

52 '''New L{AuxAngle}. 

53 

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}. 

60 

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 

80 

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 

87 

88 def __add__(self, other): 

89 '''Return C{B{self} + B{other}} as an L{AuxAngle}. 

90 

91 @arg other: An L{AuxAngle}. 

92 

93 @return: The sum (L{AuxAngle}). 

94 

95 @raise TypeError: Invalid B{C{other}}. 

96 ''' 

97 a = self._copy_2(self.__add__) 

98 return a._iadd(other, _add_op_) 

99 

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) 

104 

105 def __eq__(self, other): 

106 '''Return C{B{self} == B{other}} as C{bool}. 

107 ''' 

108 return not self.__ne__(other) 

109 

110 def __float__(self): 

111 '''Return this angle's C{tan} (C{float}). 

112 ''' 

113 return self.tan 

114 

115 def __iadd__(self, other): 

116 '''Apply C{B{self} += B{other}} to this angle. 

117 

118 @arg other: An L{AuxAngle}. 

119 

120 @return: This angle, updated (L{AuxAngle}). 

121 

122 @raise TypeError: Invalid B{C{other}}. 

123 ''' 

124 return self._iadd(other, _iadd_op_) 

125 

126 def __isub__(self, other): 

127 '''Apply C{B{self} -= B{other}} to this angle. 

128 

129 @arg other: An L{AuxAngle}. 

130 

131 @return: This instance, updated (L{AuxAngle}). 

132 

133 @raise TypeError: Invalid B{C{other}} type. 

134 ''' 

135 return self._iadd(-other, _isub_op_) 

136 

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 

145 

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 

155 

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__) 

160 

161 def __radd__(self, other): 

162 '''Return C{B{other} + B{self}} as an L{AuxAngle}. 

163 

164 @see: Method L{AuxAngle.__add__}. 

165 ''' 

166 a = self._copy_r2(other, self.__radd__) 

167 return a._iadd(self, _add_op_) 

168 

169 def __rsub__(self, other): 

170 '''Return C{B{other} - B{self}} as an L{AuxAngle}. 

171 

172 @see: Method L{AuxAngle.__sub__}. 

173 ''' 

174 a = self._copy_r2(other, self.__rsub__) 

175 return a._iadd(-self, _sub_op_) 

176 

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) 

180 

181 def __sub__(self, other): 

182 '''Return C{B{self} - B{other}} as an L{AuxAngle}. 

183 

184 @arg other: An L{AuxAngle}. 

185 

186 @return: The difference (L{AuxAngle}). 

187 

188 @raise TypeError: Invalid B{C{other}} type. 

189 ''' 

190 a = self._copy_2(self.__sub__) 

191 return a._iadd(-other, _sub_op_) 

192 

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 

204 

205 def _copy_2(self, which): 

206 '''(INTERNAL) Copy for I{dyadic} operators. 

207 ''' 

208 return _Named.copy(self, deep=False, name=which.__name__) 

209 

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) 

215 

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 

223 

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 

229 

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) 

235 

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) 

241 

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) 

247 

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) 

253 

254 @Property_RO 

255 def iteration(self): 

256 '''Get the iteration (C{int} or C{None}). 

257 ''' 

258 return self._iter 

259 

260 def normal(self): 

261 '''Normalize this angle I{in-place}. 

262 

263 @return: This angle, normalized (L{AuxAngle}). 

264 ''' 

265 self._yx = self._yx_normalized 

266 return self 

267 

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) 

274 

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 

281 

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 

288 

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) 

293 

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) 

298 

299 @Property_RO 

300 def toDegrees(self): 

301 '''Get this angle as L{Degrees}. 

302 ''' 

303 return Degrees(atan2d(*self._yx), name=self.name) 

304 

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) 

311 

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) 

317 

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) 

322 

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) 

327 

328 @Property_RO 

329 def toRadians(self): 

330 '''Get this angle as L{Radians}. 

331 ''' 

332 return Radians(atan2(*self._yx), name=self.name) 

333 

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) 

339 

340 @Property 

341 def x(self): 

342 '''Get this angle's C{x} (C{float}). 

343 ''' 

344 return self._x 

345 

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 

354 

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 

361 

362 @Property 

363 def y(self): 

364 '''Get this angle's C{y} (C{float}). 

365 ''' 

366 return self._y 

367 

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 

376 

377 @Property 

378 def _yx(self): 

379 '''(INTERNAL) Get this angle as 2-tuple C{(y, x)}. 

380 ''' 

381 return self._y, self._x 

382 

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 

391 

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 

413 

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 

422 

423 

424class AuxBeta(AuxAngle): 

425 '''A I{Parametric, Auxiliary} latitude. 

426 ''' 

427 _AUX = Aux.BETA 

428 

429 @staticmethod 

430 def fromDegrees(deg, name=NN): 

431 '''Get an L{AuxBeta} from degrees. 

432 ''' 

433 return AuxBeta(*sincos2d(deg), name=name) 

434 

435 @staticmethod 

436 def fromRadians(rad, name=NN): 

437 '''Get an L{AuxBeta} from radians. 

438 ''' 

439 return AuxBeta(*sincos2(rad), name=name) 

440 

441 

442class AuxChi(AuxAngle): 

443 '''A I{Conformal, Auxiliary} latitude. 

444 ''' 

445 _AUX = Aux.CHI 

446 

447 @staticmethod 

448 def fromDegrees(deg, name=NN): 

449 '''Get an L{AuxChi} from degrees. 

450 ''' 

451 return AuxChi(*sincos2d(deg), name=name) 

452 

453 

454class AuxMu(AuxAngle): 

455 '''A I{Rectifying, Auxiliary} latitude. 

456 ''' 

457 _AUX = Aux.MU 

458 

459 @staticmethod 

460 def fromDegrees(deg, name=NN): 

461 '''Get an L{AuxMu} from degrees. 

462 ''' 

463 return AuxMu(*sincos2d(deg), name=name) 

464 

465 

466class AuxPhi(AuxAngle): 

467 '''A I{Geodetic or Geographic, Auxiliary} latitude. 

468 ''' 

469 _AUX = Aux.PHI 

470 _diff = _1_0 # see .auxLat._Newton 

471 

472 @staticmethod 

473 def fromDegrees(deg, name=NN): 

474 '''Get an L{AuxPhi} from degrees. 

475 ''' 

476 return AuxPhi(*sincos2d(deg), name=name) 

477 

478 

479class AuxTheta(AuxAngle): 

480 '''A I{Geocentric, Auxiliary} latitude. 

481 ''' 

482 _AUX = Aux.THETA 

483 

484 @staticmethod 

485 def fromDegrees(deg, name=NN): 

486 '''Get an L{AuxTheta} from degrees. 

487 ''' 

488 return AuxTheta(*sincos2d(deg), name=name) 

489 

490 

491class AuxXi(AuxAngle): 

492 '''An I{Authalic, Auxiliary} latitude. 

493 ''' 

494 _AUX = Aux.XI 

495 

496 @staticmethod 

497 def fromDegrees(deg, name=NN): 

498 '''Get an L{AuxXi} from degrees. 

499 ''' 

500 return AuxXi(*sincos2d(deg), name=name) 

501 

502 

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} 

509 

510def _AuxClass(aux=None, **unused): # PYCHOK C{classof(aux)} 

511 return _AUXClass.get(aux, AuxAngle) 

512 

513 

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 

523 

524 

525__all__ += _ALL_DOCS(AuxAngle, AuxBeta, AuxMu, AuxPhi) 

526 

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.