Coverage for pygeodesy/geodesicw.py: 91%
233 statements
« prev ^ index » next coverage.py v7.6.1, created at 2025-05-29 12:40 -0400
« prev ^ index » next coverage.py v7.6.1, created at 2025-05-29 12:40 -0400
2# -*- coding: utf-8 -*-
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.
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.
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'''
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!
38from contextlib import contextmanager
39# from math import fabs # from .utily
41__all__ = _ALL_LAZY.geodesicw
42__version__ = '25.05.28'
44_plumb_ = 'plumb'
45_TRIPS = 65
48class _gWrapped(_kWrapped):
49 '''(INTERNAL) Wrapper for some of I{Karney}'s U{geographiclib
50 <https://PyPI.org/project/geographiclib>} classes.
51 '''
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))
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
83 def __init__(self, a_ellipsoid=_EWGS84, f=None, **name): # PYCHOK signature
84 '''New I{wrapped} C{geodesic.Geodesic} instance.
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)
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
104 class _PolygonArea(_AreaBase):
105 # def __init__(self, *earth_polyline):
106 # _PolygonArea.__init__(self, *earth_polyline)
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
117 A = _PolygonArea(self, polyline)
118 A.name = _name2__(name, _or_nameof=self)
119 return A
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)
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)
133 @property_RO
134 def datum(self):
135 '''Get this geodesic's datum (C{Datum}).
136 '''
137 return self._datum
139 @Property
140 def debug(self):
141 '''Get the C{debug} option (C{bool}).
142 '''
143 return bool(self._debug)
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
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)
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)
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)
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)
176 @Property_RO
177 def ellipsoid(self):
178 '''Get this geodesic's ellipsoid (C{Ellipsoid}).
179 '''
180 return self.datum.ellipsoid
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)
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
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
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)
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)
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)
224 def Inverse1(self, lat1, lon1, lat2, lon2, wrap=False):
225 '''Return the non-negative, I{angular} distance in C{degrees}.
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)
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))
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)
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)
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)
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
274 @property_RO
275 def name(self):
276 '''Get the name (C{str}).
277 '''
278 return self._name
280 Polygon = Area
281 WGS84 = None # _EWGS84.geodesicw recusion
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
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
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
305 def __init__(self, geodesic, lat1, lon1, azi1, **caps_name_): # salp1=NAN, calp1=NAN
306 '''New I{wrapped} C{geodesicline.GeodesicLine} instance.
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
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)}.
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
339 equatorarc = a1
341 def Arc(self):
342 '''Return the angular distance to point 3 (C{degrees} or C{NAN}).
343 '''
344 return self.a13
346 def ArcPosition(self, a12, outmask=Caps.STANDARD):
347 '''Return the position at C{B{a12} degrees} on this line.
349 @arg a12: Angular distance from this line's first point
350 (C{degrees}).
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)
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.
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
371 equatorazimuth = azi0
373 def Distance(self):
374 '''Return the distance to reference point 3 (C{meter} or C{NAN}).
375 '''
376 return self.s13
378 @property_RO
379 def geodesic(self):
380 '''Get the I{wrapped} geodesic (L{Geodesic}).
381 '''
382 return self._geodesic
384 def Intersecant2(self, lat0, lon0, radius, tol=_TOL):
385 '''Compute the intersection(s) of this geodesic line and a circle.
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}).
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.
404 @raise IntersectionError: The circle and this geodesic line do not
405 intersect.
407 @raise UnitError: Invalid B{C{radius}}.
408 '''
409 return _Intersecant2(self, lat0, lon0, radius, tol=tol)
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.
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)).
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.
432 @see: Methods C{Intersecant2}, C{Intersection} and C{Position}.
433 '''
434 return _PlumbTo(self, lat0, lon0, est=est, tol=tol)
436 def Position(self, s12, outmask=Caps.STANDARD):
437 '''Return the position at distance C{B{s12} meter} on this line.
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.
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)
452 # GeodesicLine.ArcPosition.__doc__ = _GeodesicLine.ArcPosition.__doc__
453 # GeodesicLine.Position.__doc__ = _GeodesicLine.Position.__doc__
454 return GeodesicLine # overwrite property_ROver
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
464_wrapped = _gWrapped() # PYCHOK singleton, .ellipsoids, .test/base.py
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}.
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
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}.
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
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
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)
533_wargs = _wargs() # PYCHOK singleton
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
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
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
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)
600 return P, Q
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)
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.