1

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

3

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

5

6 Class L{AuxAngle} transcoded to Python from I{Karney}'s C++ class U{AuxAngle

7<https://GeographicLib.SourceForge.io/C++/doc/classGeographicLib_1_1AuxAngle.html>}

8 in I{GeographicLib version 2.2+}.

9

10 Copyright (C) U{Charles Karney<mailto:Charles@Karney.com>} (2022-2023) and licensed

11 under 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

15 from __future__ import division as _; del _ # PYCHOK semicolon

16

17 from pygeodesy.auxilats.auxily import Aux, AuxError, _Aux2Greek

18 from pygeodesy.basics import _xinstanceof, _xkwds_get

19 from 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

22 from pygeodesy.fmath import hypot, unstr

23 from pygeodesy.fsums import _add_op_, _isub_op_, _sub_op_, _Named

24 from pygeodesy.interns import NN, _iadd_op_ # _near_

25# from pygeodesy.named import _Named # from .fsums

26 from pygeodesy.lazily import _ALL_DOCS, _ALL_MODS as _MODS

27 from pygeodesy.props import Property, Property_RO, property_RO, _update_all

28# from pygeodesy.streprs import unstr # from .fmath

29 from pygeodesy.units import Degrees, Radians

30 from pygeodesy.utily import atan2d, sincos2, sincos2d

31

32 from 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

41 class 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

424 class 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

442 class 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

454 class 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

466 class 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

479 class 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

491 class 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

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

511 return _AUXClass.get(aux, AuxAngle)

512

513

514 def _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.