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

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:Karney@Alum.MIT.edu>} (2008-2023)

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

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

41 _copysign_1_0, isfinite, \

42 _0_0, _1_0, _180_0, _360_0, \

43 _2__PI # PYCHOK used!

44 from pygeodesy.errors import _xError, _xkwds_pop2

45# from pygeodesy.fmath import fremainder # from .karney

46 from pygeodesy.fsums import fsumf_, fsum1f_

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

48 _sincos12, _sin1cos2, \

49 _sinf1cos2d, _TINY, _toNAN

50# from pygeodesy.geodesicw import _Intersecant2 # _MODS

51 from pygeodesy.lazily import _ALL_DOCS, _ALL_MODS as _MODS

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

53 _K_2_0, _llz2gl, _norm2, _norm180, \

54 _sincos2, _sincos2d, fremainder

55 from pygeodesy.props import Property_RO, property_ROver, _update_all

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

57

58 from math import degrees, fabs, radians

59

60 __all__ = ()

61 __version__ = '25.09.09'

62

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

64

65

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

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

68 any L{GeodesicLineExact} instances tied to the given

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

70 '''

71 _xGeodesicExact(gX=gX)

72 for glX in _glXs: # PYCHOK use weakref?

73 if glX._gX is gX:

74 _update_all(glX)

75

76

77 def _xGeodesicExact(**gX):

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

79 '''

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

81

82

83 class _GeodesicLineExact(_GeodesicBase):

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

85 '''

86 _a13 = _s13 = NAN

87# _azi1 = _0_0

88 _caps = Caps._AZIMUTH_LATITUDE_LONG_UNROLL

89# _cchi1 = NAN

90# _dn1 = NAN

91 _gX = None # Exact only

92# _k2 = NAN

93# _lat1 = _lon1 = _0_0

94# _salp0 = _calp0 = NAN

95# _salp1 = _calp1 = NAN

96# _somg1 = _comg1 = NAN

97# _ssig1 = _csig1 = NAN

98# _toNAN = False

99

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

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

102 '''

103# _xGeodesicExact(gX=gX)

104 if azi1 is None: # see GeodesicExact.InverseLine

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

106 azi1 = _atan2d(salp1, calp1)

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

108 azi1 = _norm180(azi1)

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

110 if name_:

111 self.name = name_

112

113 self._gX = gX # GeodesicExact only

114 self._lat1 = lat1 = _fix90(lat1)

115 self._lon1 = lon1

116 self._azi1 = azi1

117 self._salp1 = salp1

118 self._calp1 = calp1

119 # allow lat, azimuth and unrolling of lon

120 self._caps |= caps | gX.caps # | Caps._AZIMUTH_LATITUDE_LONG_UNROLL

121

122 self._toNAN = _toNAN(self._caps, lat1, lon1, azi1, salp1, calp1)

123

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

140 self._somg1 = sbet1 * self._salp0

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

142 # Without normalization we have schi1 = somg1.

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

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

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

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

147 if not (caps & Caps.LINE_OFF):

148 _glXs.append(self)

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

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

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

152

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

154 if _glXs: # may be empty or None

155 try: # PYCHOK no cover

156 _glXs.remove(self)

157 except (TypeError, ValueError):

158 pass

159 self._gX = None

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

161

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

163 if updated:

164 _update_all(self, *attrs)

165

166 @Property_RO

167 def a1(self):

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

169 the northward equatorial crossing and the first point.

170 '''

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

172

173 equatorarc = a1

174

175 @Property_RO

176 def a13(self):

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

178

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

180 '''

181 return self._a13

182

183 def Arc(self):

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

185

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

187 '''

188 return self.a13

189

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

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

192

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

194 second point (C{degrees}).

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

196 values specifying the quantities to be returned.

197

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

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

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

201 except when C{a12=NAN}.

202

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

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

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

206 '''

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

208

209 @Property_RO

210 def azi0(self):

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

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

213 '''

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

215

216 equatorazimuth = azi0

217

218 @Property_RO

219 def azi0_sincos2(self):

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

221 '''

222 return self._salp0, self._calp0

223

224 @Property_RO

225 def azi1(self):

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

227 '''

228 return self._azi1

229

230 @Property_RO

231 def azi1_sincos2(self):

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

233 '''

234 return self._salp1, self._calp1

235

236 @Property_RO

237 def _B41(self):

238 '''(INTERNAL) Cached/memoized.

239 '''

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

241

242 @Property_RO

243 def _C4a(self):

244 '''(INTERNAL) Cached/memoized.

245 '''

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

247

248 @Property_RO

249 def _caps_DISTANCE_IN(self):

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

251 '''

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

253

254 @Property_RO

255 def _D0k2(self):

256 '''(INTERNAL) Cached/memoized.

257 '''

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

259

260 @Property_RO

261 def _D1(self):

262 '''(INTERNAL) Cached/memoized.

263 '''

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

265

266 def Distance(self):

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

268

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

270 '''

271 return self.s13

272

273 @Property_RO

274 def _E0b(self):

275 '''(INTERNAL) Cached/memoized.

276 '''

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

278

279 @Property_RO

280 def _E1(self):

281 '''(INTERNAL) Cached/memoized.

282 '''

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

284

285 @Property_RO

286 def _eF(self):

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

288 '''

289 e = _MODS.elliptic

290 try: # see .gx.GeodesicExact._ef_reset_k2

291 return e.Elliptic(k2=-self._k2, alpha2=-self.geodesic.ep2)

292 except e.EllipticError: # nonfinite

293 return None

294

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

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

297

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

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

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

301 except when C{a12=NAN}.

302 '''

303 r, Cs = GDict(), Caps

304 if outmask:

305 outmask &= self._caps & Cs._OUT_MASK

306 eF = self._eF

307 if eF is None or self._toNAN or not isfinite(s12_a12): # _toNAN(outmask, s12_a12)?

308 # E2 = sig12 = ssig12 = csig12 = NAN

309 d = dict(a12=s12_a12) if arcmode else dict(s12=s12_a12)

310 return r._toNAN(outmask | Cs.NONFINITONAN, # for backward compatibility

311 lat1=self.lat1, lon1=self.lon1, azi1=self.azi1, **d)

312 gX = self.geodesic # ._gX

313

314 if arcmode: # s12_a12 is (spherical) arc length

315 r.set_(a12=s12_a12, s12=NAN)

316 sig12 = radians(s12_a12)

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

318 if not _K_2_0: # PYCHOK no cover

319 d = fremainder(fabs(s12_a12), _180_0)

320 if d == 90:

321 csig12 = _0_0

322 elif d == 0:

323 ssig12 = _0_0

324 E2 = _0_0

325 elif self._caps_DISTANCE_IN: # s12_a12 is distance

326 t = s12_a12 / self._E0b

327 s, c = _sincos2(t) # tau12

328 # tau2 = tau1 + tau12

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

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

331 ssig12, csig12 = _sincos2(sig12)

332 r.set_(a12=degrees(sig12), s12=s12_a12)

333 else: # uninitialized or impossible distance requested

334 return r.set_(a12=NAN, s12=NAN)

335

336 # sig2 = sig1 + sig12

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

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

339 dn2 = eF.fDelta(*t)

340

341 if (outmask & Cs.DISTANCE):

342 if arcmode: # or f_0_01

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

344 # AB1 = _E0 * (E2 - _E1)

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

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

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

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

349 else:

350 s12 = s12_a12

351 r.set_(s12=s12)

352

353 if not (outmask ^ Cs.DISTANCE): # all done, see ._GenSet

354 return r

355

356 if self._debug: # PYCHOK no cover

357 outmask |= self._debug & Cs._DEBUG_DIRECT_LINE

358

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

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

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

362

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

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

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

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

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

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

369 cbet2 = csig2 = _TINY

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

371 salp2 = salp0

372 calp2 = calp0 * csig2 # no need to normalize

373

374 if (outmask & Cs.AZIMUTH):

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

376 reverse=outmask & Cs.REVERSE2))

377

378 if (outmask & Cs.LATITUDE):

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

380

381 if (outmask & Cs.LONGITUDE):

382 schi1 = self._somg1

383 cchi1 = self._cchi1

384 schi2 = ssig2 * salp0 # schi2 = somg2 without normalization

385 cchi2 = gX.f1 * dn2 * csig2

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

387 -self._H1, sig12)

388 if (outmask & Cs.LONG_UNROLL):

389 e = _copysign_1_0(salp0) # east-going?

390 tchi1 = e * schi1

391 tchi2 = e * schi2

392 chi12 = e * fsum1f_(atan2(ssig1, csig1), -atan2(ssig2, csig2),

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

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

395 if fabs(lon2) > _360_0: # XXX kludge

396 lon2 = _norm180(lon2)

397 else:

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

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

400 r.set_(lon2=lon2)

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

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

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

404

405 if (outmask & Cs._REDUCEDLENGTH_GEODESICSCALE):

406 dn1 = self._dn1

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

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

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

410 csig1=csig1, dn2=dn2, D1=self._D1, J12=J12)

411 if (outmask & Cs.REDUCEDLENGTH):

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

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

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

415 -dn1 * (ssig1 * csig2),

416 -J12 * (csig1 * csig2)))

417 if (outmask & Cs.GEODESICSCALE):

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

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

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

421

422 if (outmask & Cs.AREA):

423 A4 = salp0 * calp0

424 if A4:

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

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

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

428 # If csig12 > 0, write

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

430 # else

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

432 # No need to normalize

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

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

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

436 A4 *= gX._e2a2

437 B41 = self._B41

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

439 S12 = (B42 - B41) * A4

440 else:

441 S12 = A4 = B41 = B42 = _0_0

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

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

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

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

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

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

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

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

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

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

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

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

454 r.set_(S12=S12)

455

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

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

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

459 return r

460

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

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

463

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

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

466 '''

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

468 return r.toDirect9Tuple()

469

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

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

472 '''

473 Cs = Caps

474 if debug: # PYCHOK no cover

475 self._debug |= debug & Cs._DEBUG_ALL

476 # _CapsBase.debug._update(self)

477 if s12 is None:

478 if a12 is None: # see GeodesicExact.Line

479 return self

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

481 elif a12 is None:

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

483 self._s13 = s12

484 self._a13 = a12

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

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

487 return _llz2gl(self, **llz2)

488

489 @Property_RO

490 def geodesic(self):

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

492 '''

493 _xGeodesicExact(geodesic=self._gX)

494 return self._gX

495

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

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

498

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

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

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

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

503

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

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

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

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

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

509 along the geodesic from the circle center to the intersection

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

511 and this line at the intersection. The geodesic azimuth

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

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

514 L{GDict} instance.

515

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

517 intersect, no I{perpencular} geodetic

518 intersection or no convergence.

519

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

521 '''

522 try:

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

524 except (TypeError, ValueError) as x:

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

526

527 @Property_RO

528 def _H0e2_f1(self):

529 '''(INTERNAL) Cached/memoized.

530 '''

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

532

533 @Property_RO

534 def _H1(self):

535 '''(INTERNAL) Cached/memoized.

536 '''

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

538

539 @Property_RO

540 def lat1(self):

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

542 '''

543 return self._lat1

544

545 @Property_RO

546 def lon1(self):

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

548 '''

549 return self._lon1

550

551 @Property_RO

552 def _lon1_norm180(self):

553 '''(INTERNAL) Cached/memoized.

554 '''

555 return _norm180(self._lon1)

556

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

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

559 and a geodesic from the given point.

560

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

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

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

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

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

566

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

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

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

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

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

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

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

574 geodesic and this line at the intersection. The geodesic

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

576 L{Position} for further details.

577

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

579 '''

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

581

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

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

584

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

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

587 values specifying the quantities to be returned.

588

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

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

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

592 except when C{a12=NAN}.

593

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

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

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

597

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

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

600 '''

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

602

603 @Property_RO

604 def s13(self):

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

606

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

608 '''

609 return self._s13

610

611 def SetArc(self, a13):

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

613

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

615 point (C{degrees}).

616

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

618 the reference point or C{NAN}.

619 '''

620 if self._a13 != a13:

621 self._GenSet(0, a12=a13)

622 _update_all(self)

623 return self._s13

624

625 def SetDistance(self, s13):

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

627

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

629

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

631 and the reference point or C{NAN}.

632 '''

633 if self._s13 != s13:

634 self._GenSet(0, s12=s13)

635 _update_all(self)

636 return self._a13

637

638 @Property_RO

639 def _stau1_ctau1(self):

640 '''(INTERNAL) Cached/memoized.

641 '''

642 s, c = _sincos2(self._E1)

643 # tau1 = sig1 + B11

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

645 # unnecessary because Einv inverts E

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

647

648 @property_ROver

649 def _toProps7(self):

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

651 '''

652 C = _GeodesicLineExact

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

654

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

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

657

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

659 for further details.

660

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

662 '''

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

664

665

666 __all__ += _ALL_DOCS(_GeodesicLineExact)

667

668# **) MIT License

669#

670# Copyright (C) 2016-2026 -- mrJean1 at Gmail -- All Rights Reserved.

671#

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

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

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

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

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

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

678#

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

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

681#

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

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

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

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

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

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

688# OTHER DEALINGS IN THE SOFTWARE.