1

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

3

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

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

6 that package is installed.

7

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

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

10

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

12 L{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

16 from pygeodesy.basics import _copysign, _xinstanceof

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

18 from pygeodesy.datums import _earth_datum, _WGS84, _EWGS84

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

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

21 from pygeodesy.errors import _AssertionError, GeodesicError, \

22 IntersectionError

23 from pygeodesy.fsums import Fsum, Fmt, unstr

24 from pygeodesy.internals import typename, _under

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

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

27 Inverse10Tuple, _kWrapped

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

29 from pygeodesy.lazily import _ALL_LAZY, _ALL_MODS as _MODS

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

31 from pygeodesy.namedTuples import Destination3Tuple, Distance3Tuple

32 from 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

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

37

38 from contextlib import contextmanager

39# from math import fabs # from .utily

40

41__all__ = _ALL_LAZY.geodesicw

42__version__ = '25.04.14'

43

44_plumb_ = 'plumb'

45_TRIPS = 65

46

47

48 class _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 ArcDirect(self, lat1, lon1, azi1, a12, outmask=Caps.STANDARD): # PYCHOK no cover

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

101 '''

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

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

104 return GDict(d)

105

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

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

108 '''

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

110

111 Area = _Geodesic.Polygon # like GeodesicExact.Area

112

113 @property_RO

114 def datum(self):

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

116 '''

117 return self._datum

118

119 @Property

120 def debug(self):

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

122 '''

123 return bool(self._debug)

124

125 @debug.setter # PYCHOK setter!

126 def debug(self, debug):

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

128 details in L{GDict} results.

129 '''

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

131

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

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

134 '''

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

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

137 return GDict(d)

138

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

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

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

142 '''

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

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

145

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

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

148 '''

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

150

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

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

153 '''

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

155

156 @Property_RO

157 def ellipsoid(self):

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

159 '''

160 return self.datum.ellipsoid

161

162 @property_RO

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

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

165 '''

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

167

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

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

170 '''

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

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

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

174

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

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

177 '''

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

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

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

181

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

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

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

185 '''

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

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

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

189

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

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

192 '''

193 if wrap:

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

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

196

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

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

199 '''

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

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

202 return GDict(d)

203

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

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

206

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

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

209 '''

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

211 # and .HeightIDWkarney._distances

212 if wrap:

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

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

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

216 return fabs(r.a12)

217

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

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

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

221 '''

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

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

224

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

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

227 '''

228 if wrap:

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

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

231

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

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

234 '''

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

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

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

238

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

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

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

242 '''

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

244

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

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

247 to reference point 3.

248 '''

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

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

251 gl.a13, gl.s13 = t.a13, t.s13

252 return gl

253

254 @property_RO

255 def name(self):

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

257 '''

258 return self._name

259

260# Polygon = _Geodesic.Polygon

261

262 WGS84 = None # _EWGS84.geodesicw recusion

263

264 # Geodesic.ArcDirect.__doc__ = _Geodesic.ArcDirect.__doc__

265 # Geodesic.Direct.__doc__ = _Geodesic.Direct.__doc__

266 # Geodesic.Inverse.__doc__ = _Geodesic.Inverse.__doc__

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

268 # Geodesic.Line.__doc__ = _Geodesic.Line.__doc__

269 return Geodesic # overwrite property_ROver

270

271 @property_ROver # MCCABE 16

272 def GeodesicLine(self):

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

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

275 provided the latter is installed.

276 '''

277 _GeodesicLine = self.geographiclib.GeodesicLine

278

279 class GeodesicLine(_GeodesicLine):

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

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

282 '''

283 _geodesic = None

284 _name = NN

285

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

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

288

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

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

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

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

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

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

295 values specifying the capabilities the C{GeodesicLine} instance

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

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

298 '''

299 _xinstanceof(_wrapped.Geodesic, geodesic=geodesic)

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

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

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

303 if name:

304 self._name = name

305 self._geodesic = geodesic

306

307 @Property_RO

308 def a1(self):

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

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

311

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

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

314 '''

315 try:

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

317 except AttributeError:

318 return NAN # see .geodesicx.gxline._GeodesicLineExact

319

320 equatorarc = a1

321

322 def Arc(self):

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

324 '''

325 return self.a13

326

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

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

329

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

331 (C{degrees}).

332

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

334 '''

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

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

337 return GDict(d)

338

339 @Property_RO

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

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

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

343

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

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

346 '''

347 try:

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

349 except AttributeError:

350 return NAN # see .geodesicx.gxline._GeodesicLineExact

351

352 equatorazimuth = azi0

353

354 def Distance(self):

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

356 '''

357 return self.s13

358

359 @property_RO

360 def geodesic(self):

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

362 '''

363 return self._geodesic

364

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

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

367

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

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

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

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

372

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

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

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

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

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

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

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

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

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

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

383 intersections are the same L{GDict} instance.

384

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

386 intersect.

387

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

389 '''

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

391

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

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

394 with a geodesic from the given point.

395

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

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

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

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

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

401

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

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

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

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

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

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

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

409 geodesic and this line at the intersection point. The

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

411 See method L{Position} for further details.

412

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

414 '''

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

416

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

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

419

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

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

422 values specifying the quantities to be returned.

423

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

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

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

427 except when C{a12=NAN}.

428 '''

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

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

431 return GDict(d)

432

433 # GeodesicLine.ArcPosition.__doc__ = _GeodesicLine.ArcPosition.__doc__

434 # GeodesicLine.Position.__doc__ = _GeodesicLine.Position.__doc__

435 return GeodesicLine # overwrite property_ROver

436

437 @property_ROver

438 def Geodesic_WGS84(self):

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

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

441 is installed, otherwise an C{ImportError}.

442 '''

443 return _EWGS84.geodesicw # overwrite property_ROver

444

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

446

447

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

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

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

451 '''

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

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

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

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

456

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

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

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

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

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

462 '''

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

464 _MODS.geodesicw.Geodesic = g.__class__ # overwrite class

465 return g

466

467

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

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

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

471 '''

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

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

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

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

476

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

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

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

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

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

482 values specifying the capabilities the C{GeodesicLine} instance

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

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

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

486 '''

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

488 _MODS.geodesicw.GeodesicLine = gl.__class__ # overwrite class

489 return gl

490

491

492 def Geodesic_WGS84():

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

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

495 installed, otherwise an C{ImportError}.

496 '''

497 return _wrapped.Geodesic_WGS84

498

499

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

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

502 '''

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

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

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

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

507 '''

508 try:

509 yield args

510 except Exception as x:

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

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

513

514_wargs = _wargs() # PYCHOK singleton

515

516

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

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

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

520 r = Radius_(radius)

521 n = typename(_Intersecant2)[1:]

522 _P = gl.Position

523 _I = gl.geodesic.Inverse

524

525 def _R3(s):

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

527 P = _P(s)

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

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

530

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

532 _s = Fsum(c).fsumf_

533 for i in range(_TRIPS):

534 b = _s(s)

535 Rb, P, d = _R3(b)

536 if Rb > r:

537 break

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

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

540 # Rb > r > Rc

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

542 s = (b + c) * _0_5

543 R, P, d = _R3(s)

544 if Rb > R > r:

545 b, Rb = s, R

546 elif Rc < R < r:

547 c, Rc = s, R

548# else:

549# break

550 t = fabs(b - c)

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

552 break

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

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

555 i += C.iteration # combine iterations

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

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

558 return P, s

559

560 # get the perpendicular intersection of 2 geodesics,

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

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

563 try:

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

565 if a < r:

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

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

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

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

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

571 Q = P

572 elif a > r:

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

574 else: # tangential

575 P = Q = C

576 except Exception as x:

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

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

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

580

581 return P, Q

582

583

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

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

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

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

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

589 est=est, tol=tol)

590

591# **) MIT License

592#

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

594#

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

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

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

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

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

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

601#

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

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

604#

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

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

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

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

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

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

611# OTHER DEALINGS IN THE SOFTWARE.