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, AuxError

18 from pygeodesy.basics import _xinstanceof, _xkwds_get

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, _xkwds_get # from .auxily, .basics

22 from pygeodesy.fmath import hypot, unstr

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

24# from pygeodesy.interns import NN # from .fsums

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__ = '24.04.26'

36

37_0_INF_NAN_NINF = (0, _0_0) + _INF_NAN_NINF

38_MAX_2 = MAX * _0_5 # PYCHOK used!

39# del _INF_NAN_NINF, MAX

40

41

42 class AuxAngle(_Named):

43 '''U{An accurate representation of angles

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

45 '''

46 _AUX = None # overloaded/-ridden

47 _diff = NAN # default

48 _iter = None # like .Named._NamedBase

49 _y = _0_0

50 _x = _1_0

51

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

53 '''New L{AuxAngle}.

54

55 @kwarg y_angle: The Y component (C{scalar}, including C{INF}, C{NAN}

56 and C{NINF}) or a previous L{AuxAngle} instance.

57 @kwarg x: The X component, ignored if C{B{y_angle}} is non-C{scalar}.

58 @kwarg name: Optional name (C{str}).

59 @kwarg aux: I{Auxiliary} kind (C{Aux.KIND}), ignored if B{C{y_angle}}

60 is non-C{scalar}.

61

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

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 if aux not in _AUXClass:

76 raise AuxError(aux=aux)

77 self._AUX = aux

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

79 if name:

80 self.name = name

81

82 def __abs__(self):

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

84 '''

85 a = self._copy_2(self.__abs__)

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

87 return a

88

89 def __add__(self, other):

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

91

92 @arg other: An L{AuxAngle}.

93

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

95

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

97 '''

98 a = self._copy_2(self.__add__)

99 return a._iadd(other, _add_op_)

100

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

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

103 '''

104 return bool(self.tan)

105

106 def __eq__(self, other):

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

108 '''

109 return not self.__ne__(other)

110

111 def __float__(self):

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

113 '''

114 return self.tan

115

116 def __iadd__(self, other):

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

118

119 @arg other: An L{AuxAngle}.

120

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

122

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

124 '''

125 return self._iadd(other, _iadd_op_)

126

127 def __isub__(self, other):

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

129

130 @arg other: An L{AuxAngle}.

131

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

133

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

135 '''

136 return self._iadd(-other, _isub_op_)

137

138 def __ne__(self, other):

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

140 '''

141 _xinstanceof(AuxAngle, other=other)

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

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

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

145 or fabs(r - t) > EPS

146

147 def __neg__(self):

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

149 '''

150 a = self._copy_2(self.__neg__)

151 if a.y or not a.x:

152 a.y = -a.y

153 else:

154 a.x = -a.x

155 return a

156

157 def __pos__(self):

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

159 '''

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

161

162 def __radd__(self, other):

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

164

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

166 '''

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

168 return a._iadd(self, _add_op_)

169

170 def __rsub__(self, other):

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

172

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

174 '''

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

176 return a._iadd(-self, _sub_op_)

177

178 def __str__(self):

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

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

181

182 def __sub__(self, other):

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

184

185 @arg other: An L{AuxAngle}.

186

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

188

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

190 '''

191 a = self._copy_2(self.__sub__)

192 return a._iadd(-other, _sub_op_)

193

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

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

196 '''

197 _xinstanceof(AuxAngle, other=other)

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

199 if other.tan:

200 s, c = other._yx

201 y, x = self._yx

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

203 (x * c - y * s)

204 return self

205

206 def _copy_2(self, which):

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

208 '''

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

210

211 def _copy_r2(self, other, which):

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

213 '''

214 _xinstanceof(AuxAngle, other=other)

215 return other._copy_2(which)

216

217 def copyquadrant(self, other):

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

219 '''

220 _xinstanceof(AuxAngle, other=other)

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

222 copysign(self.x, other.x)

223 return self

224

225 @Property_RO

226 def diff(self):

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

228 '''

229 return self._diff

230

231 @staticmethod

232 def fromDegrees(deg, **name_aux):

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

234 '''

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

236

237 @staticmethod

238 def fromLambertianDegrees(psi, **name_aux):

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

240 '''

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

242

243 @staticmethod

244 def fromLambertianRadians(psi, **name_aux):

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

246 '''

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

248

249 @staticmethod

250 def fromRadians(rad, **name_aux):

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

252 '''

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

254

255 @Property_RO

256 def iteration(self):

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

258 '''

259 return self._iter

260

261 def normal(self):

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

263

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

265 '''

266 self._yx = self._yx_normalized

267 return self

268

269 @Property_RO

270 def normalized(self):

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

272 '''

273 y, x = self._yx_normalized

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

275

276 @property_RO

277 def _RhumbAux(self):

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

279 '''

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

281 return R

282

283 @Property_RO

284 def tan(self):

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

286 '''

287 y, x = self._yx

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

289

290 def toBeta(self, rhumb):

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

292 '''

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

294

295 def toChi(self, rhumb):

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

297 '''

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

299

300 @Property_RO

301 def toDegrees(self):

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

303 '''

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

305

306 @Property_RO

307 def toLambertianDegrees(self): # PYCHOK no cover

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

309 '''

310 r = self.toLambertianRadians

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

312

313 @Property_RO

314 def toLambertianRadians(self):

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

316 '''

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

318

319 def toMu(self, rhumb):

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

321 '''

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

323

324 def toPhi(self, rhumb):

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

326 '''

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

328

329 @Property_RO

330 def toRadians(self):

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

332 '''

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

334

335 def _toRhumbAux(self, rhumb, aux):

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

337 '''

338 _xinstanceof(self._RhumbAux, rhumb=rhumb)

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

340

341 @Property

342 def x(self):

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

344 '''

345 return self._x

346

347 @x.setter # PYCHOK setter!

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

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

350 '''

351 x = float(x)

352 if self._x != x:

353 _update_all(self)

354 self._x = x

355

356 @property_RO

357 def _x_normalized(self):

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

359 '''

360 _, x = self._yx_normalized

361 return x

362

363 @Property

364 def y(self):

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

366 '''

367 return self._y

368

369 @y.setter # PYCHOK setter!

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

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

372 '''

373 y = float(y)

374 if self.y != y:

375 _update_all(self)

376 self._y = y

377

378 @Property

379 def _yx(self):

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

381 '''

382 return self._y, self._x

383

384 @_yx.setter # PYCHOK setter!

385 def _yx(self, yx):

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

387 '''

388 yx = _yx2(yx)

389 if self._yx != yx:

390 _update_all(self)

391 self._y, self._x = yx

392

393 @Property_RO

394 def _yx_normalized(self):

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

396 '''

397 y, x = self._yx

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

399 and fabs(x) < _MAX_2 \

400 and isfinite(self.tan):

401 h = hypot(y, x)

402 if h > 0 and y:

403 y = y / h # /= chokes PyChecker

404 x = x / h

405 if isnan(y): # PYCHOK no cover

406 y = _copysign_1_0(self.y)

407 if isnan(x): # PYCHOK no cover

408 x = _copysign_1_0(self.x)

409 else: # scalar 0

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

411 else: # scalar NAN

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

413 return y, x

414

415 def _yxr_normalized(self, abs_y=False):

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

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

418 '''

419 y, x = self._yx_normalized

420 if abs_y:

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

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

423

424

425 class AuxBeta(AuxAngle):

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

427 '''

428 _AUX = Aux.BETA

429

430 @staticmethod

431 def fromDegrees(deg, name=NN):

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

433 '''

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

435

436 @staticmethod

437 def fromRadians(rad, name=NN):

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

439 '''

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

441

442

443 class AuxChi(AuxAngle):

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

445 '''

446 _AUX = Aux.CHI

447

448 @staticmethod

449 def fromDegrees(deg, name=NN):

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

451 '''

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

453

454

455 class AuxMu(AuxAngle):

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

457 '''

458 _AUX = Aux.MU

459

460 @staticmethod

461 def fromDegrees(deg, name=NN):

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

463 '''

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

465

466

467 class AuxPhi(AuxAngle):

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

469 '''

470 _AUX = Aux.PHI

471 _diff = _1_0 # see .auxLat._Newton

472

473 @staticmethod

474 def fromDegrees(deg, name=NN):

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

476 '''

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

478

479

480 class AuxTheta(AuxAngle):

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

482 '''

483 _AUX = Aux.THETA

484

485 @staticmethod

486 def fromDegrees(deg, name=NN):

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

488 '''

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

490

491

492 class AuxXi(AuxAngle):

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

494 '''

495 _AUX = Aux.XI

496

497 @staticmethod

498 def fromDegrees(deg, name=NN):

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

500 '''

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

502

503

504_AUXClass = {Aux.BETA: AuxBeta,

505 Aux.CHI: AuxChi,

506 Aux.MU: AuxMu,

507 Aux.PHI: AuxPhi,

508 Aux.THETA: AuxTheta,

509 Aux.XI: AuxXi}

510

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

512 return _AUXClass.get(aux, AuxAngle)

513

514

515 def _yx2(yx):

516 try:

517 y, x = map(float, yx)

518 if y in _0_INF_NAN_NINF:

519 x = _copysign_1_0(x)

520 except (TypeError, ValueError) as e:

521 y, x = yx

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

523 return y, x

524

525

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

527

528# **) MIT License

529#

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

531#

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

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

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

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

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

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

538#

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

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

541#

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

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

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

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

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

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

548# OTHER DEALINGS IN THE SOFTWARE.