Coverage for pygeodesy/geodesicx/gxline.py: 97%

236 statements  

« prev     ^ index     » next       coverage.py v7.2.2, created at 2023-10-04 12:08 -0400

1 

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

3 

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

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

6 

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

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

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

10 

11Copyright (C) U{Charles Karney<mailto:Karney@Alum.MIT.edu>} (2008-2023) 

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

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

14''' 

15# make sure int/int division yields float quotient 

16from __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 

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

41 _2__PI, _copysign_1_0 

42from pygeodesy.fsums import _COMMASPACE_, fsumf_, fsum1f_ 

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

44 _sincos12, _sin1cos2 

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

46from pygeodesy.lazily import _ALL_DOCS, _ALL_MODS as _MODS 

47from pygeodesy.karney import _around, _atan2d, Caps, GDict, _fix90, \ 

48 _K_2_0, _norm2, _norm180, _sincos2, \ 

49 _sincos2d, _xinstanceof 

50from pygeodesy.props import Property_RO, _update_all 

51# from pygeodesy.streprs import pairs # _MODS 

52from pygeodesy.utily import atan2d as _atan2d_reverse, sincos2 

53 

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

55 

56__all__ = () 

57__version__ = '23.09.29' 

58 

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

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

61_TINY = _EPSmin 

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

63 

64 

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

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

67 any L{GeodesicLineExact} instances tied to the given 

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

69 ''' 

70 _xinstanceof(_MODS.geodesicx.GeodesicExact, gX=gX) 

71 for glX in _glXs: # PYCHOK use weakref? 

72 if glX._gX is gX: 

73 _update_all(glX) 

74 

75 

76class _GeodesicLineExact(_GeodesicBase): 

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

78 ''' 

79 _a13 = _s13 = NAN 

80 _azi1 = _0_0 

81 _cchi1 = NAN 

82 _dn1 = NAN 

83 _gX = None # Exact only 

84 _k2 = NAN 

85 _lat1 = _lon1 = _0_0 

86 _salp0 = _calp0 = NAN 

87 _salp1 = _calp1 = NAN 

88 _somg1 = _comg1 = NAN 

89 _ssig1 = _csig1 = NAN 

90 

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

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

93 ''' 

94 _xinstanceof(_MODS.geodesicx.GeodesicExact, gX=gX) 

95 Cs = Caps 

96 if _debug: # PYCHOK no cover 

97 self._debug |= _debug & Cs._DEBUG_ALL 

98 # _CapsBase.debug._update(self) 

99 if salp1_calp1: 

100 salp1, calp1 = salp1_calp1 

101 else: 

102 azi1 = _norm180(azi1) 

103 # guard against salp0 underflow, 

104 # also -0 is converted to +0 

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

106 if name: 

107 self.name = name 

108 

109 self._gX = gX # GeodesicExact only 

110 self._lat1 = lat1 = _fix90(lat1) 

111 self._lon1 = lon1 

112 self._azi1 = azi1 

113 self._salp1 = salp1 

114 self._calp1 = calp1 

115 # allow lat, azimuth and unrolling of lon 

116 self._caps = caps | Cs._LINE 

117 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

134 self._somg1 = sbet1 * self._salp0 

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

136 # Without normalization we have schi1 = somg1. 

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

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

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

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

141 if not (caps & Cs.LINE_OFF): 

142 _glXs.append(self) 

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

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

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

146 

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

148 if _glXs: # may be empty or None 

149 try: # PYCHOK no cover 

150 _glXs.remove(self) 

151 except (TypeError, ValueError): 

152 pass 

153 self._gX = None 

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

155 

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

157 if updated: 

158 _update_all(self, *attrs) 

159 

160 @Property_RO 

161 def a1(self): 

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

163 the northward equatorial crossing and the first point. 

164 ''' 

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

166 

167 equatorarc = a1 

168 

169 @Property_RO 

170 def a13(self): 

171 '''Get the arc length to reference point 3 (C{degrees}). 

172 

173 @see: Methods L{Arc} and L{SetArc}. 

174 ''' 

175 return self._a13 

176 

177 def Arc(self): 

178 '''Return the arc length to reference point 3 (C{degrees} or C{NAN}). 

179 

180 @see: Method L{SetArc} and property L{a13}. 

181 ''' 

182 return self.a13 

183 

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

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

186 

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

188 second point (C{degrees}). 

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

190 the quantities to be returned. 

191 

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

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

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

195 except when C{a12=NAN}. 

196 

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

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

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

200 ''' 

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

202 

203 @Property_RO 

204 def azi0(self): 

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

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

207 ''' 

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

209 

210 equatorazimuth = azi0 

211 

212 @Property_RO 

213 def azi0_sincos2(self): 

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

215 ''' 

216 return self._salp0, self._calp0 

217 

218 @Property_RO 

219 def azi1(self): 

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

221 ''' 

222 return self._azi1 

223 

224 @Property_RO 

225 def azi1_sincos2(self): 

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

227 ''' 

228 return self._salp1, self._calp1 

229 

230 @Property_RO 

231 def _B41(self): 

232 '''(INTERNAL) Cached/memoized. 

233 ''' 

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

235 

236 @Property_RO 

237 def _C4a(self): 

238 '''(INTERNAL) Cached/memoized. 

239 ''' 

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

241 

242 @Property_RO 

243 def _caps_DISTANCE_IN(self): 

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

245 ''' 

246 return self.caps & (Caps.DISTANCE_IN & Caps._OUT_MASK) 

247 

248 @Property_RO 

249 def _D0k2(self): 

250 '''(INTERNAL) Cached/memoized. 

251 ''' 

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

253 

254 @Property_RO 

255 def _D1(self): 

256 '''(INTERNAL) Cached/memoized. 

257 ''' 

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

259 

260 def Distance(self): 

261 '''Return the distance to reference point 3 (C{meter} or C{NAN}). 

262 

263 @see: Method L{SetDistance} and property L{s13}. 

264 ''' 

265 return self.s13 

266 

267 @Property_RO 

268 def _E0b(self): 

269 '''(INTERNAL) Cached/memoized. 

270 ''' 

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

272 

273 @Property_RO 

274 def _E1(self): 

275 '''(INTERNAL) Cached/memoized. 

276 ''' 

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

278 

279 @Property_RO 

280 def _eF(self): 

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

282 ''' 

283 # see .gx.GeodesicExact._ef_reset_k2 

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

285 

286 def _GDictPosition(self, arcmode, s12_a12, outmask=Caps.STANDARD): # MCCABE 17 

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

288 

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

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

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

292 except when C{a12=NAN}. 

293 ''' 

294 

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

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

297 return r # Uninitialized or impossible distance requested 

298 

299 Cs = Caps 

300 if self._debug: # PYCHOK no cover 

301 outmask |= self._debug & Cs._DEBUG_DIRECT_LINE 

302 outmask &= self._caps & Cs._OUT_MASK 

303 

304 eF = self._eF 

305 gX = self.geodesic # ._gX 

306 

307 if arcmode: 

308 # s12_a12 is spherical arc length 

309 E2 = _0_0 

310 sig12 = radians(s12_a12) 

311 if _K_2_0: 

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

313 else: # PYCHOK no cover 

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

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

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

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

318 else: # s12_a12 is distance 

319 t = s12_a12 / self._E0b 

320 s, c = _sincos2(t) # tau12 

321 # tau2 = tau1 + tau12 

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

323 sig12 = fsum1f_(self._E1, -E2, t) # == t - (E2 - E1) 

324 ssig12, csig12 = _sincos2(sig12) 

325 

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

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

328 

329 # sig2 = sig1 + sig12 

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

331 dn2 = eF.fDelta(ssig2, csig2) 

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

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

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

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

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

337 cbet2 = csig2 = _TINY 

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

339 salp2 = salp0 

340 calp2 = calp0 * csig2 # no need to normalize 

341 

342 if (outmask & Cs.DISTANCE): 

343 if arcmode: # or f_0_01 

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

345 # AB1 = _E0 * (E2 - _E1) 

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

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

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

349 s12 = self._E0b * fsum1f_(E2, -self._E1, sig12) 

350 else: 

351 s12 = s12_a12 

352 r.set_(s12=s12) 

353 

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

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

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

357 

358 if (outmask & Cs.LONGITUDE): 

359 schi1 = self._somg1 

360 cchi1 = self._cchi1 

361 schi2 = ssig2 * salp0 

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

363 lam12 = salp0 * self._H0e2_f1 * fsum1f_(eF.deltaH(ssig2, csig2, dn2), 

364 -self._H1, sig12) 

365 if (outmask & Cs.LONG_UNROLL): 

366 _a, t = atan2, _copysign_1_0(salp0) # east-going? 

367 tchi1 = t * schi1 

368 tchi2 = t * schi2 

369 chi12 = t * fsum1f_(_a(ssig1, csig1), -_a(ssig2, csig2), 

370 _a(tchi2, cchi2), -_a(tchi1, cchi1), sig12) 

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

372 else: 

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

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

375 r.set_(lon2=lon2) 

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

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

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

379 

380 if (outmask & Cs.LATITUDE): 

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

382 

383 if (outmask & Cs.AZIMUTH): 

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

385 

386 if (outmask & Cs._REDUCEDLENGTH_GEODESICSCALE): 

387 dn1 = self._dn1 

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

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

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

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

392 if (outmask & Cs.REDUCEDLENGTH): 

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

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

395 r.set_(m12=gX.b * fsum1f_(dn2 * (csig1 * ssig2), 

396 -dn1 * (ssig1 * csig2), 

397 -J12 * (csig1 * csig2))) 

398 if (outmask & Cs.GEODESICSCALE): 

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

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

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

402 

403 if (outmask & Cs.AREA): 

404 A4 = salp0 * calp0 

405 if A4: 

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

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

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

409 # If csig12 > 0, write 

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

411 # else 

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

413 # No need to normalize 

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

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

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

417 A4 *= gX._e2a2 

418 B41 = self._B41 

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

420 S12 = (B42 - B41) * A4 

421 else: 

422 S12 = A4 = B41 = B42 = _0_0 

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

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

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

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

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

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

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

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

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

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

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

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

435 r.set_(S12=S12) 

436 

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

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

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

440 azi1=_norm180(self.azi1)) 

441 return r 

442 

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

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

445 

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

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

448 ''' 

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

450 return r.toDirect9Tuple() 

451 

452 def _GenSet(self, arcmode, s13_a13): 

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

454 ''' 

455 if arcmode: 

456 self.SetArc(s13_a13) 

457 else: 

458 self.SetDistance(s13_a13) 

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

460 

461 @Property_RO 

462 def geodesic(self): 

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

464 ''' 

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

466 return self._gX 

467 

468 @Property_RO 

469 def _H0e2_f1(self): 

470 '''(INTERNAL) Cached/memoized. 

471 ''' 

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

473 

474 @Property_RO 

475 def _H1(self): 

476 '''(INTERNAL) Cached/memoized. 

477 ''' 

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

479 

480 @Property_RO 

481 def lat1(self): 

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

483 ''' 

484 return self._lat1 

485 

486 @Property_RO 

487 def lon1(self): 

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

489 ''' 

490 return self._lon1 

491 

492 @Property_RO 

493 def _lon1_norm180(self): 

494 '''(INTERNAL) Cached/memoized. 

495 ''' 

496 return _norm180(self._lon1) 

497 

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

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

500 

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

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

503 the quantities to be returned. 

504 

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

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

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

508 except when C{a12=NAN}. 

509 

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

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

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

513 

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

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

516 ''' 

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

518 

519 @Property_RO 

520 def s13(self): 

521 '''Get the distance to reference point 3 (C{meter} or C{NAN}). 

522 

523 @see: Methods L{Distance} and L{SetDistance}. 

524 ''' 

525 return self._s13 

526 

527 def SetArc(self, a13): 

528 '''Set reference point 3 in terms relative to the first point. 

529 

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

531 point (C{degrees}). 

532 

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

534 the reference point or C{NAN}. 

535 ''' 

536 self._a13 = a13 

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

538 _update_all(self) 

539 return s13 

540 

541 def SetDistance(self, s13): 

542 '''Set reference point 3 in terms relative to the first point. 

543 

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

545 

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

547 and the reference point or C{NAN}. 

548 ''' 

549 self._s13 = s13 

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

551 _update_all(self) 

552 return a13 # NAN for GeodesicLineExact without Cap.DISTANCE_IN 

553 

554 @Property_RO 

555 def _stau1_ctau1(self): 

556 '''(INTERNAL) Cached/memoized. 

557 ''' 

558 s, c = _sincos2(self._E1) 

559 # tau1 = sig1 + B11 

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

561 # unnecessary because Einv inverts E 

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

563 

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

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

566 

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

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

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

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

571 

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

573 ''' 

574 d = dict(geodesic=self.geodesic, 

575 lat1=self.lat1, lon1=self.lon1, azi1=self.azi1, 

576 a13=self.a13, s13=self.s13) 

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

578 

579 

580__all__ += _ALL_DOCS(_GeodesicLineExact) 

581 

582# **) MIT License 

583# 

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

585# 

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

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

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

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

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

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

592# 

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

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

595# 

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

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

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

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

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

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

602# OTHER DEALINGS IN THE SOFTWARE.