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

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

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

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

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

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

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

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

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

500 return _AUXClass.get(aux, AuxAngle)

501

502

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