1

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

3

4 u'''A pure Python version of I{Karney}'s C++ class U{GeodesicLineExact

5<https://GeographicLib.SourceForge.io/C++/doc/classGeographicLib_1_1GeodesicLineExact.html>}.

6

7 Class L{GeodesicLineExact} follows the naming, methods and return

8 values from class C{GeodesicLine} from I{Karney}'s Python U{geographiclib

9<https://GeographicLib.SourceForge.io/1.52/python/index.html>}.

10

11 Copyright (C) U{Charles Karney<mailto:Charles@Karney.com>} (2008-2022)

12 and licensed under the MIT/X11 License. For more information, see the

13 U{GeographicLib<https://GeographicLib.SourceForge.io>} documentation.

14'''

15# make sure int/int division yields float quotient

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

17

18# A copy of comments from Karney's C{GeodesicLineExact.cpp}:

19#

20# This is a reformulation of the geodesic problem. The

21# notation is as follows:

22# - at a general point (no suffix or 1 or 2 as suffix)

23# - phi = latitude

24# - beta = latitude on auxiliary sphere

25# - omega = longitude on auxiliary sphere

26# - lambda = longitude

27# - alpha = azimuth of great circle

28# - sigma = arc length along great circle

29# - s = distance

30# - tau = scaled distance (= sigma at multiples of PI/2)

31# - at northwards equator crossing

32# - beta = phi = 0

33# - omega = lambda = 0

34# - alpha = alpha0

35# - sigma = s = 0

36# - a 12 suffix means a difference, e.g., s12 = s2 - s1.

37# - s and c prefixes mean sin and cos

38

39# from pygeodesy.basics import _xinstanceof # from .karney

40 from pygeodesy.constants import NAN, _EPSmin, _0_0, _1_0, _180_0, _2__PI

41 from pygeodesy.fsums import _COMMASPACE_, fsum_, fsum1_

42 from pygeodesy.geodesicx.gxbases import _cosSeries, _GeodesicBase, \

43 _sincos12, _sin1cos2

44# from pygeodesy.interns import _COMMASPACE_ # from .fsums

45 from pygeodesy.lazily import _ALL_DOCS, _ALL_MODS as _MODS

46 from pygeodesy.karney import _around, _atan2d, Caps, _copysign, GDict, \

47 _fix90, _K_2_0, _norm2, _norm180, \

48 _sincos2, _sincos2d, _xinstanceof

49 from pygeodesy.props import Property_RO, _update_all

50# from pygeodesy.streprs import pairs # from _MODS

51 from pygeodesy.utily import atan2d as _atan2d_reverse, sincos2

52

53 from math import atan2, cos, degrees, fabs, floor, radians, sin

54

55__all__ = ()

56__version__ = '23.04.04'

57

58_glXs = [] # instances of C{[_]GeodesicLineExact} to be updated

59# underflow guard, we require _TINY * EPS > 0, _TINY + EPS == EPS

60_TINY = _EPSmin

61# assert (_TINY * EPS) > 0 and (_TINY + EPS) == EPS

62

63

64 def _update_glXs(gX): # see GeodesicExact.C4order and -._ef_reset_k2

65 '''(INTERNAL) Zap cached/memoized C{Property[_RO]}s of

66 any L{GeodesicLineExact} instances tied to the given

67 L{GeodesicExact} instance B{C{gX}}.

68 '''

69 _xinstanceof(gX, _MODS.geodesicx.GeodesicExact)

70 for glX in _glXs: # PYCHOK use weakref?

71 if glX._gX is gX:

72 _update_all(glX)

73

74

75 class _GeodesicLineExact(_GeodesicBase):

76 '''(INTERNAL) Base class for L{GeodesicLineExact}.

77 '''

78 _a13 = _s13 = NAN

79 _azi1 = _0_0

80 _cchi1 = NAN

81 _dn1 = NAN

82 _gX = None # Exact only

83 _k2 = NAN

84 _lat1 = _lon1 = _0_0

85 _salp0 = _calp0 = NAN

86 _salp1 = _calp1 = NAN

87 _somg1 = _comg1 = NAN

88 _ssig1 = _csig1 = NAN

89

90 def __init__(self, gX, lat1, lon1, azi1, caps, _debug, *salp1_calp1, **name):

91 '''(INTERNAL) New C{[_]GeodesicLineExact} instance.

92 '''

93 _xinstanceof(gX, _MODS.geodesicx.GeodesicExact)

94 Cs = Caps

95 if _debug: # PYCHOK no cover

96 self._debug |= _debug & Cs._DEBUG_ALL

97 # _CapsBase.debug._update(self)

98 if salp1_calp1:

99 salp1, calp1 = salp1_calp1

100 else:

101 azi1 = _norm180(azi1)

102 # guard against salp0 underflow,

103 # also -0 is converted to +0

104 salp1, calp1 = _sincos2d(_around(azi1))

105 if name:

106 self.name = name

107

108 self._gX = gX # GeodesicExact only

109 self._lat1 = lat1 = _fix90(lat1)

110 self._lon1 = lon1

111 self._azi1 = azi1

112 self._salp1 = salp1

113 self._calp1 = calp1

114 # allow lat, azimuth and unrolling of lon

115 self._caps = caps | Cs._LINE

116

117 sbet1, cbet1 = gX._sinf1cos2d(_around(lat1))

118 self._dn1 = gX._dn(sbet1, cbet1)

119 # Evaluate alp0 from sin(alp1) * cos(bet1) = sin(alp0), with alp0

120 # in [0, pi/2 - |bet1|]. Alt: calp0 = hypot(sbet1, calp1 * cbet1),

121 # but the following is slightly better, consider the case salp1 = 0.

122 self._salp0, self._calp0 = _sin1cos2(salp1, calp1, sbet1, cbet1)

123 self._k2 = self._calp0**2 * gX.ep2

124 # Evaluate sig with tan(bet1) = tan(sig1) * cos(alp1).

125 # sig = 0 is nearest northward crossing of equator.

126 # With bet1 = 0, alp1 = pi/2, we have sig1 = 0 (equatorial line).

127 # With bet1 = pi/2, alp1 = -pi, sig1 = pi/2

128 # With bet1 = -pi/2, alp1 = 0 , sig1 = -pi/2

129 # Evaluate omg1 with tan(omg1) = sin(alp0) * tan(sig1).

130 # With alp0 in (0, pi/2], quadrants for sig and omg coincide.

131 # No atan2(0,0) ambiguity at poles since cbet1 = +epsilon.

132 # With alp0 = 0, omg1 = 0 for alp1 = 0, omg1 = pi for alp1 = pi.

133 self._somg1 = sbet1 * self._salp0

134 self._comg1 = c = (cbet1 * calp1) if (sbet1 or calp1) else _1_0

135 # Without normalization we have schi1 = somg1.

136 self._cchi1 = gX.f1 * self._dn1 * c

137 self._ssig1, self._csig1 = _norm2(sbet1, c) # sig1 in (-pi, pi]

138 # _norm2(somg1, comg1) # no need to normalize!

139 # _norm2(schi1?, cchi1) # no need to normalize!

140 if not (caps & Cs.LINE_OFF):

141 _glXs.append(self)

142 # no need to pre-compute other attrs based on _Caps.X. All are

143 # Property_RO's, computed once and cached/memoized until reset

144 # when C4order is changed or Elliptic function reset is invoked.

145

146 def __del__(self): # XXX use weakref?

147 if _glXs: # may be empty or None

148 try: # PYCHOK no cover

149 _glXs.remove(self)

150 except (TypeError, ValueError):

151 pass

152 self._gX = None

153 # _update_all(self) # throws TypeError during Python 2 cleanup

154

155 def _update(self, updated, *attrs, **unused):

156 if updated:

157 _update_all(self, *attrs)

158

159 @Property_RO

160 def a1(self):

161 '''Get the I{equatorial arc} (C{degrees}), the arc length between

162 the northward equatorial crossing and the first point.

163 '''

164 return _atan2d(self._ssig1, self._csig1) # or NAN

165

166 equatorarc = a1

167

168 @Property_RO

169 def a13(self):

170 '''Get (spherical) arc length from the first to the reference point (C{degrees}).

171

172 @see: Method L{SetArc}.

173 '''

174 return self._a13

175

176 def ArcPosition(self, a12, outmask=Caps.STANDARD):

177 '''Find the position on the line given B{C{a12}}.

178

179 @arg a12: Spherical arc length from the first point to the

180 second point (C{degrees}).

181 @kwarg outmask: Bit-or'ed combination of L{Caps} values specifying

182 the quantities to be returned.

183

184 @return: A L{GDict} with up to 12 items C{lat1, lon1, azi1, lat2,

185 lon2, azi2, m12, a12, s12, M12, M21, S12} with C{lat1},

186 C{lon1}, C{azi1} and arc length C{a12} always included,

187 except when C{a12=NAN}.

188

189 @note: By default, C{B{outmask}=STANDARD}, meaning thc C{lat1},

190 C{lon1}, C{azi1}, C{lat2}, C{lon2}, C{azi2}, C{s12} and

191 C{a12} entries are returned, except when C{a12=NAN}.

192 '''

193 return self._GDictPosition(True, a12, outmask)

194

195 @Property_RO

196 def azi0(self):

197 '''Get the I{equatorial azimuth}, the azimuth of this geodesic line

198 as it crosses the equator in a northward direction (C{degrees90}).

199 '''

200 return _atan2d(*self.azi0_sincos2) # or NAN

201

202 equatorazimuth = azi0

203

204 @Property_RO

205 def azi0_sincos2(self):

206 '''Get the sine and cosine of the I{equatorial azimuth} (2-tuple C{(sin, cos)}).

207 '''

208 return self._salp0, self._calp0

209

210 @Property_RO

211 def azi1(self):

212 '''Get the azimuth at the first point (compass C{degrees}).

213 '''

214 return self._azi1

215

216 @Property_RO

217 def azi1_sincos2(self):

218 '''Get the sine and cosine of the first point's azimuth (2-tuple C{(sin, cos)}).

219 '''

220 return self._salp1, self._calp1

221

222 @Property_RO

223 def _B41(self):

224 '''(INTERNAL) Cached/memoized.

225 '''

226 return _cosSeries(self._C4a, self._ssig1, self._csig1)

227

228 @Property_RO

229 def _C4a(self):

230 '''(INTERNAL) Cached/memoized.

231 '''

232 return self.geodesic._C4f_k2(self._k2)

233

234 @Property_RO

235 def _caps_DISTANCE_IN(self):

236 '''(INTERNAL) Get C{Caps.DISTANCE_IN} and C{_OUT}.

237 '''

238 return self.caps & Caps._DISTANCE_IN_OUT

239

240 @Property_RO

241 def _D0k2(self):

242 '''(INTERNAL) Cached/memoized.

243 '''

244 return self._eF.cD * _2__PI * self._k2

245

246 @Property_RO

247 def _D1(self):

248 '''(INTERNAL) Cached/memoized.

249 '''

250 return self._eF.deltaD(self._ssig1, self._csig1, self._dn1)

251

252 @Property_RO

253 def _E0b(self):

254 '''(INTERNAL) Cached/memoized.

255 '''

256 return self._eF.cE * _2__PI * self.geodesic.b

257

258 @Property_RO

259 def _E1(self):

260 '''(INTERNAL) Cached/memoized.

261 '''

262 return self._eF.deltaE(self._ssig1, self._csig1, self._dn1)

263

264 @Property_RO

265 def _eF(self):

266 '''(INTERNAL) Cached/memoized C{Elliptic} function.

267 '''

268 # see .gx.GeodesicExact._ef_reset_k2

269 return _MODS.elliptic.Elliptic(k2=-self._k2, alpha2=-self.geodesic.ep2)

270

271 def _GDictPosition(self, arcmode, s12_a12, outmask): # MCCABE 17

272 '''(INTERNAL) Generate a new position along the geodesic.

273

274 @return: A L{GDict} with up to 12 items C{lat1, lon1, azi1, lat2,

275 lon2, azi2, m12, a12, s12, M12, M21, S12} with C{lat1},

276 C{lon1}, C{azi1} and arc length C{a12} always included,

277 except when C{a12=NAN}.

278 '''

279

280 r = GDict(a12=NAN, s12=NAN) # note both a12 and s12, always

281 if not (arcmode or self._caps_DISTANCE_IN): # PYCHOK no cover

282 return r # Uninitialized or impossible distance requested

283

284 Cs = Caps

285 if self._debug: # PYCHOK no cover

286 outmask |= self._debug & Cs._DEBUG_DIRECT_LINE

287 outmask &= self._caps & Cs._OUT_MASK

288

289 eF = self._eF

290 gX = self.geodesic # ._gX

291

292 if arcmode:

293 # s12_a12 is spherical arc length

294 E2 = _0_0

295 sig12 = radians(s12_a12)

296 if _K_2_0:

297 ssig12, csig12 = sincos2(sig12) # utily, no NEG0

298 else: # PYCHOK no cover

299 a = fabs(s12_a12) # 0 <= fabs(_remainder(s12_a12, _180_0)) <= 90

300 a -= floor(a / _180_0) * _180_0 # 0 <= 0 < 180

301 ssig12 = _0_0 if a == 0 else sin(sig12)

302 csig12 = _0_0 if a == 90 else cos(sig12)

303 else: # s12_a12 is distance

304 t = s12_a12 / self._E0b

305 s, c = _sincos2(t) # tau12

306 # tau2 = tau1 + tau12

307 E2 = -eF.deltaEinv(*_sincos12(-s, c, *self._stau1_ctau1))

308 sig12 = fsum1_(self._E1, -E2, t) # == t - (E2 - E1)

309 ssig12, csig12 = _sincos2(sig12)

310

311 salp0, calp0 = self._salp0, self._calp0

312 ssig1, csig1 = self._ssig1, self._csig1

313

314 # sig2 = sig1 + sig12

315 ssig2, csig2 = _sincos12(-ssig12, csig12, ssig1, csig1)

316 dn2 = eF.fDelta(ssig2, csig2)

317 # sin(bet2) = cos(alp0) * sin(sig2) and

318 # cbet2 = hypot(salp0, calp0 * csig2). Alt:

319 # cbet2 = hypot(csig2, salp0 * ssig2)

320 sbet2, cbet2 = _sin1cos2(calp0, salp0, csig2, ssig2)

321 if cbet2 == 0: # salp0 = 0, csig2 = 0, break degeneracy

322 cbet2 = csig2 = _TINY

323 # tan(alp0) = cos(sig2) * tan(alp2)

324 salp2 = salp0

325 calp2 = calp0 * csig2 # no need to normalize

326

327 if (outmask & Cs.DISTANCE):

328 if arcmode: # or f_0_01

329 E2 = eF.deltaE(ssig2, csig2, dn2)

330 # AB1 = _E0 * (E2 - _E1)

331 # s12 = _b * (_E0 * sig12 + AB1)

332 # = _b * _E0 * (sig12 + (E2 - _E1))

333 # = _b * _E0 * (E2 - _E1 + sig12)

334 s12 = self._E0b * fsum1_(E2, -self._E1, sig12)

335 else:

336 s12 = s12_a12

337 r.set_(s12=s12)

338

339 if (outmask & Cs._DEBUG_DIRECT_LINE): # PYCHOK no cover

340 r.set_(sig12=sig12, dn2=dn2, b=gX.b, e2=gX.e2, f1=gX.f1,

341 E0b=self._E0b, E1=self._E1, E2=E2, eFk2=eF.k2, eFa2=eF.alpha2)

342

343 if (outmask & Cs.LONGITUDE):

344 schi1 = self._somg1

345 cchi1 = self._cchi1

346 schi2 = ssig2 * salp0

347 cchi2 = gX.f1 * dn2 * csig2 # schi2 = somg2 without normalization

348 lam12 = salp0 * self._H0e2_f1 * fsum1_(eF.deltaH(ssig2, csig2, dn2),

349 -self._H1, sig12)

350 if (outmask & Cs.LONG_UNROLL):

351 t = _copysign(_1_0, salp0) # east-going?

352 tchi1 = t * schi1

353 tchi2 = t * schi2

354 chi12 = t * fsum1_(atan2(ssig1, csig1), -atan2(ssig2, csig2),

355 atan2(tchi2, cchi2), -atan2(tchi1, cchi1), sig12)

356 lon2 = self.lon1 + degrees(chi12 - lam12)

357 else:

358 chi12 = atan2(*_sincos12(schi1, cchi1, schi2, cchi2))

359 lon2 = _norm180(self._lon1_norm180 + _norm180(degrees(chi12 - lam12)))

360 r.set_(lon2=lon2)

361 if (outmask & Cs._DEBUG_DIRECT_LINE): # PYCHOK no cover

362 r.set_(ssig2=ssig2, chi12=chi12, H0e2_f1=self._H0e2_f1,

363 csig2=csig2, lam12=lam12, H1=self._H1)

364

365 if (outmask & Cs.LATITUDE):

366 r.set_(lat2=_atan2d(sbet2, gX.f1 * cbet2))

367

368 if (outmask & Cs.AZIMUTH):

369 r.set_(azi2=_atan2d_reverse(salp2, calp2, reverse=outmask & Cs.REVERSE2))

370

371 if (outmask & Cs._REDUCEDLENGTH_GEODESICSCALE):

372 dn1 = self._dn1

373 J12 = self._D0k2 * fsum_(eF.deltaD(ssig2, csig2, dn2), -self._D1, sig12)

374 if (outmask & Cs._DEBUG_DIRECT_LINE): # PYCHOK no cover

375 r.set_(ssig1=ssig1, dn1=dn1, D0k2=self._D0k2,

376 csig1=csig1, J12=J12, D1=self._D1)

377 if (outmask & Cs.REDUCEDLENGTH):

378 # Add parens around (csig1 * ssig2) and (ssig1 * csig2) to

379 # ensure accurate cancellation in the case of coincident points.

380 r.set_(m12=gX.b * fsum1_(dn2 * (csig1 * ssig2),

381 -dn1 * (ssig1 * csig2),

382 -J12 * (csig1 * csig2)))

383 if (outmask & Cs.GEODESICSCALE):

384 t = self._k2 * (ssig2 - ssig1) * (ssig2 + ssig1) / (dn2 + dn1)

385 r.set_(M12=csig12 + ssig1 * (t * ssig2 - csig2 * J12) / dn1,

386 M21=csig12 - ssig2 * (t * ssig1 - csig1 * J12) / dn2)

387

388 if (outmask & Cs.AREA):

389 A4 = salp0 * calp0

390 if A4:

391 # tan(alp) = tan(alp0) * sec(sig)

392 # tan(alp2-alp1) = (tan(alp2) - tan(alp1)) / (tan(alp2) * tan(alp1) + 1)

393 # = calp0 * salp0 * (csig1 - csig2) / (salp0^2 + calp0^2 * csig1 * csig2)

394 # If csig12 > 0, write

395 # csig1 - csig2 = ssig12 * (csig1 * ssig12 / (1 + csig12) + ssig1)

396 # else

397 # csig1 - csig2 = csig1 * (1 - csig12) + ssig12 * ssig1

398 # No need to normalize

399 salp12 = (((ssig12 * csig1 / (_1_0 + csig12) + ssig1) * ssig12) if csig12 > 0 else

400 (csig1 * (_1_0 - csig12) + ssig1 * ssig12)) * A4

401 calp12 = salp0**2 + calp0**2 * csig1 * csig2

402 A4 *= gX._e2a2

403 B41 = self._B41

404 B42 = _cosSeries(self._C4a, ssig2, csig2)

405 S12 = (B42 - B41) * A4

406 else:

407 S12 = A4 = B41 = B42 = _0_0

408 # alp12 = alp2 - alp1, used in atan2 so no need to normalize

409 salp12, calp12 = _sincos12(self._salp1, self._calp1, salp2, calp2)

410 # We used to include some patch up code that purported to deal

411 # with nearly meridional geodesics properly. However, this turned

412 # out to be wrong once salp1 = -0 was allowed (via InverseLine).

413 # In fact, the calculation of {s,c}alp12 was already correct

414 # (following the IEEE rules for handling signed zeros). So,

415 # the patch up code was unnecessary (as well as dangerous).

416 if (outmask & Cs._DEBUG_DIRECT_LINE): # PYCHOK no cover

417 r.set_(salp12=salp12, salp0=salp0, B41=B41, A4=A4,

418 calp12=calp12, calp0=calp0, B42=B42, c2=gX.c2)

419 S12 += gX.c2 * atan2(salp12, calp12)

420 r.set_(S12=S12)

421

422 r.set_(a12=s12_a12 if arcmode else degrees(sig12),

423 lat1=self.lat1, # == _fix90(lat1)

424 lon1=self.lon1 if (outmask & Cs.LONG_UNROLL) else self._lon1_norm180,

425 azi1=_norm180(self.azi1))

426 return r

427

428 def _GenPosition(self, arcmode, s12_a12, outmask):

429 '''(INTERNAL) Generate a new position along the geodesic.

430

431 @return: L{Direct9Tuple}C{(a12, lat2, lon2, azi2,

432 s12, m12, M12, M21, S12)}.

433 '''

434 r = self._GDictPosition(arcmode, s12_a12, outmask)

435 return r.toDirect9Tuple()

436

437 def _GenSet(self, arcmode, s13_a13):

438 '''(INTERNAL) Aka C++ C{GenSetDistance}.

439 '''

440 if arcmode:

441 self.SetArc(s13_a13)

442 else:

443 self.SetDistance(s13_a13)

444 return self # for gx.GeodesicExact.InverseLine and -._GenDirectLine

445

446 @Property_RO

447 def geodesic(self):

448 '''Get the I{exact} geodesic (L{GeodesicExact}).

449 '''

450 assert isinstance(self._gX, _MODS.geodesicx.GeodesicExact)

451 return self._gX

452

453 @Property_RO

454 def _H0e2_f1(self):

455 '''(INTERNAL) Cached/memoized.

456 '''

457 return self._eF.cH * _2__PI * self.geodesic._e2_f1

458

459 @Property_RO

460 def _H1(self):

461 '''(INTERNAL) Cached/memoized.

462 '''

463 return self._eF.deltaH(self._ssig1, self._csig1, self._dn1)

464

465 @Property_RO

466 def lat1(self):

467 '''Get the latitude of the first point (C{degrees}).

468 '''

469 return self._lat1

470

471 @Property_RO

472 def lon1(self):

473 '''Get the longitude of the first point (C{degrees}).

474 '''

475 return self._lon1

476

477 @Property_RO

478 def _lon1_norm180(self):

479 '''(INTERNAL) Cached/memoized.

480 '''

481 return _norm180(self._lon1)

482

483 def Position(self, s12, outmask=Caps.STANDARD):

484 '''Find the position on the line given B{C{s12}}.

485

486 @arg s12: Distance from the first point to the second (C{meter}).

487 @kwarg outmask: Bit-or'ed combination of L{Caps} values specifying

488 the quantities to be returned.

489

490 @return: A L{GDict} with up to 12 items C{lat1, lon1, azi1, lat2,

491 lon2, azi2, m12, a12, s12, M12, M21, S12} with C{lat1},

492 C{lon1}, C{azi1} and arc length C{a12} always included,

493 except when C{a12=NAN}.

494

495 @note: By default, C{B{outmask}=STANDARD}, meaning thc C{lat1},

496 C{lon1}, C{azi1}, C{lat2}, C{lon2}, C{azi2}, C{s12} and

497 C{a12} entries are returned, except when C{a12=NAN}.

498

499 @note: This L{GeodesicLineExact} instance must have been

500 constructed with capability C{Caps.DISTANCE_IN} set.

501 '''

502 return self._GDictPosition(False, s12, outmask)

503

504 @Property_RO

505 def s13(self):

506 '''Get the distance from the first to the reference point (C{meter}).

507

508 @see: Method L{SetDistance}.

509 '''

510 return self._s13

511

512 def SetArc(self, a13):

513 '''Set reference point 3 in terms of distance to the first point.

514

515 @arg a13: Spherical arc length from the first to the reference

516 point (C{degrees}).

517

518 @return: The distance C{s13} (C{meter}) between the first and

519 the reference point or C{NAN}.

520 '''

521 self._a13 = a13

522 self._s13 = s13 = self._GDictPosition(True, a13, Caps.DISTANCE).s12

523 return s13

524

525 def SetDistance(self, s13):

526 '''Set reference point 3 in terms of distance to the first point.

527

528 @arg s13: Distance from the first to the reference point (C{meter}).

529

530 @return: The arc length C{a13} (C{degrees}) between the first

531 and the reference point or C{NAN}.

532 '''

533 self._s13 = s13

534 self._a13 = a13 = self._GDictPosition(False, s13, 0).a12

535 return a13 # NAN for GeodesicLineExact without Cap.DISTANCE_IN

536

537 @Property_RO

538 def _stau1_ctau1(self):

539 '''(INTERNAL) Cached/memoized.

540 '''

541 s, c = _sincos2(self._E1)

542 # tau1 = sig1 + B11

543 return _sincos12(-s, c, self._ssig1, self._csig1)

544 # unnecessary because Einv inverts E

545 # return -self._eF.deltaEinv(stau1, ctau1)

546

547 def toStr(self, prec=6, sep=_COMMASPACE_, **unused): # PYCHOK signature

548 '''Return this C{GeodesicLineExact} as string.

549

550 @kwarg prec: The C{float} precision, number of decimal digits (0..9).

551 Trailing zero decimals are stripped for B{C{prec}} values

552 of 1 and above, but kept for negative B{C{prec}} values.

553 @kwarg sep: Separator to join (C{str}).

554

555 @return: C{GeodesicLineExact} (C{str}).

556 '''

557 d = dict(geodesic=self.geodesic,

558 lat1=self.lat1, lon1=self.lon1, azi1=self.azi1,

559 a13=self.a13, s13=self.s13)

560 return sep.join(_MODS.streprs.pairs(d, prec=prec))

561

562

563__all__ += _ALL_DOCS(_GeodesicLineExact)

564

565# **) MIT License

566#

567# Copyright (C) 2016-2023 -- mrJean1 at Gmail -- All Rights Reserved.

568#

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

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

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

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

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

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

575#

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

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

578#

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

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

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

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

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

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

585# OTHER DEALINGS IN THE SOFTWARE.