1

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

3

4 u'''(INTERNAL) I{Auxiliary} latitudes' 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:Karney@Alum.MIT.edu>} (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, _Aux2Greek

18 from pygeodesy.basics import _xinstanceof

19 from pygeodesy.constants import EPS, _INF_NAN_NINF, MAX, NAN, _0_0, _0_5, _1_0, \

20 _copysign_1_0, _over, _pos_self, isfinite, isnan

21 from pygeodesy.errors import AuxError, _UnexpectedError, _xkwds_pop2

22 from pygeodesy.fmath import hypot, unstr

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

24 from pygeodesy.named import _name2__, _Named, _ALL_DOCS, _MODS

25# from pygeodesy.lazily import _ALL_DOCS, _ALL_MODS as _MODS # from .named

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

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

28 from pygeodesy.units import Degrees, Radians

29 from pygeodesy.utily import atan2d, sincos2, sincos2d

30

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

32

33__all__ = ()

34__version__ = '24.05.25'

35

36_0_INF_NAN_NINF = (0, _0_0) + _INF_NAN_NINF

37_MAX_2 = MAX * _0_5 # PYCHOK used!

38# del _INF_NAN_NINF, 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_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_aux: Optional C{B{name}=NN} (C{str}) and I{Auxiliary} kind

58 (C{B{aux}=Aux.KIND}), ignored if B{C{y_angle}} is L{AuxAngle}.

59

60 @raise AuxError: Invalid B{C{y_angle}}, B{C{x}} or B{C{aux}}.

61 '''

62 name, aux = _name2__(**name_aux)

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 a, kwds = _xkwds_pop2(aux, aux=self._AUX)

73 if kwds:

74 raise _UnexpectedError(**kwds)

75 if a is not self._AUX:

76 if a not in _AUXClass:

77 raise AuxError(aux=a)

78 self._AUX = a

79 self._y, self._x = _yx2(yx)

80 if name:

81 self.name = name

82

83 def __abs__(self):

84 '''Return this angle's absolute value (L{AuxAngle}).

85 '''

86 a = self._copy_2(self.__abs__)

87 a._yx = map(fabs, self._yx)

88 return a

89

90 def __add__(self, other):

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

92

93 @arg other: An L{AuxAngle}.

94

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

96

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

98 '''

99 a = self._copy_2(self.__add__)

100 return a._iadd(other, _add_op_)

101

102 def __bool__(self): # PYCHOK not special in Python 2-

103 '''Return C{True} if this angle is non-zero.

104 '''

105 return bool(self.tan)

106

107 def __eq__(self, other):

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

109 '''

110 return not self.__ne__(other)

111

112 def __float__(self):

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

114 '''

115 return self.tan

116

117 def __iadd__(self, other):

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

119

120 @arg other: An L{AuxAngle}.

121

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

123

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

125 '''

126 return self._iadd(other, _iadd_op_)

127

128 def __isub__(self, other):

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

130

131 @arg other: An L{AuxAngle}.

132

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

134

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

136 '''

137 return self._iadd(-other, _isub_op_)

138

139 def __ne__(self, other):

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

141 '''

142 _xinstanceof(AuxAngle, other=other)

143 y, x, r = self._yxr_normalized()

144 s, c, t = other._yxr_normalized()

145 return fabs(y - s) > EPS or fabs(x - c) > EPS \

146 or fabs(r - t) > EPS

147

148 def __neg__(self):

149 '''Return I{a copy of} this angle, negated.

150 '''

151 a = self._copy_2(self.__neg__)

152 if a.y or not a.x:

153 a.y = -a.y

154 else:

155 a.x = -a.x

156 return a

157

158 def __pos__(self):

159 '''Return this angle I{as-is}, like C{float.__pos__()}.

160 '''

161 return self if _pos_self else self._copy_2(self.__pos__)

162

163 def __radd__(self, other):

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

165

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

167 '''

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

169 return a._iadd(self, _add_op_)

170

171 def __rsub__(self, other):

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

173

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

175 '''

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

177 return a._iadd(-self, _sub_op_)

178

179 def __str__(self):

180 n = _Aux2Greek.get(self._AUX, self.classname)

181 return unstr(n, y=self.y, x=self.x, tan=self.tan)

182

183 def __sub__(self, other):

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

185

186 @arg other: An L{AuxAngle}.

187

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

189

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

191 '''

192 a = self._copy_2(self.__sub__)

193 return a._iadd(-other, _sub_op_)

194

195 def _iadd(self, other, *unused): # op

196 '''(INTERNAL) Apply C{B{self} += B{other}}.

197 '''

198 _xinstanceof(AuxAngle, other=other)

199 # ignore zero other to preserve signs of _y and _x

200 if other.tan:

201 s, c = other._yx

202 y, x = self._yx

203 self._yx = (y * c + x * s), \

204 (x * c - y * s)

205 return self

206

207 def _copy_2(self, which):

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

209 '''

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

211

212 def _copy_r2(self, other, which):

213 '''(INTERNAL) Copy for I{reverse-dyadic} operators.

214 '''

215 _xinstanceof(AuxAngle, other=other)

216 return other._copy_2(which)

217

218 def copyquadrant(self, other):

219 '''Copy an B{C{other}} angle's quadrant into this angle (L{auxAngle}).

220 '''

221 _xinstanceof(AuxAngle, other=other)

222 self._yx = copysign(self.y, other.y), \

223 copysign(self.x, other.x)

224 return self

225

226 @Property_RO

227 def diff(self):

228 '''Get derivative C{dtan(Eta) / dtan(Phi)} (C{float} or C{NAN}).

229 '''

230 return self._diff

231

232 @staticmethod

233 def fromDegrees(deg, **name_aux):

234 '''Get an L{AuxAngle} from degrees.

235 '''

236 return _AuxClass(**name_aux)(*sincos2d(deg), **name_aux)

237

238 @staticmethod

239 def fromLambertianDegrees(psi, **name_aux):

240 '''Get an L{AuxAngle} from I{Lambertian} degrees.

241 '''

242 return _AuxClass(**name_aux)(sinh(radians(psi)), **name_aux)

243

244 @staticmethod

245 def fromLambertianRadians(psi, **name_aux):

246 '''Get an L{AuxAngle} from I{Lambertian} radians.

247 '''

248 return _AuxClass(**name_aux)(sinh(psi), **name_aux)

249

250 @staticmethod

251 def fromRadians(rad, **name_aux):

252 '''Get an L{AuxAngle} from radians.

253 '''

254 return _AuxClass(**name_aux)(*sincos2(rad), **name_aux)

255

256 @Property_RO

257 def iteration(self):

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

259 '''

260 return self._iter

261

262 def normal(self):

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

264

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

266 '''

267 self._yx = self._yx_normalized

268 return self

269

270 @Property_RO

271 def normalized(self):

272 '''Get a normalized copy of this angle (L{AuxAngle}).

273 '''

274 y, x = self._yx_normalized

275 return self.classof(y, x, name=self.name, aux=self._AUX)

276

277 @property_RO

278 def _RhumbAux(self):

279 '''(INTERNAL) Import the L{RhumbAux} class, I{once}.

280 '''

281 AuxAngle._RhumbAux = R = _MODS.rhumb.aux_.RhumbAux # overwrite property_RO

282 return R

283

284 @Property_RO

285 def tan(self):

286 '''Get this angle's C{tan} (C{float}).

287 '''

288 y, x = self._yx

289 return _over(y, x) if isfinite(y) and y else y

290

291 def toBeta(self, rhumb):

292 '''Short for C{rhumb.auxDLat.convert(Aux.BETA, self, exact=rhumb.exact)}

293 '''

294 return self._toRhumbAux(rhumb, Aux.BETA)

295

296 def toChi(self, rhumb):

297 '''Short for C{rhumb.auxDLat.convert(Aux.CHI, self, exact=rhumb.exact)}

298 '''

299 return self._toRhumbAux(rhumb, Aux.CHI)

300

301 @Property_RO

302 def toDegrees(self):

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

304 '''

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

306

307 @Property_RO

308 def toLambertianDegrees(self): # PYCHOK no cover

309 '''Get this angle's I{Lambertian} in L{Degrees}.

310 '''

311 r = self.toLambertianRadians

312 return Degrees(degrees(r), name=r.name)

313

314 @Property_RO

315 def toLambertianRadians(self):

316 '''Get this angle's I{Lambertian} in L{Radians}.

317 '''

318 return Radians(asinh(self.tan), name=self.name)

319

320 def toMu(self, rhumb):

321 '''Short for C{rhumb.auxDLat.convert(Aux.MU, self, exact=rhumb.exact)}

322 '''

323 return self._toRhumbAux(rhumb, Aux.MU)

324

325 def toPhi(self, rhumb):

326 '''Short for C{rhumb.auxDLat.convert(Aux.PHI, self, exact=rhumb.exact)}

327 '''

328 return self._toRhumbAux(rhumb, Aux.PHI)

329

330 @Property_RO

331 def toRadians(self):

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

333 '''

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

335

336 def _toRhumbAux(self, rhumb, aux):

337 '''(INTERNAL) Create an C{aux}-KIND angle from this angle.

338 '''

339 _xinstanceof(self._RhumbAux, rhumb=rhumb)

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

341

342 @Property

343 def x(self):

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

345 '''

346 return self._x

347

348 @x.setter # PYCHOK setter!

349 def x(self, x): # PYCHOK no cover

350 '''Set this angle's C{x} (C{float}).

351 '''

352 x = float(x)

353 if self._x != x:

354 _update_all(self)

355 self._x = x

356

357 @property_RO

358 def _x_normalized(self):

359 '''(INTERNAL) Get the I{normalized} C{x}.

360 '''

361 _, x = self._yx_normalized

362 return x

363

364 @Property

365 def y(self):

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

367 '''

368 return self._y

369

370 @y.setter # PYCHOK setter!

371 def y(self, y): # PYCHOK no cover

372 '''Set this angle's C{y} (C{float}).

373 '''

374 y = float(y)

375 if self.y != y:

376 _update_all(self)

377 self._y = y

378

379 @Property

380 def _yx(self):

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

382 '''

383 return self._y, self._x

384

385 @_yx.setter # PYCHOK setter!

386 def _yx(self, yx):

387 '''(INTERNAL) Set this angle's C{y} and C{x}.

388 '''

389 yx = _yx2(yx)

390 if self._yx != yx:

391 _update_all(self)

392 self._y, self._x = yx

393

394 @Property_RO

395 def _yx_normalized(self):

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

397 '''

398 y, x = self._yx

399 if isfinite(y) and fabs(y) < _MAX_2 \

400 and fabs(x) < _MAX_2 \

401 and isfinite(self.tan):

402 h = hypot(y, x)

403 if h > 0 and y:

404 y = y / h # /= chokes PyChecker

405 x = x / h

406 if isnan(y): # PYCHOK no cover

407 y = _copysign_1_0(self.y)

408 if isnan(x): # PYCHOK no cover

409 x = _copysign_1_0(self.x)

410 else: # scalar 0

411 y, x = _0_0, _copysign_1_0(y * x)

412 else: # scalar NAN

413 y, x = NAN, _copysign_1_0(y * x)

414 return y, x

415

416 def _yxr_normalized(self, abs_y=False):

417 '''(INTERNAL) Get 3-tuple C{(y, x, r)}, I{normalized}

418 with C{y} or C{abs(y)} and C{r} as C{.toRadians}.

419 '''

420 y, x = self._yx_normalized

421 if abs_y:

422 y = fabs(y) # only y, not x

423 return y, x, atan2(y, x) # .toRadians

424

425

426 class AuxBeta(AuxAngle):

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

428 '''

429 _AUX = Aux.BETA

430

431 @staticmethod

432 def fromDegrees(deg, **name):

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

434 '''

435 return AuxBeta(*sincos2d(deg), **name)

436

437 @staticmethod

438 def fromRadians(rad, **name):

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

440 '''

441 return AuxBeta(*sincos2(rad), **name)

442

443

444 class AuxChi(AuxAngle):

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

446 '''

447 _AUX = Aux.CHI

448

449 @staticmethod

450 def fromDegrees(deg, **name):

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

452 '''

453 return AuxChi(*sincos2d(deg), **name)

454

455

456 class AuxMu(AuxAngle):

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

458 '''

459 _AUX = Aux.MU

460

461 @staticmethod

462 def fromDegrees(deg, **name):

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

464 '''

465 return AuxMu(*sincos2d(deg), **name)

466

467

468 class AuxPhi(AuxAngle):

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

470 '''

471 _AUX = Aux.PHI

472 _diff = _1_0 # see .auxLat._Newton

473

474 @staticmethod

475 def fromDegrees(deg, **name):

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

477 '''

478 return AuxPhi(*sincos2d(deg), **name)

479

480

481 class AuxTheta(AuxAngle):

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

483 '''

484 _AUX = Aux.THETA

485

486 @staticmethod

487 def fromDegrees(deg, **name):

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

489 '''

490 return AuxTheta(*sincos2d(deg), **name)

491

492

493 class AuxXi(AuxAngle):

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

495 '''

496 _AUX = Aux.XI

497

498 @staticmethod

499 def fromDegrees(deg, **name):

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

501 '''

502 return AuxXi(*sincos2d(deg), **name)

503

504

505_AUXClass = {Aux.BETA: AuxBeta,

506 Aux.CHI: AuxChi,

507 Aux.MU: AuxMu,

508 Aux.PHI: AuxPhi,

509 Aux.THETA: AuxTheta,

510 Aux.XI: AuxXi}

511

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

513 return _AUXClass.get(aux, AuxAngle)

514

515

516 def _yx2(yx):

517 try:

518 y, x = map(float, yx)

519 if y in _0_INF_NAN_NINF:

520 x = _copysign_1_0(x)

521 except (TypeError, ValueError) as e:

522 y, x = yx

523 raise AuxError(y=y, x=x, cause=e)

524 return y, x

525

526

527__all__ += _ALL_DOCS(AuxAngle, AuxBeta, AuxChi, AuxMu, AuxPhi, AuxTheta, AuxXi)

528

529# **) MIT License

530#

531# Copyright (C) 2023-2024 -- mrJean1 at Gmail -- All Rights Reserved.

532#

533# Permission is hereby granted, free of charge, to any person obtaining a

534# copy of this software and associated documentation files (the "Software"),

535# to deal in the Software without restriction, including without limitation

536# the rights to use, copy, modify, merge, publish, distribute, sublicense,

537# and/or sell copies of the Software, and to permit persons to whom the

538# Software is furnished to do so, subject to the following conditions:

539#

540# The above copyright notice and this permission notice shall be included

541# in all copies or substantial portions of the Software.

542#

543# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS

544# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,

545# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL

546# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR

547# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,

548# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR

549# OTHER DEALINGS IN THE SOFTWARE.