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

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.06' 

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

50 

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

53 

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. 

61 

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 

79 

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 

86 

87 def __add__(self, other): 

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

89 

90 @arg other: An L{AuxAngle}. 

91 

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

93 

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

95 ''' 

96 a = self._copy_2(self.__add__) 

97 return a._iadd(other, _add_op_) 

98 

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) 

103 

104 def __eq__(self, other): 

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

106 ''' 

107 return not self.__ne__(other) 

108 

109 def __float__(self): 

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

111 ''' 

112 return self.tan 

113 

114 def __iadd__(self, other): 

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

116 

117 @arg other: An L{AuxAngle}. 

118 

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

120 

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

122 ''' 

123 return self._iadd(other, _iadd_op_) 

124 

125 def __isub__(self, other): 

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

127 

128 @arg other: An L{AuxAngle}. 

129 

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

131 

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

133 ''' 

134 return self._iadd(-other, _isub_op_) 

135 

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 

144 

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 

154 

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

159 

160 def __radd__(self, other): 

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

162 

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

164 ''' 

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

166 return a._iadd(self, _add_op_) 

167 

168 def __rsub__(self, other): 

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

170 

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

172 ''' 

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

174 return a._iadd(-self, _sub_op_) 

175 

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) 

179 

180 def __sub__(self, other): 

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

182 

183 @arg other: An L{AuxAngle}. 

184 

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

186 

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

188 ''' 

189 a = self._copy_2(self.__sub__) 

190 return a._iadd(-other, _sub_op_) 

191 

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 

203 

204 def _copy_2(self, which): 

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

206 ''' 

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

208 

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) 

214 

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 

222 

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 

228 

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) 

234 

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) 

240 

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) 

246 

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) 

252 

253 @Property_RO 

254 def iteration(self): 

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

256 ''' 

257 return self._iter 

258 

259 def normal(self): 

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

261 

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

263 ''' 

264 self._yx = self._yx_normalized 

265 return self 

266 

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) 

273 

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 

280 

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) 

285 

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) 

290 

291 @Property_RO 

292 def toDegrees(self): 

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

294 ''' 

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

296 

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) 

303 

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) 

309 

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) 

314 

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) 

319 

320 @Property_RO 

321 def toRadians(self): 

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

323 ''' 

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

325 

326 def _toRhumbAux(self, rhumb, aux): 

327 _xinstanceof(_MODS.rhumbaux.RhumbAux, rhumb=rhumb) 

328 return rhumb._auxD.convert(aux, self, exact=rhumb.exact) 

329 

330 @Property 

331 def x(self): 

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

333 ''' 

334 return self._x 

335 

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 

344 

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 

351 

352 @Property 

353 def y(self): 

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

355 ''' 

356 return self._y 

357 

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 

366 

367 @Property 

368 def _yx(self): 

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

370 ''' 

371 return self._y, self._x 

372 

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 

381 

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 

403 

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 

412 

413 

414class AuxBeta(AuxAngle): 

415 '''An I{Prametric} latitude. 

416 ''' 

417 _AUX = Aux.BETA 

418 

419 @staticmethod 

420 def fromDegrees(deg, name=NN): 

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

422 ''' 

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

424 

425 @staticmethod 

426 def fromRadians(rad, name=NN): 

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

428 ''' 

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

430 

431 

432class AuxChi(AuxAngle): 

433 '''An I{Conformal} latitude. 

434 ''' 

435 _AUX = Aux.CHI 

436 

437 @staticmethod 

438 def fromDegrees(deg, name=NN): 

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

440 ''' 

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

442 

443 

444class AuxMu(AuxAngle): 

445 '''An I{Rectifying, Auxiliary} latitude. 

446 ''' 

447 _AUX = Aux.MU 

448 

449 @staticmethod 

450 def fromDegrees(deg, name=NN): 

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

452 ''' 

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

454 

455 

456class AuxPhi(AuxAngle): 

457 '''An I{Geographic, Auxiliary} latitude. 

458 ''' 

459 _AUX = Aux.PHI 

460 

461 @staticmethod 

462 def fromDegrees(deg, name=NN): 

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

464 ''' 

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

466 

467 

468class AuxTheta(AuxAngle): 

469 '''An I{Geocentric} latitude. 

470 ''' 

471 _AUX = Aux.THETA 

472 

473 @staticmethod 

474 def fromDegrees(deg, name=NN): 

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

476 ''' 

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

478 

479 

480class AuxXi(AuxAngle): 

481 '''An I{Authalic} latitude. 

482 ''' 

483 _AUX = Aux.XI 

484 

485 @staticmethod 

486 def fromDegrees(deg, name=NN): 

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

488 ''' 

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

490 

491 

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} 

498 

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

500 return _AUXClass.get(aux, AuxAngle) 

501 

502 

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 

512 

513 

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

515 

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.