Coverage for pygeodesy/geodesicw.py: 91%

233 statements  

« prev     ^ index     » next       coverage.py v7.6.1, created at 2025-05-29 12:40 -0400

1 

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

3 

4u'''Wrapper around Python classes C{geodesic.Geodesic} and C{geodesicline.GeodesicLine} from 

5I{Karney}'s Python package U{geographiclib<https://PyPI.org/project/geographiclib>}, provided 

6that package is installed. 

7 

8The I{wrapped} class methods return a L{GDict} instance offering access to the C{dict} items 

9either by C{key} or by C{attribute} name. 

10 

11With env variable C{PYGEODESY_GEOGRAPHICLIB} left undefined or set to C{"2"}, this module and modules 

12L{pygeodesy.geodesici}, L{pygeodesy.geodesicx} and L{pygeodesy.karney} will use U{GeographicLib 2.0 

13<https://GeographicLib.SourceForge.io/C++/doc/>} transcoding, otherwise C{1.52} or older. 

14''' 

15 

16from pygeodesy.basics import _copysign, _xinstanceof 

17from pygeodesy.constants import EPS, NAN, _EPSqrt as _TOL, _0_5 

18from pygeodesy.datums import _earth_datum, _WGS84, _EWGS84 

19# from pygeodesy.dms import F_D # from .latlonBase 

20# from pygeodesy.ellipsoids import _EWGS84 # from .datums 

21from pygeodesy.errors import _AssertionError, GeodesicError, \ 

22 IntersectionError 

23from pygeodesy.fsums import Fsum, Fmt, unstr 

24from pygeodesy.internals import typename, _under 

25from pygeodesy.interns import NN, _DOT_, _SPACE_, _to_, _too_ 

26from pygeodesy.karney import _atan2d, Caps, Direct9Tuple, GDict, \ 

27 Inverse10Tuple, _kWrapped 

28from pygeodesy.latlonBase import LatLonBase as _LLB, F_D, Radius_ 

29from pygeodesy.lazily import _ALL_LAZY, _ALL_MODS as _MODS 

30from pygeodesy.named import callername, classname, _name1__, _name2__ 

31from pygeodesy.namedTuples import Destination3Tuple, Distance3Tuple 

32from pygeodesy.props import Property, Property_RO, property_RO, \ 

33 property_ROver 

34# from pygeodesy.streprs import Fmt, unstr # from .fsums 

35# from pygeodesy.units import Radius_ # from .latlonBase 

36from pygeodesy.utily import _unrollon, _Wrap, wrap360, fabs # PYCHOK used! 

37 

38from contextlib import contextmanager 

39# from math import fabs # from .utily 

40 

41__all__ = _ALL_LAZY.geodesicw 

42__version__ = '25.05.28' 

43 

44_plumb_ = 'plumb' 

45_TRIPS = 65 

46 

47 

48class _gWrapped(_kWrapped): 

49 '''(INTERNAL) Wrapper for some of I{Karney}'s U{geographiclib 

50 <https://PyPI.org/project/geographiclib>} classes. 

51 ''' 

52 

53 @property_ROver # MCCABE 24 

54 def Geodesic(self): 

55 '''Get the I{wrapped} C{geodesic.Geodesic} class from I{Karney}'s Python 

56 U{geographiclib<https://GitHub.com/geographiclib/geographiclib-python>}, 

57 provided the latter is installed. 

58 ''' 

59 _Geodesic = self.geographiclib.Geodesic 

60 if not (Caps.LATITUDE == _Geodesic.LATITUDE and 

61 Caps.LONGITUDE == _Geodesic.LONGITUDE and 

62 Caps.AZIMUTH == _Geodesic.AZIMUTH and 

63 Caps.DISTANCE == _Geodesic.DISTANCE and 

64 Caps.DISTANCE_IN == _Geodesic.DISTANCE_IN and 

65 Caps.REDUCEDLENGTH == _Geodesic.REDUCEDLENGTH and 

66 Caps.GEODESICSCALE == _Geodesic.GEODESICSCALE and 

67 Caps.AREA == _Geodesic.AREA and 

68 Caps.STANDARD == _Geodesic.STANDARD and 

69 Caps.ALL == _Geodesic.ALL): 

70 raise _AssertionError(Caps=bin(Caps.ALL), 

71 Geodesic=bin(_Geodesic.ALL)) 

72 

73 class Geodesic(_Geodesic): 

74 '''I{Wrapper} for I{Karney}'s Python U{geodesic.Geodesic 

75 <https://PyPI.org/project/geographiclib>} class. 

76 ''' 

77 _datum = _WGS84 

78 _debug = 0 # like .geodesicx.bases._GeodesicBase 

79 LINE_OFF = 0 # in .azimuthal._GnomonicBase and .css.CassiniSoldner 

80 _name = NN 

81 STANDARD_LINE = Caps.STANDARD_LINE 

82 

83 def __init__(self, a_ellipsoid=_EWGS84, f=None, **name): # PYCHOK signature 

84 '''New I{wrapped} C{geodesic.Geodesic} instance. 

85 

86 @arg a_ellipsoid: The equatorial radius I{a} (C{meter}, conventionally), 

87 an ellipsoid (L{Ellipsoid}) or a datum (L{Datum}). 

88 @arg f: The ellipsoid's flattening (C{scalar}), required if B{C{a_ellipsoid}) 

89 is C{meter}, ignored otherwise. 

90 @kwarg name: Optional C{B{name}=NN} (C{str}). 

91 ''' 

92 _earth_datum(self, a_ellipsoid, f=f, **name) # raiser=NN 

93 E = self.ellipsoid 

94 with _wargs(self, *E.a_f, **name) as args: 

95 _Geodesic.__init__(self, *args) 

96 if name: 

97 self._name, _ = _name2__(name, _or_nameof=E) 

98 

99 def Area(self, polyline=False, **name): # like GeodesicExact.Area 

100 '''Return a C{PolygonArea} instance with method C{Compute} extended. 

101 ''' 

102 _AreaBase = _wrapped._PolygonArea # in .karney._kwrapped 

103 

104 class _PolygonArea(_AreaBase): 

105 # def __init__(self, *earth_polyline): 

106 # _PolygonArea.__init__(self, *earth_polyline) 

107 

108 def Compute(self, reverse=False, sign=True, polar=False): 

109 '''Use C{B{polar}=True} to adjust the area, see function 

110 L{areaOf<pygeodesy.geodesicx.gxarea.areaOf>}. 

111 ''' 

112 n, p, a = _AreaBase.Compute(self, reverse=reverse, sign=sign) 

113 if polar: # see .geodesicx.gxarea.GeodesicAreaExact._reduced 

114 a += _copysign(self.area0 * _0_5 * n, a) 

115 return n, p, a 

116 

117 A = _PolygonArea(self, polyline) 

118 A.name = _name2__(name, _or_nameof=self) 

119 return A 

120 

121 def ArcDirect(self, lat1, lon1, azi1, a12, outmask=Caps.STANDARD): # PYCHOK no cover 

122 '''Return the C{_Geodesic.ArcDirect} result as L{GDict}. 

123 ''' 

124 with _wargs(self, lat1, lon1, azi1, a12, outmask) as args: 

125 d = _Geodesic.ArcDirect(self, *args) 

126 return GDict(d) 

127 

128 def ArcDirectLine(self, lat1, lon1, azi1, a12, caps=Caps.STANDARD_LINE, **name): # PYCHOK no cover 

129 '''Return the C{_Geodesic.ArcDirectLine} as I{wrapped} C{GeodesicLine}. 

130 ''' 

131 return self._GenDirectLine(lat1, lon1, azi1, True, a12, caps, **name) 

132 

133 @property_RO 

134 def datum(self): 

135 '''Get this geodesic's datum (C{Datum}). 

136 ''' 

137 return self._datum 

138 

139 @Property 

140 def debug(self): 

141 '''Get the C{debug} option (C{bool}). 

142 ''' 

143 return bool(self._debug) 

144 

145 @debug.setter # PYCHOK setter! 

146 def debug(self, debug): 

147 '''Set the C{debug} option (C{bool}) to include more 

148 details in L{GDict} results. 

149 ''' 

150 self._debug = Caps._DEBUG_ALL if debug else 0 

151 

152 def Direct(self, lat1, lon1, azi1, s12=0, outmask=Caps.STANDARD): 

153 '''Return the C{_Geodesic.Direct} result as L{GDict}. 

154 ''' 

155 with _wargs(self, lat1, lon1, azi1, s12, outmask) as args: 

156 d = _Geodesic.Direct(self, *args) 

157 return GDict(d) 

158 

159 def Direct3(self, lat1, lon1, azi1, s12): # PYCHOK outmask 

160 '''Return the destination lat, lon and reverse azimuth 

161 in C{degrees} as L{Destination3Tuple}. 

162 ''' 

163 d = self.Direct(lat1, lon1, azi1, s12, outmask=Caps._DIRECT3) 

164 return Destination3Tuple(d.lat2, d.lon2, d.azi2) 

165 

166 def _DirectLine(self, ll1, azi12, s12=0, **caps_name): 

167 '''(INTERNAL) Short-cut version. 

168 ''' 

169 return self.DirectLine(ll1.lat, ll1.lon, azi12, s12, **caps_name) 

170 

171 def DirectLine(self, lat1, lon1, azi1, s12, caps=Caps.STANDARD_LINE, **name): 

172 '''Return the C{_Geodesic.DirectLine} as I{wrapped} C{GeodesicLine}. 

173 ''' 

174 return self._GenDirectLine(lat1, lon1, azi1, False, s12, caps, **name) 

175 

176 @Property_RO 

177 def ellipsoid(self): 

178 '''Get this geodesic's ellipsoid (C{Ellipsoid}). 

179 ''' 

180 return self.datum.ellipsoid 

181 

182 @property_RO 

183 def f1(self): # in .css.CassiniSoldner.reset 

184 '''Get the geodesic's ellipsoid's I{1 - flattening} (C{float}). 

185 ''' 

186 return getattr(self, _under(Geodesic.f1.name), self.ellipsoid.f1) 

187 

188 def _GDictDirect(self, lat, lon, azi, arcmode, s12_a12, outmask=Caps.STANDARD): 

189 '''(INTERNAL) Get C{_Geodesic._GenDirect} result as C{GDict}. 

190 ''' 

191 with _wargs(self, lat, lon, azi, arcmode, s12_a12, outmask) as args: 

192 t = _Geodesic._GenDirect(self, *args) 

193 return Direct9Tuple(t).toGDict() # *t 

194 

195 def _GDictInverse(self, lat1, lon1, lat2, lon2, outmask=Caps.STANDARD): 

196 '''(INTERNAL) Get C{_Geodesic._GenInverse} result as L{Inverse10Tuple}. 

197 ''' 

198 with _wargs(self, lat1, lon1, lat2, lon2, outmask) as args: 

199 t = _Geodesic._GenInverse(self, *args) 

200 return Inverse10Tuple(t).toGDict(lon1=lon1, lon2=lon2) # *t 

201 

202 def _GenDirectLine(self, lat1, lon1, azi1, arcmode, s12_a12, *caps, **name): 

203 '''(INTERNAL) Invoked by C{_Geodesic.DirectLine} and C{-.ArcDirectLine}, 

204 returning the result as a I{wrapped} C{GeodesicLine}. 

205 ''' 

206 with _wargs(self, lat1, lon1, azi1, arcmode, s12_a12, *caps, **name) as args: 

207 t = _Geodesic._GenDirectLine(self, *args) 

208 return self._Line13(t, **name) 

209 

210 def _Inverse(self, ll1, ll2, wrap, **outmask): 

211 '''(INTERNAL) Short-cut version, see .ellipsoidalBaseDI.intersecant2. 

212 ''' 

213 if wrap: 

214 ll2 = _unrollon(ll1, _Wrap.point(ll2)) 

215 return self.Inverse(ll1.lat, ll1.lon, ll2.lat, ll2.lon, **outmask) 

216 

217 def Inverse(self, lat1, lon1, lat2, lon2, outmask=Caps.STANDARD): 

218 '''Return the C{_Geodesic.Inverse} result as L{GDict}. 

219 ''' 

220 with _wargs(self, lat1, lon1, lat2, lon2, outmask) as args: 

221 d = _Geodesic.Inverse(self, *args) 

222 return GDict(d) 

223 

224 def Inverse1(self, lat1, lon1, lat2, lon2, wrap=False): 

225 '''Return the non-negative, I{angular} distance in C{degrees}. 

226 

227 @kwarg wrap: If C{True}, wrap or I{normalize} and unroll 

228 B{C{lat2}} and BC{lon2}} (C{bool}). 

229 ''' 

230 # see .FrechetKarney.distance, .HausdorffKarney._distance 

231 # and .HeightIDWkarney._distances 

232 if wrap: 

233 _, lat2, lon2 = _Wrap.latlon3(lat1, lat2, lon2, True) # _Geodesic.LONG_UNROLL 

234 r = self.Inverse(lat1, lon1, lat2, lon2) 

235 # XXX _Geodesic.DISTANCE needed for 'a12'? 

236 return fabs(r.a12) 

237 

238 def Inverse3(self, lat1, lon1, lat2, lon2): # PYCHOK outmask 

239 '''Return the distance in C{meter} and the forward and reverse 

240 azimuths in C{degrees} as L{Distance3Tuple}. 

241 ''' 

242 r = self.Inverse(lat1, lon1, lat2, lon2, outmask=Caps._INVERSE3) 

243 return Distance3Tuple(r.s12, wrap360(r.azi1), wrap360(r.azi2)) 

244 

245 def _InverseLine(self, ll1, ll2, wrap, **caps_name): 

246 '''(INTERNAL) Short-cut version. 

247 ''' 

248 if wrap: 

249 ll2 = _unrollon(ll1, _Wrap.point(ll2)) 

250 return self.InverseLine(ll1.lat, ll1.lon, ll2.lat, ll2.lon, **caps_name) 

251 

252 def InverseLine(self, lat1, lon1, lat2, lon2, caps=Caps.STANDARD_LINE, **name): 

253 '''Return the C{_Geodesic.InverseLine} as I{wrapped} C{GeodesicLine}. 

254 ''' 

255 with _wargs(self, lat1, lon1, lat2, lon2, caps, **name) as args: 

256 t = _Geodesic.InverseLine(self, *args) 

257 return self._Line13(t, **name) 

258 

259 def Line(self, lat1, lon1, azi1, caps=Caps.STANDARD_LINE, **name): 

260 '''Set up a I{wrapped} C{GeodesicLine} to compute several points 

261 along a single, I{wrapped} (this) geodesic. 

262 ''' 

263 return _wrapped.GeodesicLine(self, lat1, lon1, azi1, caps=caps, **name) 

264 

265 def _Line13(self, t, **name): 

266 '''(INTERNAL) Wrap C{_GeodesicLine}, add distance and arc length 

267 to reference point 3. 

268 ''' 

269 gl = _wrapped.GeodesicLine(self, t.lat1, t.lon1, t.azi1, caps=t.caps, 

270 salp1=t.salp1, calp1=t.calp1, **name) 

271 gl.a13, gl.s13 = t.a13, t.s13 

272 return gl 

273 

274 @property_RO 

275 def name(self): 

276 '''Get the name (C{str}). 

277 ''' 

278 return self._name 

279 

280 Polygon = Area 

281 WGS84 = None # _EWGS84.geodesicw recusion 

282 

283 # Geodesic.ArcDirect.__doc__ = _Geodesic.ArcDirect.__doc__ 

284 # Geodesic.Direct.__doc__ = _Geodesic.Direct.__doc__ 

285 # Geodesic.Inverse.__doc__ = _Geodesic.Inverse.__doc__ 

286 # Geodesic.InverseLine.__doc__ = _Geodesic.InverseLinr.__doc__ 

287 # Geodesic.Line.__doc__ = _Geodesic.Line.__doc__ 

288 return Geodesic # overwrite property_ROver 

289 

290 @property_ROver # MCCABE 16 

291 def GeodesicLine(self): 

292 '''Get the I{wrapped} C{geodesicline.GeodesicLine} class from I{Karney}'s 

293 Python U{geographiclib<https://GitHub.com/geographiclib/geographiclib-python>}, 

294 provided the latter is installed. 

295 ''' 

296 _GeodesicLine = self.geographiclib.GeodesicLine 

297 

298 class GeodesicLine(_GeodesicLine): 

299 '''I{Wrapper} for I{Karney}'s Python U{geodesicline.GeodesicLine 

300 <https://PyPI.org/project/geographiclib>} class. 

301 ''' 

302 _geodesic = None 

303 _name = NN 

304 

305 def __init__(self, geodesic, lat1, lon1, azi1, **caps_name_): # salp1=NAN, calp1=NAN 

306 '''New I{wrapped} C{geodesicline.GeodesicLine} instance. 

307 

308 @arg geodesic: A I{wrapped} C{Geodesic} instance. 

309 @arg lat1: Latitude of the first points (C{degrees}). 

310 @arg lon1: Longitude of the first points (C{degrees}). 

311 @arg azi1: Azimuth at the first points (compass C{degrees360}). 

312 @kwarg caps_name_: Optional keyword arguments C{B{caps}=Caps.STANDARD}, 

313 a bit-or'ed combination of L{Caps<pygeodesy.karney.Caps>} 

314 values specifying the capabilities the C{GeodesicLine} instance 

315 should possess, an optional C{B{name}=NN} plus C{salp1=NAN} and 

316 C{calp1=NAN} for I{INTERNAL} use. 

317 ''' 

318 _xinstanceof(_wrapped.Geodesic, geodesic=geodesic) 

319 with _wargs(self, geodesic, lat1, lon1, azi1, **caps_name_) as args: 

320 name, caps_ = _name2__(caps_name_, _or_nameof=geodesic) 

321 _GeodesicLine.__init__(self, *args, **caps_) # XXX avoid updates? 

322 if name: 

323 self._name = name 

324 self._geodesic = geodesic 

325 

326 @Property_RO 

327 def a1(self): 

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

329 the northward equatorial crossing and point C{(lat1, lon1)}. 

330 

331 @see: U{EquatorialArc<https://GeographicLib.SourceForge.io/ 

332 C++/doc/classGeographicLib_1_1GeodesicLine.html>} 

333 ''' 

334 try: 

335 return _atan2d(self._ssig1, self._csig1) 

336 except AttributeError: 

337 return NAN # see .geodesicx.gxline._GeodesicLineExact 

338 

339 equatorarc = a1 

340 

341 def Arc(self): 

342 '''Return the angular distance to point 3 (C{degrees} or C{NAN}). 

343 ''' 

344 return self.a13 

345 

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

347 '''Return the position at C{B{a12} degrees} on this line. 

348 

349 @arg a12: Angular distance from this line's first point 

350 (C{degrees}). 

351 

352 @see: Method L{Position} for further details. 

353 ''' 

354 with _wargs(self, a12, outmask) as args: 

355 d = _GeodesicLine.ArcPosition(self, *args) 

356 return GDict(d) 

357 

358 @Property_RO 

359 def azi0(self): # see .css.CassiniSoldner.forward4 

360 '''Get the I{equatorial azimuth} (C{degrees}), the azimuth of the 

361 geodesic line as it crosses the equator in a northward direction. 

362 

363 @see: U{EquatorialAzimuth<https://GeographicLib.SourceForge.io/ 

364 C++/doc/classGeographicLib_1_1GeodesicLine.html>} 

365 ''' 

366 try: 

367 return _atan2d(self._salp0, self._calp0) 

368 except AttributeError: 

369 return NAN # see .geodesicx.gxline._GeodesicLineExact 

370 

371 equatorazimuth = azi0 

372 

373 def Distance(self): 

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

375 ''' 

376 return self.s13 

377 

378 @property_RO 

379 def geodesic(self): 

380 '''Get the I{wrapped} geodesic (L{Geodesic}). 

381 ''' 

382 return self._geodesic 

383 

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

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

386 

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

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

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

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

391 

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

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

394 extended to 14 items C{lat1, lon1, azi1, lat2, lon2, azi2, a12, 

395 s12, lat0, lon0, azi0, a02, s02, at} with the circle center 

396 C{lat0}, C{lon0}, azimuth C{azi0} at the intersection, distance 

397 C{a02} in C{degrees} and C{s02} in C{meter} along the geodesic 

398 from the circle center to the intersection C{lat2, lon2} and 

399 the angle C{at} between the geodesic and this line at the 

400 intersection. The I{geodesic} azimuth at the intersection is 

401 C{(at + azi2)}. If this line is tangential to the circle, both 

402 intersections are the same L{GDict} instance. 

403 

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

405 intersect. 

406 

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

408 ''' 

409 return _Intersecant2(self, lat0, lon0, radius, tol=tol) 

410 

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

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

413 with a geodesic from the given point. 

414 

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

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

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

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

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

420 

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

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

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

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

425 and C{meter} from the given point C{lat0, lon0} to the 

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

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

428 geodesic and this line at the intersection point. The 

429 geodesic azimuth at the intersection is C{(at + azi2)}. 

430 See method L{Position} for further details. 

431 

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

433 ''' 

434 return _PlumbTo(self, lat0, lon0, est=est, tol=tol) 

435 

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

437 '''Return the position at distance C{B{s12} meter} on this line. 

438 

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

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

441 values specifying the quantities to be returned. 

442 

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

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

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

446 except when C{a12=NAN}. 

447 ''' 

448 with _wargs(self, s12, outmask) as args: 

449 d = _GeodesicLine.Position(self, *args) 

450 return GDict(d) 

451 

452 # GeodesicLine.ArcPosition.__doc__ = _GeodesicLine.ArcPosition.__doc__ 

453 # GeodesicLine.Position.__doc__ = _GeodesicLine.Position.__doc__ 

454 return GeodesicLine # overwrite property_ROver 

455 

456 @property_ROver 

457 def Geodesic_WGS84(self): 

458 '''Get the I{wrapped} C{Geodesic(WGS84)} singleton, provided the 

459 U{geographiclib<https://PyPI.org/project/geographiclib>} package 

460 is installed, otherwise an C{ImportError}. 

461 ''' 

462 return _EWGS84.geodesicw # overwrite property_ROver 

463 

464_wrapped = _gWrapped() # PYCHOK singleton, .ellipsoids, .test/base.py 

465 

466 

467class Geodesic(_gWrapped): # overwritten by 1st instance 

468 '''I{Wrapper} around I{Karney}'s class U{geographiclib.geodesic.Geodesic 

469 <https://GeographicLib.SourceForge.io/Python/doc/code.html>}. 

470 ''' 

471 def __new__(unused, a_ellipsoid=_EWGS84, f=None, **name): 

472 '''Return a I{wrapped} C{geodesic.Geodesic} instance from I{Karney}'s 

473 Python U{geographiclib<https://PyPI.org/project/geographiclib>}, 

474 provide the latter is installed, otherwise an C{ImportError}. 

475 

476 @arg a_ellipsoid: An ellipsoid (L{Ellipsoid}) or datum (L{Datum}) 

477 or the equatorial radius I{a} of the ellipsoid (C{meter}). 

478 @arg f: The flattening of the ellipsoid (C{scalar}), required if 

479 B{C{a_ellipsoid}}) is C{meter}, ignored otherwise. 

480 @kwarg name: Optional C{B{name}=NN} (C{str}). 

481 ''' 

482 g = _wrapped.Geodesic(a_ellipsoid, f=f, **name) 

483 _MODS.geodesicw.Geodesic = type(g) # overwrite class 

484 return g 

485 

486 

487class GeodesicLine(_gWrapped): # overwritten by 1st instance 

488 '''I{Wrapper} around I{Karney}'s class U{geographiclib.geodesicline.GeodesicLine 

489 <https://GeographicLib.SourceForge.io/Python/doc/code.html>}. 

490 ''' 

491 def __new__(unused, geodesic, lat1, lon1, azi1, caps=Caps.STANDARD_LINE, **name): 

492 '''Return a I{wrapped} C{geodesicline.GeodesicLine} instance from I{Karney}'s 

493 Python U{geographiclib<https://PyPI.org/project/geographiclib>}, provided 

494 the latter is installed, otherwise an C{ImportError}. 

495 

496 @arg geodesic: A I{wrapped} L{Geodesic} instance. 

497 @arg lat1: Latitude of the first points (C{degrees}). 

498 @arg lon1: Longitude of the first points (C{degrees}). 

499 @arg azi1: Azimuth at the first points (compass C{degrees360}). 

500 @kwarg caps: Optional, bit-or'ed combination of L{Caps<pygeodesy.karney.Caps>} 

501 values specifying the capabilities the C{GeodesicLine} instance 

502 should possess, i.e., which quantities can be returned by methods 

503 C{GeodesicLine.Position} and C{GeodesicLine.ArcPosition}. 

504 @kwarg name: Optional C{B{name}=NN} (C{str}). 

505 ''' 

506 gl = _wrapped.GeodesicLine(geodesic, lat1, lon1, azi1, caps=caps, **name) 

507 _MODS.geodesicw.GeodesicLine = type(gl) # overwrite class 

508 return gl 

509 

510 

511def Geodesic_WGS84(): 

512 '''Get the I{wrapped} L{Geodesic}C{(WGS84)} singleton, provided 

513 U{geographiclib<https://PyPI.org/project/geographiclib>} is 

514 installed, otherwise an C{ImportError}. 

515 ''' 

516 return _wrapped.Geodesic_WGS84 

517 

518 

519class _wargs(object): # see also .formy._idllmn6, .latlonBase._toCartesian3, .vector2d._numpy 

520 '''(INTERNAL) C{geographiclib} arguments and exception handler. 

521 ''' 

522 @contextmanager # <https://www.Python.org/dev/peps/pep-0343/> Examples 

523 def __call__(self, inst, *args, **kwds): 

524 '''(INTERNAL) Yield C{tuple(B{args})} with any errors raised 

525 as L{GeodesicError} embellished with all B{C{kwds}}. 

526 ''' 

527 try: 

528 yield args 

529 except Exception as x: 

530 u = _DOT_(classname(inst), callername(up=2, underOK=True)) 

531 raise GeodesicError(unstr(u, *args, **_name1__(kwds)), cause=x) 

532 

533_wargs = _wargs() # PYCHOK singleton 

534 

535 

536def _Intersecant2(gl, lat0, lon0, radius, tol=_TOL, form=F_D): # MCCABE in LatLonEllipsoidalBaseDI.intersecant2, .geodesicx.gxline.Intersecant2 

537 # (INTERNAL) Return the intersections of a circle at C{lat0, lon0} 

538 # and a geodesic line as a 2-Tuple C{(P, Q)}, each a C{GDict}. 

539 r = Radius_(radius) 

540 n = typename(_Intersecant2)[1:] 

541 _P = gl.Position 

542 _I = gl.geodesic.Inverse 

543 

544 def _R3(s): 

545 # radius, intersection, etc. at distance C{s} 

546 P = _P(s) 

547 d = _I(lat0, lon0, P.lat2, P.lon2) 

548 return fabs(d.s12), P, d 

549 

550 def _bisect2(s, c, Rc, r, tol, _R3): 

551 _s = Fsum(c).fsumf_ 

552 for i in range(_TRIPS): 

553 b = _s(s) 

554 Rb, P, d = _R3(b) 

555 if Rb > r: 

556 break 

557 else: # b >>> s and c >>> s 

558 raise ValueError(Fmt.no_convergence(b, s)) 

559 # Rb > r > Rc 

560 for i in range(_TRIPS): # 47-48 

561 s = (b + c) * _0_5 

562 R, P, d = _R3(s) 

563 if Rb > R > r: 

564 b, Rb = s, R 

565 elif Rc < R < r: 

566 c, Rc = s, R 

567# else: 

568# break 

569 t = fabs(b - c) 

570 if t < tol: # or fabs(R - r) < tol: 

571 break 

572 else: # t = min(t, fabs(R - r)) 

573 raise ValueError(Fmt.no_convergence(t, tol)) 

574 i += C.iteration # combine iterations 

575 P.set_(lat0=lat0, lon0=lon0, azi0=d.azi1, iteration=i, 

576 a02=d.a12, s02=d.s12, at=d.azi2 - P.azi2, name=n) 

577 return P, s 

578 

579 # get the perpendicular intersection of 2 geodesics, 

580 # one the plumb, pseudo-rhumb line to the other 

581 C = _PlumbTo(gl, lat0, lon0, tol=tol) 

582 try: 

583 a = fabs(C.s02) # distance between centers 

584 if a < r: 

585 c = C.s12 # distance along pseudo-rhumb line 

586 h = _copysign(r, c) # past half chord length 

587 P, p = _bisect2( h, c, a, r, tol, _R3) 

588 Q, q = _bisect2(-h, c, a, r, tol, _R3) 

589 if fabs(p - q) < max(EPS, tol): 

590 Q = P 

591 elif a > r: 

592 raise ValueError(_too_(Fmt.distant(a))) 

593 else: # tangential 

594 P = Q = C 

595 except Exception as x: 

596 t = _LLB(C.lat2, C.lon2).toStr(form=form) 

597 t = _SPACE_(x, _plumb_, _to_, Fmt.PAREN(t)) 

598 raise IntersectionError(t, txt=None, cause=x) 

599 

600 return P, Q 

601 

602 

603def _PlumbTo(gl, lat0, lon0, est=None, tol=_TOL): 

604 # (INTERNAL) Return the I{perpendicular} intersection of 

605 # a geodesic line C{gl} and geodesic from C{(lat0, lon0)}. 

606 pl = _MODS.rhumb.bases._PseudoRhumbLine(gl) 

607 return pl.PlumbTo(lat0, lon0, exact=gl.geodesic, 

608 est=est, tol=tol) 

609 

610# **) MIT License 

611# 

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

613# 

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

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

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

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

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

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

620# 

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

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

623# 

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

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

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

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

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

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

630# OTHER DEALINGS IN THE SOFTWARE.