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

253 statements  

« prev     ^ index     » next       coverage.py v7.6.1, created at 2025-05-29 12:40 -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 _ # noqa: E702 ; 

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# - lambda = longitude 

25# - beta = latitude on auxiliary sphere 

26# - omega = longitude on auxiliary sphere 

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 # _MODS 

40from pygeodesy.constants import NAN, _EPSqrt as _TOL, \ 

41 _0_0, _1_0, _180_0, _2__PI, \ 

42 _copysign_1_0, isfinite 

43from pygeodesy.errors import _xError, _xkwds_pop2 

44from pygeodesy.fsums import fsumf_, fsum1f_ 

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

46 _sincos12, _sin1cos2, \ 

47 _sinf1cos2d, _TINY, _toNAN 

48# from pygeodesy.geodesicw import _Intersecant2 # _MODS 

49from pygeodesy.lazily import _ALL_DOCS, _ALL_MODS as _MODS 

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

51 _K_2_0, _llz2gl, _norm2, _norm180, \ 

52 _sincos2, _sincos2d 

53from pygeodesy.props import Property_RO, property_ROver, _update_all 

54from pygeodesy.utily import atan2, atan2d as _atan2d_reverse, sincos2 

55 

56from math import cos, degrees, fabs, floor, radians, sin 

57 

58__all__ = () 

59__version__ = '25.05.28' 

60 

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

62 

63 

64def _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 _xGeodesicExact(gX=gX) 

70 for glX in _glXs: # PYCHOK use weakref? 

71 if glX._gX is gX: 

72 _update_all(glX) 

73 

74 

75def _xGeodesicExact(**gX): 

76 '''(INTERNAL) Check a L{GeodesicExact} instance. 

77 ''' 

78 _MODS.basics._xinstanceof(_MODS.geodesicx.GeodesicExact, **gX) 

79 

80 

81class _GeodesicLineExact(_GeodesicBase): 

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

83 ''' 

84 _a13 = _s13 = NAN 

85# _azi1 = _0_0 

86# _cchi1 = NAN 

87# _dn1 = NAN 

88 _gX = None # Exact only 

89# _k2 = NAN 

90# _lat1 = _lon1 = _0_0 

91# _salp0 = _calp0 = NAN 

92# _salp1 = _calp1 = NAN 

93# _somg1 = _comg1 = NAN 

94# _ssig1 = _csig1 = NAN 

95# _toNAN = False 

96 

97 def __init__(self, gX, lat1, lon1, azi1, caps, **name_): 

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

99 ''' 

100# _xGeodesicExact(gX=gX) 

101 if azi1 is None: # see GeodesicExact.InverseLine 

102 (salp1, calp1), name_ = _xkwds_pop2(name_, _s_calp1=(_0_0, _1_0)) 

103 azi1 = _atan2d(salp1, calp1) 

104 else: # guard against salp0 underflow, convert -0 to +0 

105 azi1 = _norm180(azi1) 

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

107 if name_: 

108 self.name = name_ 

109 self._toNAN = _toNAN(caps, lat1, lon1, azi1, salp1, calp1) 

110 

111 self._gX = gX # GeodesicExact only 

112 self._lat1 = lat1 = _fix90(lat1) 

113 self._lon1 = lon1 

114 self._azi1 = azi1 

115 self._salp1 = salp1 

116 self._calp1 = calp1 

117 # allow lat, azimuth and unrolling of lon 

118 self._caps = caps | Caps._AZIMUTH_LATITUDE_LONG_UNROLL 

119 

120 sbet1, cbet1 = _sinf1cos2d(_around(lat1), gX.f1) 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

136 self._somg1 = sbet1 * self._salp0 

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

138 # Without normalization we have schi1 = somg1. 

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

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

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

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

143 if not (caps & Caps.LINE_OFF): 

144 _glXs.append(self) 

145 # no need to pre-compute other attrs for (caps & Caps.X). All are 

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

147 # arc, distance, C4order is changed or Elliptic function is reset. 

148 

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

150 if _glXs: # may be empty or None 

151 try: # PYCHOK no cover 

152 _glXs.remove(self) 

153 except (TypeError, ValueError): 

154 pass 

155 self._gX = None 

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

157 

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

159 if updated: 

160 _update_all(self, *attrs) 

161 

162 @Property_RO 

163 def a1(self): 

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

165 the northward equatorial crossing and the first point. 

166 ''' 

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

168 

169 equatorarc = a1 

170 

171 @Property_RO 

172 def a13(self): 

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

174 

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

176 ''' 

177 return self._a13 

178 

179 def Arc(self): 

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

181 

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

183 ''' 

184 return self.a13 

185 

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

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

188 

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

190 second point (C{degrees}). 

191 @kwarg outmask: Bit-or'ed combination of L{Caps<pygeodesy.karney.Caps>} 

192 values specifying the quantities to be returned. 

193 

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

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

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

197 except when C{a12=NAN}. 

198 

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

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

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

202 ''' 

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

204 

205 @Property_RO 

206 def azi0(self): 

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

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

209 ''' 

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

211 

212 equatorazimuth = azi0 

213 

214 @Property_RO 

215 def azi0_sincos2(self): 

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

217 ''' 

218 return self._salp0, self._calp0 

219 

220 @Property_RO 

221 def azi1(self): 

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

223 ''' 

224 return self._azi1 

225 

226 @Property_RO 

227 def azi1_sincos2(self): 

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

229 ''' 

230 return self._salp1, self._calp1 

231 

232 @Property_RO 

233 def _B41(self): 

234 '''(INTERNAL) Cached/memoized. 

235 ''' 

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

237 

238 @Property_RO 

239 def _C4a(self): 

240 '''(INTERNAL) Cached/memoized. 

241 ''' 

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

243 

244 @Property_RO 

245 def _caps_DISTANCE_IN(self): 

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

247 ''' 

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

249 

250 @Property_RO 

251 def _D0k2(self): 

252 '''(INTERNAL) Cached/memoized. 

253 ''' 

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

255 

256 @Property_RO 

257 def _D1(self): 

258 '''(INTERNAL) Cached/memoized. 

259 ''' 

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

261 

262 def Distance(self): 

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

264 

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

266 ''' 

267 return self.s13 

268 

269 @Property_RO 

270 def _E0b(self): 

271 '''(INTERNAL) Cached/memoized. 

272 ''' 

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

274 

275 @Property_RO 

276 def _E1(self): 

277 '''(INTERNAL) Cached/memoized. 

278 ''' 

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

280 

281 @Property_RO 

282 def _eF(self): 

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

284 ''' 

285 # see .gx.GeodesicExact._ef_reset_k2 

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

287 

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

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

290 

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

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

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

294 except when C{a12=NAN}. 

295 ''' 

296 Cs = Caps 

297 if outmask: 

298 outmask &= self._caps & Cs._OUT_MASK 

299 eF = self._eF 

300 gX = self.geodesic # ._gX 

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

302 

303 if self._toNAN or not isfinite(s12_a12): # _toNAN(outmask, s12_a12)? 

304 # E2 = sig12 = ssig12 = csig12 = NAN 

305 return r._toNAN(outmask | Cs.NONFINITONAN) # for backward compatibility 

306 elif arcmode: # s12_a12 is (spherical) arc length 

307 r.set_(a12=s12_a12) 

308 sig12 = radians(s12_a12) 

309 if _K_2_0: 

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

311 else: # PYCHOK no cover 

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

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

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

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

316 E2 = _0_0 

317 elif self._caps_DISTANCE_IN: # s12_a12 is distance 

318 t = s12_a12 / self._E0b 

319 s, c = _sincos2(t) # tau12 

320 # tau2 = tau1 + tau12 

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

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

323 ssig12, csig12 = _sincos2(sig12) 

324 r.set_(a12=degrees(sig12)) 

325 else: # uninitialized or impossible distance requested 

326 return r 

327 

328 # sig2 = sig1 + sig12 

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

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

331 dn2 = eF.fDelta(*t) 

332 

333 if (outmask & Cs.DISTANCE): 

334 outmask ^= Cs.DISTANCE 

335 if arcmode: # or f_0_01 

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

337 # AB1 = _E0 * (E2 - _E1) 

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

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

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

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

342 else: 

343 s12 = s12_a12 

344 r.set_(s12=s12) 

345 

346 if not outmask: # all done, see ._GenSet 

347 return r 

348 

349 if self._debug: # PYCHOK no cover 

350 outmask |= self._debug & Cs._DEBUG_DIRECT_LINE 

351 

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

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

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

355 

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

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

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

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

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

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

362 cbet2 = csig2 = _TINY 

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

364 salp2 = salp0 

365 calp2 = calp0 * csig2 # no need to normalize 

366 

367 if (outmask & Cs.AZIMUTH): 

368 r.set_(azi2=_atan2d_reverse(salp2, calp2, 

369 reverse=outmask & Cs.REVERSE2)) 

370 

371 if (outmask & Cs.LATITUDE): 

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

373 

374 if (outmask & Cs.LONGITUDE): 

375 schi1 = self._somg1 

376 cchi1 = self._cchi1 

377 schi2 = ssig2 * salp0 

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

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

380 -self._H1, sig12) 

381 if (outmask & Cs.LONG_UNROLL): 

382 t = _copysign_1_0(salp0) # east-going? 

383 tchi1 = t * schi1 

384 tchi2 = t * schi2 

385 chi12 = t * fsum1f_(atan2(ssig1, csig1), -atan2(ssig2, csig2), 

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

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

388 else: 

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

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

391 r.set_(lon2=lon2) 

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

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

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

395 

396 if (outmask & Cs._REDUCEDLENGTH_GEODESICSCALE): 

397 dn1 = self._dn1 

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

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

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

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

402 if (outmask & Cs.REDUCEDLENGTH): 

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

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

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

406 -dn1 * (ssig1 * csig2), 

407 -J12 * (csig1 * csig2))) 

408 if (outmask & Cs.GEODESICSCALE): 

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

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

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

412 

413 if (outmask & Cs.AREA): 

414 A4 = salp0 * calp0 

415 if A4: 

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

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

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

419 # If csig12 > 0, write 

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

421 # else 

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

423 # No need to normalize 

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

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

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

427 A4 *= gX._e2a2 

428 B41 = self._B41 

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

430 S12 = (B42 - B41) * A4 

431 else: 

432 S12 = A4 = B41 = B42 = _0_0 

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

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

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

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

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

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

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

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

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

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

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

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

445 r.set_(S12=S12) 

446 

447 r.set_(azi1=_norm180(self.azi1), 

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

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

450 return r 

451 

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

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

454 

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

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

457 ''' 

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

459 return r.toDirect9Tuple() 

460 

461 def _GenSet(self, debug, s12=None, a12=None, **llz2): 

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

463 ''' 

464 Cs = Caps 

465 if debug: # PYCHOK no cover 

466 self._debug |= debug & Cs._DEBUG_ALL 

467 # _CapsBase.debug._update(self) 

468 if s12 is None: 

469 if a12 is None: # see GeodesicExact.Line 

470 return self 

471 s12 = self._GDictPosition(True, a12, outmask=Cs.DISTANCE).s12 if a12 else _0_0 

472 elif a12 is None: 

473 a12 = self._GDictPosition(False, s12, 0).a12 if s12 else _0_0 

474 self._s13 = s12 

475 self._a13 = a12 

476 self._caps |= Cs.DISTANCE | Cs.DISTANCE_IN 

477 # _update_all(self) # new, from GeodesicExact.*Line 

478 return _llz2gl(self, **llz2) 

479 

480 @Property_RO 

481 def geodesic(self): 

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

483 ''' 

484 _xGeodesicExact(geodesic=self._gX) 

485 return self._gX 

486 

487 def Intersecant2(self, lat0, lon0, radius, tol=_TOL): 

488 '''Compute the intersection(s) of this geodesic line and a circle. 

489 

490 @arg lat0: Latitude of the circle center (C{degrees}). 

491 @arg lon0: Longitude of the circle center (C{degrees}). 

492 @arg radius: Radius of the circle (C{meter}, conventionally). 

493 @kwarg tol: Convergence tolerance (C{scalar}). 

494 

495 @return: 2-Tuple C{(P, Q)} with both intersections (representing 

496 a geodesic chord), each a L{GDict} from method L{Position} 

497 extended to 14 items by C{lon0, lat0, azi0, a02, s02, at} 

498 with the circle center C{lat0}, C{lon0}, azimuth C{azi0} 

499 at, distance C{a02} in C{degrees} and C{s02} in C{meter} 

500 along the geodesic from the circle center to the intersection 

501 C{lat2}, C{lon2} and the angle C{at} between the geodesic 

502 and this line at the intersection. The geodesic azimuth 

503 at the intersection is C{(at + azi2)}. If this geodesic 

504 line is tangential to the circle, both points are the same 

505 L{GDict} instance. 

506 

507 @raise IntersectionError: The circle and this geodesic line do not 

508 intersect, no I{perpencular} geodetic 

509 intersection or no convergence. 

510 

511 @raise UnitError: Invalid B{C{radius}}. 

512 ''' 

513 try: 

514 return _MODS.geodesicw._Intersecant2(self, lat0, lon0, radius, tol=tol) 

515 except (TypeError, ValueError) as x: 

516 raise _xError(x, lat0, lon0, radius, tol=_TOL) 

517 

518 @Property_RO 

519 def _H0e2_f1(self): 

520 '''(INTERNAL) Cached/memoized. 

521 ''' 

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

523 

524 @Property_RO 

525 def _H1(self): 

526 '''(INTERNAL) Cached/memoized. 

527 ''' 

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

529 

530 @Property_RO 

531 def lat1(self): 

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

533 ''' 

534 return self._lat1 

535 

536 @Property_RO 

537 def lon1(self): 

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

539 ''' 

540 return self._lon1 

541 

542 @Property_RO 

543 def _lon1_norm180(self): 

544 '''(INTERNAL) Cached/memoized. 

545 ''' 

546 return _norm180(self._lon1) 

547 

548 def PlumbTo(self, lat0, lon0, est=None, tol=_TOL): 

549 '''Compute the I{perpendicular} intersection of this geodesic line 

550 and a geodesic from the given point. 

551 

552 @arg lat0: Latitude of the point (C{degrees}). 

553 @arg lon0: Longitude of the point (C{degrees}). 

554 @kwarg est: Optional, initial estimate for the distance C{s12} of 

555 the intersection I{along} this geodesic line (C{meter}). 

556 @kwarg tol: Convergence tolerance (C(meter)). 

557 

558 @return: The intersection point on this geodesic line, a L{GDict} 

559 from method L{Position} extended to 14 items C{lat1, lon1, 

560 azi1, lat2, lon2, azi2, a12, s12, lat0, lon0, azi0, a02, 

561 s02, at} with distance C{a02} in C{degrees} and C{s02} in 

562 C{meter} between the given C{lat0, lon0} point and the 

563 intersection C{lat2, lon2}, azimuth C{azi0} at the given 

564 point and C{at} the (perpendicular) angle between the 

565 geodesic and this line at the intersection. The geodesic 

566 azimuth at the intersection is C{(at + azi2)}. See method 

567 L{Position} for further details. 

568 

569 @see: Methods C{Intersecant2}, C{Intersection} and C{Position}. 

570 ''' 

571 return _MODS.geodesicw._PlumbTo(self, lat0, lon0, est=est, tol=tol) 

572 

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

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

575 

576 @arg s12: Distance from this this line's first point (C{meter}). 

577 @kwarg outmask: Bit-or'ed combination of L{Caps<pygeodesy.karney.Caps>} 

578 values specifying the quantities to be returned. 

579 

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

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

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

583 except when C{a12=NAN}. 

584 

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

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

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

588 

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

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

591 ''' 

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

593 

594 @Property_RO 

595 def s13(self): 

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

597 

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

599 ''' 

600 return self._s13 

601 

602 def SetArc(self, a13): 

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

604 

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

606 point (C{degrees}). 

607 

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

609 the reference point or C{NAN}. 

610 ''' 

611 if self._a13 != a13: 

612 self._GenSet(0, a12=a13) 

613 _update_all(self) 

614 return self._s13 

615 

616 def SetDistance(self, s13): 

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

618 

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

620 

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

622 and the reference point or C{NAN}. 

623 ''' 

624 if self._s13 != s13: 

625 self._GenSet(0, s12=s13) 

626 _update_all(self) 

627 return self._a13 

628 

629 @Property_RO 

630 def _stau1_ctau1(self): 

631 '''(INTERNAL) Cached/memoized. 

632 ''' 

633 s, c = _sincos2(self._E1) 

634 # tau1 = sig1 + B11 

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

636 # unnecessary because Einv inverts E 

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

638 

639 @property_ROver 

640 def _toProps7(self): 

641 '''(INTERNAL) 7-Tuple of C{toStr} properties. 

642 ''' 

643 C = _GeodesicLineExact 

644 return C.lat1, C.lon1, C.azi1, C.a13, C.s13, C.caps, C.geodesic 

645 

646 def toStr(self, **prec_sep_name): # PYCHOK signature 

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

648 

649 @see: L{Ellipsoid.toStr<pygeodesy.ellipsoids.Ellipsoid.toStr>} 

650 for further details. 

651 

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

653 ''' 

654 return self._instr(props=self._toProps7, **prec_sep_name) 

655 

656 

657__all__ += _ALL_DOCS(_GeodesicLineExact) 

658 

659# **) MIT License 

660# 

661# Copyright (C) 2016-2025 -- mrJean1 at Gmail -- All Rights Reserved. 

662# 

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

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

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

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

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

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

669# 

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

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

672# 

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

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

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

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

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

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

679# OTHER DEALINGS IN THE SOFTWARE.