Coverage for pygeodesy/geodesicx/gx.py: 93%

1

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

3

4 u'''A pure Python version of I{Karney}'s C++ class U{GeodesicExact

5<https://GeographicLib.SourceForge.io/C++/doc/classGeographicLib_1_1GeodesicExact.html>}.

6

7 Class L{GeodesicExact} follows the naming, methods and return values

8 of class C{Geodesic} from I{Karney}'s Python U{geographiclib

9<https://GitHub.com/geographiclib/geographiclib-python>}.

10

11 Copyright (C) U{Charles Karney<mailto:Karney@Alum.MIT.edu>} (2008-2024)

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{GeodesicExact.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 _copysign, _xinstanceof, _xor, unsigned0, typename

40 from pygeodesy.constants import EPS, EPS0, EPS02, MANT_DIG, NAN, PI, _EPSqrt, \

41 _SQRT2_2, isnan, _0_0, _0_001, _0_01, _0_1, _0_5, \

42 _1_0, _N_1_0, _1_75, _2_0, _N_2_0, _2__PI, _3_0, \

43 _4_0, _6_0, _8_0, _16_0, _90_0, _180_0, _1000_0

44 from pygeodesy.datums import _earth_datum, _WGS84, _EWGS84

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

46 from pygeodesy.errors import GeodesicError, _xkwds_pop2

47 from pygeodesy.fmath import fdot_, hypot, hypot2, Fmt

48 from pygeodesy.fsums import fsumf_, fsum1f_

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

50 _sincos12, _sin1cos2, _sinf1cos2d, \

51 _TINY, _toNAN, _xnC4

52 from pygeodesy.geodesicx.gxline import _GeodesicLineExact, _update_glXs

53# from pygeodesy.internals import typename # from .basics

54 from pygeodesy.interns import NN, _DOT_, _UNDER_

55 from pygeodesy.karney import GDict, _around, _atan2d, Caps, _cbrt, _diff182, \

56 _fix90, _K_2_0, _norm2, _norm180, _polynomial, \

57 _signBit, _sincos2, _sincos2d, _sincos2de, _unsigned2

58 from pygeodesy.lazily import _ALL_DOCS, _ALL_MODS as _MODS

59 from pygeodesy.namedTuples import Destination3Tuple, Distance3Tuple

60 from pygeodesy.props import deprecated_Property, Property, Property_RO, property_RO

61# from pygeodesy.streprs import Fmt # from .fmath

62 from pygeodesy.utily import atan2, atan2d as _atan2d_reverse, _unrollon, \

63 _Wrap, wrap360

64

65 from math import copysign, cos, degrees, fabs, radians, sqrt

66

67 __all__ = ()

68 __version__ = '25.09.16'

69

70 _MAXIT1 = 20

71 _MAXIT2 = 10 + _MAXIT1 + MANT_DIG # MANT_DIG == C++ digits

72

73# increased multiplier in defn of _TOL1 from 100 to 200 to fix Inverse

74# case 52.784459512564 0 -52.784459512563990912 179.634407464943777557

75# which otherwise failed for Visual Studio 10 (Release and Debug)

76 _TOL0 = EPS

77 _TOL1 = _TOL0 * -200 # negative

78 _TOL2 = _EPSqrt # == sqrt(_TOL0)

79 _TOL3 = _TOL2 * _0_1

80 _TOLb = _TOL2 * _TOL0 # Check on bisection interval

81 _THR1 = _TOL2 * _1000_0 + _1_0

82

83 _TINY3 = _TINY * _3_0

84 _TOL08 = _TOL0 * _8_0

85 _TOL016 = _TOL0 * _16_0

86

87

88 def _atan12(*sincos12, **sineg0):

89 '''(INTERNAL) Return C{ang12} in C{radians}.

90 '''

91 return atan2(*_sincos12(*sincos12, **sineg0))

92

93

94 def _eTOL2(f):

95 # Using the auxiliary sphere solution with dnm computed at

96 # (bet1 + bet2) / 2, the relative error in the azimuth

97 # consistency check is sig12^2 * abs(f) * min(1, 1-f/2) / 2.

98 # (Error measured for 1/100 < b/a < 100 and abs(f) >= 1/1000.

99 # For a given f and sig12, the max error occurs for lines

100 # near the pole. If the old rule for computing dnm = (dn1 +

101 # dn2)/2 is used, then the error increases by a factor of 2.)

102 # Setting this equal to epsilon gives sig12 = eTOL2. Here,

103 # safety factor 0.1 (error decreased by 100) and max(0.001, abs(f))

104 # stops eTOL2 getting too large in the nearly spherical case.

105 t = min(_1_0, _1_0 - f * _0_5) * max(_0_001, fabs(f)) * _0_5

106 return _TOL3 / (sqrt(t) if t > EPS02 else EPS0)

107

108

109 class _PDict(GDict):

110 '''(INTERNAL) Parameters passed around in C{._GDictInverse} and

111 optionally returned when C{GeodesicExact.debug} is C{True}.

112 '''

113 def set_sigs(self, ssig1, csig1, ssig2, csig2):

114 '''Update the C{sig1} and C{sig2} parameters.

115 '''

116 self.set_(ssig1=ssig1, csig1=csig1, sncndn1=(ssig1, csig1, self.dn1), # PYCHOK dn1

117 ssig2=ssig2, csig2=csig2, sncndn2=(ssig2, csig2, self.dn2)) # PYCHOK dn2

118

119 def toGDict(self): # PYCHOK no cover

120 '''Return as C{GDict} without attrs C{sncndn1} and C{sncndn2}.

121 '''

122 def _rest(sncndn1=None, sncndn2=None, **rest): # PYCHOK sncndn* not used

123 return GDict(rest)

124

125 return _rest(**self)

126

127

128 class GeodesicExact(_GeodesicBase):

129 '''A pure Python version of I{Karney}'s C++ class U{GeodesicExact

130 <https://GeographicLib.SourceForge.io/C++/doc/classGeographicLib_1_1GeodesicExact.html>},

131 modeled after I{Karney}'s Python class U{geodesic.Geodesic<https://GitHub.com/

132 geographiclib/geographiclib-python>}.

133 '''

134 _datum = _WGS84

135 _nC4 = 30 # default C4order

136

137 def __init__(self, a_ellipsoid=_EWGS84, f=None, caps=None, C4order=None, **name_C4Order): # for backward compatibility

138 '''New L{GeodesicExact} instance.

139

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

141 the equatorial radius of the ellipsoid (C{scalar},

142 conventionally in C{meter}), see B{C{f}}.

143 @arg f: The flattening of the ellipsoid (C{scalar}) if B{C{a_ellipsoid}}

144 is specified as C{scalar}.

145 @kwarg caps: Optional default capabilities for L{GeodesicLineExact} instances,

146 use C{B{caps}=Caps.NONFINITONAN} to handle nonfinites silently.

147 @kwarg C4order: Optional series expansion order (C{int}), see property

148 L{C4order}, default C{30}.

149 @kwarg name_C4Order: Optional C{B{name}=NN} (C{str}) and the DEPRECATED

150 keyword argument C{C4Order}, use B{C{C4order}} instead.

151

152 @raise GeodesicError: Invalid B{C{C4order}}.

153 '''

154 if name_C4Order:

155 C4order, name = _xkwds_pop2(name_C4Order, C4Order=C4order)

156 if name:

157 self.name = name

158 else:

159 name = name_C4Order # no name

160

161 _earth_datum(self, a_ellipsoid, f=f, **name)

162 if caps:

163 self._caps |= caps & Caps._OUT_MASK

164 if C4order: # XXX private copy, always?

165 self.C4order = C4order

166

167 @Property_RO

168 def a(self):

169 '''Get the I{equatorial} radius, semi-axis (C{meter}).

170 '''

171 return self.ellipsoid.a

172

173 def ArcDirect(self, lat1, lon1, azi1, a12, outmask=Caps.STANDARD):

174 '''Solve the I{Direct} geodesic problem in terms of (spherical) arc length.

175

176 @arg lat1: Latitude of the first point (C{degrees}).

177 @arg lon1: Longitude of the first point (C{degrees}).

178 @arg azi1: Azimuth at the first point (compass C{degrees}).

179 @arg a12: Arc length between the points (C{degrees}), can be negative.

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

181 values specifying the quantities to be returned.

182

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

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

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

186

187 @see: C++ U{GeodesicExact.ArcDirect

188 <https://GeographicLib.SourceForge.io/C++/doc/classGeographicLib_1_1GeodesicExact.html>}

189 and Python U{Geodesic.ArcDirect<https://GeographicLib.SourceForge.io/Python/doc/code.html>}.

190 '''

191 return self._GDictDirect(lat1, lon1, azi1, True, a12, outmask=outmask)

192

193 def ArcDirectLine(self, lat1, lon1, azi1, a12, caps=Caps.ALL, **name):

194 '''Define a L{GeodesicLineExact} in terms of the I{direct} geodesic problem and as arc length.

195

196 @arg lat1: Latitude of the first point (C{degrees}).

197 @arg lon1: Longitude of the first point (C{degrees}).

198 @arg azi1: Azimuth at the first point (compass C{degrees}).

199 @arg a12: Arc length between the points (C{degrees}), can be negative.

200 @kwarg caps: Desired capabilities for the L{GeodesicLineExact} instance.

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

202

203 @return: A L{GeodesicLineExact} instance.

204

205 @note: The third point of the L{GeodesicLineExact} is set to correspond

206 to the second point of the I{Inverse} geodesic problem.

207

208 @note: Latitude B{C{lat1}} should in the range C{[-90, +90]}.

209

210 @see: C++ U{GeodesicExact.ArcDirectLine

211 <https://GeographicLib.SourceForge.io/C++/doc/classGeographicLib_1_1GeodesicExact.html>} and

212 Python U{Geodesic.ArcDirectLine<https://GeographicLib.SourceForge.io/Python/doc/code.html>}.

213 '''

214 return GeodesicLineExact(self, lat1, lon1, azi1, caps=caps, **name)._GenSet(self._debug, a12=a12)

215

216 def Area(self, polyline=False, **name):

217 '''Set up a L{GeodesicAreaExact} to compute area and perimeter of a polygon.

218

219 @kwarg polyline: If C{True}, compute the perimeter only, otherwise the

220 perimeter and area (C{bool}).

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

222

223 @return: A L{GeodesicAreaExact} instance.

224

225 @note: The B{C{debug}} setting is passed as C{verbose}

226 to the returned L{GeodesicAreaExact} instance.

227 '''

228 gaX = _MODS.geodesicx.GeodesicAreaExact(self, polyline=polyline,

229 name=self._name__(name))

230 if self.debug:

231 gaX.verbose = True

232 return gaX

233

234 @Property_RO

235 def b(self):

236 '''Get the ellipsoid's I{polar} radius, semi-axis (C{meter}).

237 '''

238 return self.ellipsoid.b

239

240 @Property_RO

241 def c2x(self):

242 '''Get the ellipsoid's I{authalic} earth radius I{squared} (C{meter} I{squared}).

243 '''

244 # The Geodesic class substitutes atanh(sqrt(e2)) for asinh(sqrt(ep2))

245 # in the definition of _c2. The latter is more accurate for very

246 # oblate ellipsoids (which the Geodesic class does not handle). Of

247 # course, the area calculation in GeodesicExact is still based on a

248 # series and only holds for moderately oblate (or prolate) ellipsoids.

249 return self.ellipsoid.c2x

250

251 c2 = c2x # in this particular case

252

253 def C4f(self, eps):

254 '''Evaluate the C{C4x} coefficients for B{C{eps}}.

255

256 @arg eps: Polynomial factor (C{float}).

257

258 @return: C{C4order}-Tuple of C{C4x(B{eps})} coefficients.

259 '''

260 def _c4(nC4, C4x):

261 i, x, e = 0, _1_0, eps

262 _p = _polynomial

263 for r in range(nC4, 0, -1):

264 j = i + r

265 yield _p(e, C4x, i, j) * x

266 x *= e

267 i = j

268 # assert i == (nC4 * (nC4 + 1)) // 2

269

270 return tuple(_c4(self._nC4, self._C4x))

271

272 def _C4f_k2(self, k2): # in ._GDictInverse and gxline._GeodesicLineExact._C4a

273 '''(INTERNAL) Compute C{eps} from B{C{k2}} and invoke C{C4f}.

274 '''

275 return self.C4f(k2 / fsumf_(_2_0, sqrt(k2 + _1_0) * _2_0, k2))

276

277 @Property

278 def C4order(self):

279 '''Get the series expansion order (C{int}, 24, 27 or 30).

280 '''

281 return self._nC4

282

283 @C4order.setter # PYCHOK .setter!

284 def C4order(self, order):

285 '''Set the series expansion order (C{int}, 24, 27 or 30).

286

287 @raise GeodesicError: Invalid B{C{order}}.

288 '''

289 _xnC4(C4order=order)

290 if self._nC4 != order:

291 GeodesicExact._C4x._update(self)

292 _update_glXs(self) # zap cached _GeodesicLineExact attrs _B41, _C4a

293 self._nC4 = order

294

295 @deprecated_Property

296 def C4Order(self):

297 '''DEPRECATED, use property C{C4order}.

298 '''

299 return self.C4order

300

301 @C4Order.setter # PYCHOK .setter!

302 def C4Order(self, order):

303 '''DEPRECATED, use property C{C4order}.

304 '''

305 _xnC4(C4Order=order)

306 self.C4order = order

307

308 @Property_RO

309 def _C4x(self):

310 '''Get this ellipsoid's C{C4} coefficients, I{cached} tuple.

311

312 @see: Property L{C4order}.

313 '''

314 # see C4coeff() in GeographicLib.src.GeodesicExactC4.cpp

315 def _C4(nC4):

316 i, n, cs = 0, self.n, _C4coeffs(nC4)

317 _p = _polynomial

318 for r in range(nC4 + 1, 1, -1): # _reverange

319 for j in range(1, r):

320 j = j + i # (j - i - 1) order of polynomial

321 yield _p(n, cs, i, j) / cs[j]

322 i = j + 1

323 # assert i == (nC4 * (nC4 + 1) * (nC4 + 5)) // 6

324

325 return tuple(_C4(self._nC4)) # 3rd flattening

326

327 @property_RO

328 def datum(self):

329 '''Get the datum (C{Datum}).

330 '''

331 return self._datum

332

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

334 '''Solve the I{Direct} geodesic problem

335

336 @arg lat1: Latitude of the first point (C{degrees}).

337 @arg lon1: Longitude of the first point (C{degrees}).

338 @arg azi1: Azimuth at the first point (compass C{degrees}).

339 @arg s12: Distance between the points (C{meter}), can be negative.

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

341 values specifying the quantities to be returned.

342

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

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

345 C{lon1}, C{azi1} and distance C{s12} always included.

346

347 @see: C++ U{GeodesicExact.Direct

348 <https://GeographicLib.SourceForge.io/C++/doc/classGeographicLib_1_1GeodesicExact.html>}

349 and Python U{Geodesic.Direct<https://GeographicLib.SourceForge.io/Python/doc/code.html>}.

350 '''

351 return self._GDictDirect(lat1, lon1, azi1, False, s12, outmask=outmask)

352

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

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

355 (final bearing) in C{degrees}.

356

357 @return: L{Destination3Tuple}C{(lat, lon, final)}.

358 '''

359 r = self._GDictDirect(lat1, lon1, azi1, False, s12, outmask=Caps._AZIMUTH_LATITUDE_LONGITUDE)

360 return Destination3Tuple(r.lat2, r.lon2, r.azi2) # no iteration

361

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

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

364 '''

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

366

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

368 '''Define a L{GeodesicLineExact} in terms of the I{direct} geodesic problem and as distance.

369

370 @arg lat1: Latitude of the first point (C{degrees}).

371 @arg lon1: Longitude of the first point (C{degrees}).

372 @arg azi1: Azimuth at the first point (compass C{degrees}).

373 @arg s12: Distance between the points (C{meter}), can be negative.

374 @kwarg caps: Desired capabilities for the L{GeodesicLineExact} instance.

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

376

377 @return: A L{GeodesicLineExact} instance.

378

379 @note: The third point of the L{GeodesicLineExact} is set to correspond

380 to the second point of the I{Inverse} geodesic problem.

381

382 @note: Latitude B{C{lat1}} should in the range C{[-90, +90]}.

383

384 @see: C++ U{GeodesicExact.DirectLine

385 <https://GeographicLib.SourceForge.io/C++/doc/classGeographicLib_1_1GeodesicExact.html>} and

386 Python U{Geodesic.DirectLine<https://GeographicLib.SourceForge.io/Python/doc/code.html>}.

387 '''

388 return GeodesicLineExact(self, lat1, lon1, azi1, caps=caps, **name)._GenSet(self._debug, s12=s12)

389

390 def _dn(self, sbet, cbet): # in gxline._GeodesicLineExact.__init__

391 '''(INTERNAL) Helper.

392 '''

393 if self.f < 0: # PYCHOK no cover

394 dn = sqrt(_1_0 - cbet**2 * self.e2) / self.f1

395 else:

396 dn = sqrt(_1_0 + sbet**2 * self.ep2)

397 return dn

398

399 @Property_RO

400 def e2(self):

401 '''Get the ellipsoid's I{(1st) eccentricity squared} (C{float}), M{f * (2 - f)}.

402 '''

403 return self.ellipsoid.e2

404

405 @Property_RO

406 def _e2a2(self):

407 '''(INTERNAL) Cache M{E.e2 * E.a2}.

408 '''

409 return self.e2 * self.ellipsoid.a2

410

411 @Property_RO

412 def _e2_f1(self):

413 '''(INTERNAL) Cache M{E.e2 * E.f1}.

414 '''

415 return self.e2 / self.f1

416

417 @Property_RO

418 def _eF(self):

419 '''(INTERNAL) Get the elliptic function, aka C{.E}.

420 '''

421 return _MODS.elliptic.Elliptic(k2=-self.ep2)

422

423 def _eF_reset_cHe2_f1(self, x, y):

424 '''(INTERNAL) Reset elliptic function and return M{cH * e2 / f1 * ...}.

425 '''

426 if isnan(x):

427 y = NAN

428 else:

429 self._eF_reset_k2(x)

430 y *= self._eF.cH * self._e2_f1

431 return y

432

433 def _eF_reset_k2(self, x):

434 '''(INTERNAL) Reset elliptic function and return C{k2}.

435 '''

436 ep2 = self.ep2

437 x *= x * ep2 # see .gxline._GeodesicLineExact._eF

438 self._eF.reset(k2=-x, alpha2=-ep2) # kp2, alphap2 defaults

439 _update_glXs(self) # zap cached/memoized _GeodesicLineExact attrs

440 return x

441

442 @Property_RO

443 def ellipsoid(self):

444 '''Get the ellipsoid (C{Ellipsoid}).

445 '''

446 return self.datum.ellipsoid

447

448 @Property_RO

449 def ep2(self):

450 '''Get the ellipsoid's I{2nd eccentricity squared} (C{float}), M{e2 / (1 - e2)}.

451 '''

452 return self.ellipsoid.e22 # == self.e2 / self.f1**2

453

454 e22 = ep2 # for ellipsoid compatibility

455

456 @Property_RO

457 def _eTOL2(self):

458 '''(INTERNAL) The si12 threshold for "really short".

459 '''

460 return _eTOL2(self.f)

461

462 @Property_RO

463 def flattening(self):

464 '''Get the C{ellipsoid}'s I{flattening} (C{scalar}), M{(a - b) / a}, C{0} for spherical, negative for prolate.

465 '''

466 return self.ellipsoid.f

467

468 f = flattening

469

470 @Property_RO

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

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

473 '''

474 return self.ellipsoid.f1

475

476 @Property_RO

477 def _f180(self):

478 '''(INTERNAL) Cached/memoized.

479 '''

480 return self.f * _180_0

481

482 def _GDictDirect(self, lat1, lon1, azi1, arcmode, s12_a12, outmask=Caps.STANDARD):

483 '''(INTERNAL) As C{_GenDirect}, but returning a L{GDict}.

484

485 @return: A L{GDict} ...

486 '''

487 C = outmask if arcmode else (outmask | Caps.DISTANCE_IN)

488 glX = self.Line(lat1, lon1, azi1, caps=C | Caps.LINE_OFF)

489 return glX._GDictPosition(arcmode, s12_a12, outmask)

490

491 def _GDictInverse(self, lat1, lon1, lat2, lon2, outmask=Caps.STANDARD): # MCCABE 33, 41 vars

492 '''(INTERNAL) As C{_GenInverse}, but returning a L{GDict}.

493

494 @return: A L{GDict} ...

495 '''

496 r, Cs = GDict(), Caps

497 if self._debug: # PYCHOK no cover

498 outmask |= Cs._DEBUG_INVERSE & self._debug

499 outmask |= self.caps

500 outmask &= Cs._OUT_MASK # incl. _SALP_CALPs_ and _DEBUG_

501 if _toNAN(outmask, lat1, lon1, lat2, lon2):

502 return r._toNAN(outmask, lat1=lat1, lon1=lon1, lat2=lat2, lon2=lon2)

503

504 # compute longitude difference carefully (with _diff182):

505 # result is in [-180, +180] but -180 is only for west-going

506 # geodesics, +180 is for east-going and meridional geodesics

507 lon12, lon12s = _diff182(lon1, lon2)

508 # see C{result} from geographiclib.geodesic.Inverse

509 if (outmask & Cs.LONG_UNROLL): # == (lon1 + lon12) + lon12s

510 r.set_(lon1=lon1, lon2=fsumf_(lon1, lon12, lon12s))

511 elif (outmask & Cs.LONGITUDE):

512 r.set_(lon1=_norm180(lon1), lon2=_norm180(lon2))

513 if _K_2_0: # GeographicLib 2.0

514 # make longitude difference positive

515 lon12, lon_ = _unsigned2(lon12)

516 if lon_:

517 lon12s = -lon12s

518 lam12 = radians(lon12)

519 # calculate sincosd(_around(lon12 + correction))

520 slam12, clam12 = _sincos2de(lon12, lon12s)

521 # supplementary longitude difference

522 lon12s = fsumf_(_180_0, -lon12, -lon12s)

523 else: # GeographicLib 1.52

524 # make longitude difference positive and if very close

525 # to being on the same half-meridian, then make it so.

526 if lon12 < 0: # _signBit(lon12)

527 lon_, lon12 = True, -_around(lon12)

528 lon12s = _around(fsumf_(_180_0, -lon12, lon12s))

529 else:

530 lon_, lon12 = False, _around(lon12)

531 lon12s = _around(fsumf_(_180_0, -lon12, -lon12s))

532 lam12 = radians(lon12)

533 if lon12 > _90_0:

534 slam12, clam12 = _sincos2d(lon12s)

535 clam12 = -clam12

536 else:

537 slam12, clam12 = _sincos2(lam12)

538 # If really close to the equator, treat as on equator.

539 lat1 = _around(_fix90(lat1))

540 lat2 = _around(_fix90(lat2))

541 if (outmask & Cs.LATITUDE):

542 r.set_(lat1=lat1, lat2=lat2)

543 # Swap points so that point with higher (abs) latitude is

544 # point 1. If one latitude is a NAN, then it becomes lat1.

545 swap_ = isnan(lat2) or fabs(lat1) < fabs(lat2)

546 if swap_:

547 lat1, lat2 = lat2, lat1

548 lon_ = not lon_

549 if _signBit(lat1):

550 lat_ = False # note, False

551 else: # make lat1 <= -0

552 lat_ = True # note, True

553 lat1, lat2 = -lat1, -lat2

554 # Now 0 <= lon12 <= 180, -90 <= lat1 <= -0 and lat1 <= lat2 <= -lat1

555 # and lat_, lon_, swap_ register the transformation to bring the

556 # coordinates to this canonical form, where False means no change

557 # made. We make these transformations so that there are few cases

558 # to check, e.g., on verifying quadrants in atan2. In addition,

559 # this enforces some symmetries in the results returned.

560

561 # Initialize for the meridian. No longitude calculation is done in

562 # this case to let the parameter default to 0.

563 sbet1, cbet1 = _sinf1cos2d(lat1, self.f1)

564 sbet2, cbet2 = _sinf1cos2d(lat2, self.f1)

565 # If cbet1 < -sbet1, then cbet2 - cbet1 is a sensitive measure

566 # of the |bet1| - |bet2|. Alternatively (cbet1 >= -sbet1),

567 # abs(sbet2) + sbet1 is a better measure. This logic is used

568 # in assigning calp2 in _Lambda6. Sometimes these quantities

569 # vanish and in that case we force bet2 = +/- bet1 exactly. An

570 # example where is is necessary is the inverse problem

571 # 48.522876735459 0 -48.52287673545898293 179.599720456223079643

572 # which failed with Visual Studio 10 (Release and Debug)

573 if cbet1 < -sbet1:

574 if cbet2 == cbet1:

575 sbet2 = copysign(sbet1, sbet2)

576 elif fabs(sbet2) == -sbet1:

577 cbet2 = cbet1

578

579 p = _PDict(sbet1=sbet1, cbet1=cbet1, dn1=self._dn(sbet1, cbet1),

580 sbet2=sbet2, cbet2=cbet2, dn2=self._dn(sbet2, cbet2))

581

582 _meridian = _b = True # i.e. meridian = b = False

583 if lat1 == -90 or slam12 == 0:

584 # Endpoints are on a single full meridian,

585 # so the geodesic might lie on a meridian.

586 salp1, calp1 = slam12, clam12 # head to target lon

587 salp2, calp2 = _0_0, _1_0 # then head north

588 # tan(bet) = tan(sig) * cos(alp)

589 p.set_sigs(sbet1, calp1 * cbet1, sbet2, calp2 * cbet2)

590 # sig12 = sig2 - sig1

591 sig12 = _atan12(sbet1, p.csig1, sbet2, p.csig2, sineg0=True) # PYCHOK csig*

592 s12x, m12x, _, \

593 M12, M21 = self._Length5(sig12, outmask | Cs.REDUCEDLENGTH, p)

594 # Add the check for sig12 since zero length geodesics

595 # might yield m12 < 0. Test case was

596 # echo 20.001 0 20.001 0 | GeodSolve -i

597 # In fact, we will have sig12 > PI/2 for meridional

598 # geodesic which is not a shortest path.

599 if sig12 < _TOL2 or m12x >= 0: # GeographicLib 2.5.1

600 # Need at least 2 to handle 90 0 90 180

601 # Prevent negative s12 or m12 from geographiclib 1.52

602 if sig12 < _TINY3 or (sig12 < _TOL0 and (s12x < 0 or m12x < 0)):

603 sig12 = m12x = s12x = _0_0

604# else:

605# m12x *= self.b

606# s12x *= self.b

607 _meridian = _b = False # i.e. meridian = b = True

608 C = 1

609# else: # m12 < 0, prolate and too close to anti-podal

610# _meridian = True # i.e. meridian = False

611 a12 = _0_0 # if _b else degrees(sig12)

612

613 if _meridian:

614 _b = sbet1 == 0 and (self.f <= 0 or lon12s >= self._f180) # and sbet2 == 0

615 if _b: # Geodesic runs along equator

616 calp1 = calp2 = _0_0

617 salp1 = salp2 = _1_0

618 sig12 = lam12 / self.f1 # == omg12

619 somg12, comg12 = _sincos2(sig12)

620 m12x = self.b * somg12

621 s12x = self.a * lam12

622 M12 = M21 = comg12

623 a12 = lon12 / self.f1

624 C = 2

625 else:

626 # Now point1 and point2 belong within a hemisphere bounded by a

627 # meridian and geodesic is neither meridional or equatorial.

628 p.set_(slam12=slam12, clam12=clam12)

629 # Figure a starting point for Newton's method

630 sig12, salp1, calp1, \

631 salp2, calp2, dnm = self._InverseStart6(lam12, p)

632 if sig12 is None: # use Newton's method

633 # pre-compute the constant _Lambda6 term, once

634 p.set_(bet12=None if cbet2 == cbet1 and fabs(sbet2) == -sbet1 else

635 (((cbet1 + cbet2) * (cbet2 - cbet1)) if cbet1 < -sbet1 else

636 ((sbet1 + sbet2) * (sbet1 - sbet2))))

637 sig12, salp1, calp1, \

638 salp2, calp2, domg12 = self._Newton6(salp1, calp1, p)

639 s12x, m12x, _, M12, M21 = self._Length5(sig12, outmask, p)

640 if (outmask & Cs.AREA):

641 # omg12 = lam12 - domg12

642 s, c = _sincos2(domg12)

643 somg12, comg12 = _sincos12(s, c, slam12, clam12)

644 C = 3 # Newton

645 else: # from _InverseStart6: dnm, salp*, calp*

646 C = 4 # Short lines

647 s, c = _sincos2(sig12 / dnm)

648 m12x = dnm**2 * s

649 s12x = dnm * sig12

650 M12 = M21 = c

651 if (outmask & Cs.AREA):

652 somg12, comg12 = _sincos2(lam12 / (self.f1 * dnm))

653

654 else: # _meridian is False, i.e. meridian is True

655 somg12 = comg12 = NAN

656

657 r.set_(a12=a12 if _b else degrees(sig12)) # in [0, 180]

658

659 if (outmask & Cs.DISTANCE):

660 r.set_(s12=unsigned0(s12x if _b else (self.b * s12x)))

661

662 if (outmask & Cs.REDUCEDLENGTH):

663 r.set_(m12=unsigned0(m12x if _b else (self.b * m12x)))

664

665 if (outmask & Cs.GEODESICSCALE):

666 if swap_:

667 M12, M21 = M21, M12

668 r.set_(M12=unsigned0(M12),

669 M21=unsigned0(M21))

670

671 if (outmask & Cs.AREA):

672 S12 = self._InverseArea(_meridian, salp1, calp1,

673 salp2, calp2,

674 somg12, comg12, p)

675 if _xor(swap_, lat_, lon_):

676 S12 = -S12

677 r.set_(S12=unsigned0(S12))

678

679 if (outmask & (Cs.AZIMUTH | Cs._SALP_CALPs_)):

680 if swap_:

681 salp1, salp2 = salp2, salp1

682 calp1, calp2 = calp2, calp1

683 if _xor(swap_, lon_):

684 salp1, salp2 = -salp1, -salp2

685 if _xor(swap_, lat_):

686 calp1, calp2 = -calp1, -calp2

687

688 if (outmask & Cs.AZIMUTH):

689 r.set_(azi1=_atan2d(salp1, calp1),

690 azi2=_atan2d_reverse(salp2, calp2, reverse=outmask & Cs.REVERSE2))

691 if (outmask & Cs._SALP_CALPs_):

692 r.set_(salp1=salp1, calp1=calp1,

693 salp2=salp2, calp2=calp2)

694

695 if (outmask & Cs._DEBUG_INVERSE): # PYCHOK no cover

696 E, eF = self.ellipsoid, self._eF

697 p.set_(C=C, a=self.a, f=self.f, f1=self.f1,

698 e=E.e, e2=self.e2, ep2=self.ep2,

699 c2=E.c2, c2x=self.c2x,

700 eFcD=eF.cD, eFcE=eF.cE, eFcH=eF.cH,

701 eFk2=eF.k2, eFa2=eF.alpha2)

702 p.update(r) # r overrides p

703 r = p.toGDict()

704

705 return self._iter2tion(r, **p)

706

707 def _GenDirect(self, lat1, lon1, azi1, arcmode, s12_a12, outmask=Caps.STANDARD):

708 '''(INTERNAL) The general I{Inverse} geodesic calculation.

709

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

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

712 '''

713 r = self._GDictDirect(lat1, lon1, azi1, arcmode, s12_a12, outmask=outmask)

714 return r.toDirect9Tuple()

715

716 def _GenInverse(self, lat1, lon1, lat2, lon2, outmask=Caps.STANDARD):

717 '''(INTERNAL) The general I{Inverse} geodesic calculation.

718

719 @return: L{Inverse10Tuple}C{(a12, s12, salp1, calp1, salp2, calp2,

720 m12, M12, M21, S12)}.

721 '''

722 r = self._GDictInverse(lat1, lon1, lat2, lon2, outmask=outmask | Caps._SALP_CALPs_)

723 return r.toInverse10Tuple()

724

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

726 '''(INTERNAL) Short-cut version, see .base.ellipsoidalDI.intersecant2.

727 '''

728 if wrap:

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

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

731

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

733 '''Perform the I{Inverse} geodesic calculation.

734

735 @arg lat1: Latitude of the first point (C{degrees}).

736 @arg lon1: Longitude of the first point (C{degrees}).

737 @arg lat2: Latitude of the second point (C{degrees}).

738 @arg lon2: Longitude of the second point (C{degrees}).

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

740 values specifying the quantities to be returned.

741

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

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

744 C{lon1}, C{azi1} and distance C{s12} always included.

745

746 @note: The third point of the L{GeodesicLineExact} is set to correspond

747 to the second point of the I{Inverse} geodesic problem.

748

749 @note: Both B{C{lat1}} and B{C{lat2}} should in the range C{[-90, +90]}.

750

751 @see: C++ U{GeodesicExact.InverseLine

752 <https://GeographicLib.SourceForge.io/C++/doc/classGeographicLib_1_1GeodesicExact.html>} and

753 Python U{Geodesic.InverseLine<https://GeographicLib.SourceForge.io/Python/doc/code.html>}.

754 '''

755 return self._GDictInverse(lat1, lon1, lat2, lon2, outmask=outmask)

756

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

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

759

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

761 B{C{lat2}} and B{C{lon2}} (C{bool}).

762 '''

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

764 # and .HeightIDWkarney._distances

765 if wrap:

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

767 r = self._GDictInverse(lat1, lon1, lat2, lon2, outmask=Caps.EMPTY)

768 return fabs(r.a12) # a12 always

769

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

771 '''Return the distance in C{meter} and the forward and

772 reverse azimuths (initial and final bearing) in C{degrees}.

773

774 @return: L{Distance3Tuple}C{(distance, initial, final)}.

775 '''

776 r = self._GDictInverse(lat1, lon1, lat2, lon2, outmask=Caps.AZIMUTH_DISTANCE)

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

778 iteration=r.iteration)

779

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

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

782 '''

783 if wrap:

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

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

786

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

788 '''Define a L{GeodesicLineExact} in terms of the I{Inverse} geodesic problem.

789

790 @arg lat1: Latitude of the first point (C{degrees}).

791 @arg lon1: Longitude of the first point (C{degrees}).

792 @arg lat2: Latitude of the second point (C{degrees}).

793 @arg lon2: Longitude of the second point (C{degrees}).

794 @kwarg caps: Desired capabilities for the L{GeodesicLineExact} instance.

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

796

797 @return: A L{GeodesicLineExact} instance.

798

799 @note: The third point of the L{GeodesicLineExact} is set to correspond

800 to the second point of the I{Inverse} geodesic problem.

801

802 @note: Both B{C{lat1}} and B{C{lat2}} should in the range C{[-90, +90]}.

803

804 @see: C++ U{GeodesicExact.InverseLine

805 <https://GeographicLib.SourceForge.io/C++/doc/classGeographicLib_1_1GeodesicExact.html>} and

806 Python U{Geodesic.InverseLine<https://GeographicLib.SourceForge.io/Python/doc/code.html>}.

807 '''

808 r = self._GDictInverse(lat1, lon1, lat2, lon2, outmask=caps | Caps._SALP_CALPs_)

809 return GeodesicLineExact(self, lat1, lon1, None, caps=caps, _s_calp1=(r.salp1, r.calp1),

810 **name)._GenSet(self._debug, **r)

811

812 def _InverseArea(self, _meridian, salp1, calp1, # PYCHOK 9 args

813 salp2, calp2,

814 somg12, comg12, p):

815 '''(INTERNAL) Split off from C{_GDictInverse} to reduce complexity/length.

816

817 @return: Area C{S12}.

818 '''

819 # from _Lambda6: sin(alp1) * cos(bet1) = sin(alp0), calp0 > 0

820 salp0, calp0 = _sin1cos2(salp1, calp1, p.sbet1, p.cbet1)

821 A4 = calp0 * salp0

822 if A4:

823 # from _Lambda6: tan(bet) = tan(sig) * cos(alp)

824 k2 = calp0**2 * self.ep2

825 C4a = self._C4f_k2(k2)

826 B41 = _cosSeries(C4a, *_norm2(p.sbet1, calp1 * p.cbet1))

827 B42 = _cosSeries(C4a, *_norm2(p.sbet2, calp2 * p.cbet2))

828 # multiplier = a^2 * e^2 * cos(alpha0) * sin(alpha0)

829 A4 *= self._e2a2

830 S12 = A4 * (B42 - B41)

831 else: # avoid problems with indeterminate sig1, sig2 on equator

832 A4 = B41 = B42 = k2 = S12 = _0_0

833

834 if (_meridian and # omg12 < 3/4 * PI

835 comg12 > -_SQRT2_2 and # lon diff not too big

836 (p.sbet2 - p.sbet1) < _1_75): # lat diff not too big

837 # use tan(Gamma/2) = tan(omg12/2) *

838 # (tan(bet1/2) + tan(bet2/2)) /

839 # (tan(bet1/2) * tan(bet2/2) + 1))

840 # with tan(x/2) = sin(x) / (1 + cos(x))

841 dbet1 = p.cbet1 + _1_0

842 dbet2 = p.cbet2 + _1_0

843 domg12 = comg12 + _1_0

844 salp12 = fdot_(p.sbet1, dbet2, dbet1, p.sbet2) * somg12

845 calp12 = fdot_(p.sbet1, p.sbet2, dbet1, dbet2) * domg12

846 alp12 = atan2(salp12, calp12) * _2_0

847 else:

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

849 salp12, calp12 = _sincos12(salp1, calp1, salp2, calp2)

850 # The right thing appears to happen if alp1 = +/-180 and

851 # alp2 = 0, viz salp12 = -0 and alp12 = -180. However,

852 # this depends on the sign being attached to 0 correctly.

853 # Following ensures the correct behavior.

854 if salp12 == 0 and calp12 < 0:

855 alp12 = _copysign(PI, calp1)

856 else:

857 alp12 = atan2(salp12, calp12)

858

859 p.set_(alp12=alp12, A4=A4, B41=B41, B42=B42, k2=k2)

860 return S12 + self.c2x * alp12

861

862 def _InverseStart6(self, lam12, p):

863 '''(INTERNAL) Return a starting point for Newton's method in

864 C{salp1} and C{calp1} indicated by C{sig12=None}. If

865 Newton's method doesn't need to be used, return also

866 C{salp2}, C{calp2}, C{dnm} and C{sig12} non-C{None}.

867

868 @return: 6-Tuple C{(sig12, salp1, calp1, salp2, calp2, dnm)}

869 and C{p.set_sigs} updated for Newton, C{sig12=None}.

870 '''

871 sig12 = None # use Newton

872 salp1 = calp1 = salp2 = calp2 = NAN

873

874 # bet12 = bet2 - bet1 in [0, PI)

875 sbet12, cbet12 = _sincos12(p.sbet1, p.cbet1, p.sbet2, p.cbet2)

876 shortline = cbet12 >= 0 and sbet12 < _0_5 and (p.cbet2 * lam12) < _0_5

877 if shortline:

878 # sin((bet1 + bet2)/2)^2 = (sbet1 + sbet2)^2 / (

879 # (cbet1 + cbet2)^2 + (sbet1 + sbet2)^2)

880 t = p.sbet1 + p.sbet2

881 if t:

882 t *= t / hypot2(t, p.cbet1 + p.cbet2)

883 dnm = sqrt(self.ep2 * t + _1_0)

884 else:

885 dnm = _1_0

886 somg12, comg12 = _sincos2(lam12 / (self.f1 * dnm))

887 else:

888 somg12, comg12, dnm = p.slam12, p.clam12, NAN

889

890 # bet12a = bet2 + bet1 in (-PI, 0], note -sbet1

891 sbet12a, cbet12a = _sincos12(-p.sbet1, p.cbet1, p.sbet2, p.cbet2)

892

893 c = fabs(comg12) + _1_0 # == (1 - comg12) if comg12 < 0

894 s = somg12**2 / c

895 t = p.sbet1 * p.cbet2 * s

896 salp1 = p.cbet2 * somg12

897 calp1 = (sbet12a - t) if comg12 < 0 else (sbet12 + t)

898

899 ssig12 = hypot(salp1, calp1)

900 csig12 = fdot_(p.sbet1, p.sbet2, p.cbet1, p.cbet2 * comg12)

901

902 if shortline and ssig12 < self._eTOL2: # really short lines

903 t = c if comg12 < 0 else s

904 salp2, calp2 = _norm2(somg12 * p.cbet1,

905 sbet12 - p.cbet1 * p.sbet2 * t)

906 sig12 = atan2(ssig12, csig12) # do not use Newton

907

908 elif (self._n_0_1 or # Skip astroid calc if too eccentric

909 csig12 >= 0 or ssig12 >= (p.cbet1**2 * self._n6PI)):

910 pass # nothing to do, 0th order spherical approximation OK

911

912 else:

913 # Scale lam12 and bet2 to x, y coordinate system where antipodal

914 # point is at origin and singular point is at y = 0, x = -1

915 lam12x = atan2(-p.slam12, -p.clam12) # lam12 - PI

916 f = self.f

917 if f < 0: # PYCHOK no cover

918 # ssig1=sbet1, csig1=-cbet1, ssig2=sbet2, csig2=cbet2

919 p.set_sigs(p.sbet1, -p.cbet1, p.sbet2, p.cbet2)

920 # if lon12 = 180, this repeats a calculation made in Inverse

921 _, m12b, m0, _, _ = self._Length5(atan2(sbet12a, cbet12a) + PI,

922 Caps.REDUCEDLENGTH, p)

923 t = p.cbet1 * PI # x = dlat, y = dlon

924 x = m12b / (t * p.cbet2 * m0) - _1_0

925 sca = (sbet12a / (x * p.cbet1)) if x < -_0_01 else (-f * t)

926 y = lam12x / sca

927 else: # f >= 0, however f == 0 does not get here

928 sca = self._eF_reset_cHe2_f1(p.sbet1, p.cbet1 * _2_0)

929 x = lam12x / sca # dlon

930 y = sbet12a / (sca * p.cbet1) # dlat

931

932 if y > _TOL1 and x > -_THR1: # strip near cut

933 if f < 0: # PYCHOK no cover

934 calp1 = max( _0_0, x) if x > _TOL1 else max(_N_1_0, x)

935 salp1 = sqrt(_1_0 - calp1**2)

936 else:

937 salp1 = min( _1_0, -x)

938 calp1 = -sqrt(_1_0 - salp1**2)

939 else:

940 # Estimate alp1, by solving the astroid problem.

941 #

942 # Could estimate alpha1 = theta + PI/2, directly, i.e.,

943 # calp1 = y/k; salp1 = -x/(1+k); for _f >= 0

944 # calp1 = x/(1+k); salp1 = -y/k; for _f < 0 (need to check)

945 #

946 # However, it's better to estimate omg12 from astroid and use

947 # spherical formula to compute alp1. This reduces the mean

948 # number of Newton iterations for astroid cases from 2.24

949 # (min 0, max 6) to 2.12 (min 0, max 5). The changes in the

950 # number of iterations are as follows:

951 #

952 # change percent

953 # 1 5

954 # 0 78

955 # -1 16

956 # -2 0.6

957 # -3 0.04

958 # -4 0.002

959 #

960 # The histogram of iterations is (m = number of iterations

961 # estimating alp1 directly, n = number of iterations

962 # estimating via omg12, total number of trials = 148605):

963 #

964 # iter m n

965 # 0 148 186

966 # 1 13046 13845

967 # 2 93315 102225

968 # 3 36189 32341

969 # 4 5396 7

970 # 5 455 1

971 # 6 56 0

972 #

973 # omg12 is near PI, estimate work with omg12a = PI - omg12

974 k = _Astroid(x, y)

975 k1 = _1_0 + k

976 sca *= (y * k1 / k) if f < 0 else (x * k / k1)

977 s, c = _sincos2(-sca) # omg12a

978 # update spherical estimate of alp1 using omg12 instead of lam12

979 salp1 = p.cbet2 * s

980 calp1 = sbet12a - s * salp1 * p.sbet1 / (c + _1_0) # c = -c

981

982 # sanity check on starting guess. Backwards check allows NaN through.

983 salp1, calp1 = _norm2(salp1, calp1) if salp1 > 0 else (_1_0, _0_0)

984

985 return sig12, salp1, calp1, salp2, calp2, dnm

986

987 def _Lambda6(self, salp1, calp1, diffp, p):

988 '''(INTERNAL) Helper.

989

990 @return: 6-Tuple C{(lam12, sig12, salp2, calp2, domg12,

991 dlam12} and C{p.set_sigs} updated.

992 '''

993 eF = self._eF

994 f1 = self.f1

995

996 if p.sbet1 == calp1 == 0: # PYCHOK no cover

997 # Break degeneracy of equatorial line

998 calp1 = -_TINY

999

1000 # sin(alp1) * cos(bet1) = sin(alp0), # calp0 > 0

1001 salp0, calp0 = _sin1cos2(salp1, calp1, p.sbet1, p.cbet1)

1002 # tan(bet1) = tan(sig1) * cos(alp1)

1003 # tan(omg1) = sin(alp0) * tan(sig1)

1004 # = sin(bet1) * tan(alp1)

1005 somg1 = salp0 * p.sbet1

1006 comg1 = calp1 * p.cbet1

1007 ssig1, csig1 = _norm2(p.sbet1, comg1)

1008 # Without normalization we have schi1 = somg1

1009 cchi1 = f1 * p.dn1 * comg1

1010

1011 # Enforce symmetries in the case abs(bet2) = -bet1.

1012 # Need to be careful about this case, since this can

1013 # yield singularities in the Newton iteration.

1014 # sin(alp2) * cos(bet2) = sin(alp0)

1015 salp2 = (salp0 / p.cbet2) if p.cbet2 != p.cbet1 else salp1

1016 # calp2 = sqrt(1 - sq(salp2))

1017 # = sqrt(sq(calp0) - sq(sbet2)) / cbet2

1018 # and subst for calp0 and rearrange to give (choose

1019 # positive sqrt to give alp2 in [0, PI/2]).

1020 calp2 = fabs(calp1) if p.bet12 is None else (

1021 sqrt((calp1 * p.cbet1)**2 + p.bet12) / p.cbet2)

1022 # tan(bet2) = tan(sig2) * cos(alp2)

1023 # tan(omg2) = sin(alp0) * tan(sig2).

1024 somg2 = salp0 * p.sbet2

1025 comg2 = calp2 * p.cbet2

1026 ssig2, csig2 = _norm2(p.sbet2, comg2)

1027 # without normalization we have schi2 = somg2

1028 cchi2 = f1 * p.dn2 * comg2

1029

1030 # omg12 = omg2 - omg1, limit to [0, PI]

1031 somg12, comg12 = _sincos12(somg1, comg1, somg2, comg2, sineg0=True)

1032 # chi12 = chi2 - chi1, limit to [0, PI]

1033 schi12, cchi12 = _sincos12(somg1, cchi1, somg2, cchi2, sineg0=True)

1034

1035 p.set_sigs(ssig1, csig1, ssig2, csig2)

1036 # sig12 = sig2 - sig1, limit to [0, PI]

1037 sig12 = _atan12(ssig1, csig1, ssig2, csig2, sineg0=True)

1038

1039 eta12 = self._eF_reset_cHe2_f1(calp0, salp0) * _2__PI # then ...

1040 eta12 *= fsum1f_(eF.deltaH(*p.sncndn2),

1041 -eF.deltaH(*p.sncndn1), sig12)

1042 # eta = chi12 - lam12

1043 lam12 = _atan12(p.slam12, p.clam12, schi12, cchi12) - eta12

1044 # domg12 = chi12 - omg12 - deta12

1045 domg12 = _atan12( somg12, comg12, schi12, cchi12) - eta12

1046

1047 dlam12 = NAN # dv > 0 in ._Newton6

1048 if diffp:

1049 d = calp2 * p.cbet2

1050 if d:

1051 _, dlam12, _, _, _ = self._Length5(sig12, Caps.REDUCEDLENGTH, p)

1052 dlam12 *= f1 / d

1053 elif p.sbet1:

1054 dlam12 = -f1 * p.dn1 * _2_0 / p.sbet1

1055

1056 # p.set_(deta12=-eta12, lam12=lam12)

1057 return lam12, sig12, salp2, calp2, domg12, dlam12

1058

1059 def _Length5(self, sig12, outmask, p):

1060 '''(INTERNAL) Return M{m12b = (reduced length) / self.b} and

1061 calculate M{s12b = distance / self.b} and M{m0}, the

1062 coefficient of secular term in expression for reduced

1063 length and the geodesic scales C{M12} and C{M21}.

1064

1065 @return: 5-Tuple C{(s12b, m12b, m0, M12, M21)}.

1066 '''

1067 s12b = m12b = m0 = M12 = M21 = NAN

1068

1069 Cs = Caps

1070 eF = self._eF

1071

1072 # outmask &= Cs._OUT_MASK

1073 if (outmask & Cs.DISTANCE):

1074 # Missing a factor of self.b

1075 s12b = eF.cE * _2__PI * fsum1f_(eF.deltaE(*p.sncndn2),

1076 -eF.deltaE(*p.sncndn1), sig12)

1077

1078 if (outmask & Cs._REDUCEDLENGTH_GEODESICSCALE):

1079 m0x = -eF.k2 * eF.cD * _2__PI

1080 J12 = -m0x * fsum1f_(eF.deltaD(*p.sncndn2),

1081 -eF.deltaD(*p.sncndn1), sig12)

1082 if (outmask & Cs.REDUCEDLENGTH):

1083 m0 = m0x

1084 # Missing a factor of self.b. Add parens around

1085 # (csig1 * ssig2) and (ssig1 * csig2) to ensure

1086 # accurate cancellation for coincident points.

1087 m12b = fdot_(p.dn2, (p.csig1 * p.ssig2),

1088 -p.dn1, (p.ssig1 * p.csig2),

1089 J12, (p.csig1 * p.csig2))

1090 if (outmask & Cs.GEODESICSCALE):

1091 M12 = M21 = fdot_(p.ssig1, p.ssig2, p.csig1, p.csig2)

1092 t = (p.cbet1 - p.cbet2) * self.ep2 * \

1093 (p.cbet1 + p.cbet2) / (p.dn1 + p.dn2)

1094 M12 += fdot_(p.ssig2, t, p.csig2, J12) * p.ssig1 / p.dn1

1095 M21 -= fdot_(p.ssig1, t, p.csig1, J12) * p.ssig2 / p.dn2

1096

1097 return s12b, m12b, m0, M12, M21

1098

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

1100 '''Set up a L{GeodesicLineExact} to compute several points

1101 on a single geodesic.

1102

1103 @arg lat1: Latitude of the first point (C{degrees}).

1104 @arg lon1: Longitude of the first point (C{degrees}).

1105 @arg azi1: Azimuth at the first point (compass C{degrees}).

1106 @kwarg caps: Desired capabilities for the L{GeodesicLineExact} instance.

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

1108

1109 @return: A L{GeodesicLineExact} instance.

1110

1111 @note: If the point is at a pole, the azimuth is defined by keeping

1112 B{C{lon1}} fixed, writing C{B{lat1} = ±(90 − ε)}, and taking

1113 the limit C{ε → 0+}.

1114

1115 @see: C++ U{GeodesicExact.Line

1116 <https://GeographicLib.SourceForge.io/C++/doc/classGeographicLib_1_1GeodesicExact.html>}

1117 and Python U{Geodesic.Line<https://GeographicLib.SourceForge.io/Python/doc/code.html>}.

1118 '''

1119 return GeodesicLineExact(self, lat1, lon1, azi1, caps=caps, **name)._GenSet(self._debug)

1120

1121 @Property_RO

1122 def n(self):

1123 '''Get the C{ellipsoid}'s I{3rd flattening} (C{scalar}), M{f / (2 - f) == (a - b) / (a + b)}.

1124 '''

1125 return self.ellipsoid.n

1126

1127 @Property_RO

1128 def _n_0_1(self):

1129 '''(INTERNAL) Cached once.

1130 '''

1131 return fabs(self.n) > _0_1

1132

1133 @Property_RO

1134 def _n6PI(self):

1135 '''(INTERNAL) Cached once.

1136 '''

1137 return fabs(self.n) * _6_0 * PI

1138

1139 def _Newton6(self, salp1, calp1, p):

1140 '''(INTERNAL) Split off from C{_GDictInverse} to reduce complexity/length.

1141

1142 @return: 6-Tuple C{(sig12, salp1, calp1, salp2, calp2, domg12)}

1143 and C{p.iter} and C{p.trip} updated.

1144 '''

1145 _abs = fabs

1146 # This is a straightforward solution of f(alp1) = lambda12(alp1) -

1147 # lam12 = 0 with one wrinkle. f(alp) has exactly one root in the

1148 # interval (0, PI) and its derivative is positive at the root.

1149 # Thus f(alp) is positive for alp > alp1 and negative for alp < alp1.

1150 # During the course of the iteration, a range (alp1a, alp1b) is

1151 # maintained which brackets the root and with each evaluation of

1152 # f(alp) the range is shrunk, if possible. Newton's method is

1153 # restarted whenever the derivative of f is negative (because the

1154 # new value of alp1 is then further from the solution) or if the

1155 # new estimate of alp1 lies outside (0,PI); in this case, the new

1156 # starting guess is taken to be (alp1a + alp1b) / 2.

1157 salp1a = salp1b = _TINY

1158 calp1a, calp1b = _1_0, _N_1_0

1159 MAXIT1, TOL0 = _MAXIT1, _TOL0

1160 HALF, TOLb = _0_5, _TOLb

1161 tripb, TOLv = False, TOL0

1162 for i in range(_MAXIT2):

1163 # 1/4 meridian = 10e6 meter and random input,

1164 # estimated max error in nm (nano meter, by

1165 # checking Inverse problem by Direct).

1166 #

1167 # max iterations

1168 # log2(b/a) error mean sd

1169 # -7 387 5.33 3.68

1170 # -6 345 5.19 3.43

1171 # -5 269 5.00 3.05

1172 # -4 210 4.76 2.44

1173 # -3 115 4.55 1.87

1174 # -2 69 4.35 1.38

1175 # -1 36 4.05 1.03

1176 # 0 15 0.01 0.13

1177 # 1 25 5.10 1.53

1178 # 2 96 5.61 2.09

1179 # 3 318 6.02 2.74

1180 # 4 985 6.24 3.22

1181 # 5 2352 6.32 3.44

1182 # 6 6008 6.30 3.45

1183 # 7 19024 6.19 3.30

1184 v, sig12, salp2, calp2, \

1185 domg12, dv = self._Lambda6(salp1, calp1, i < MAXIT1, p)

1186

1187 # 2 * _TOL0 is approximately 1 ulp [0, PI]

1188 # reversed test to allow escape with NaNs

1189 if tripb or _abs(v) < TOLv:

1190 break

1191 # update bracketing values

1192 if v > 0 and (i > MAXIT1 or (calp1 / salp1) > (calp1b / salp1b)):

1193 salp1b, calp1b = salp1, calp1

1194 elif v < 0 and (i > MAXIT1 or (calp1 / salp1) < (calp1a / salp1a)):

1195 salp1a, calp1a = salp1, calp1

1196

1197 if i < MAXIT1 and dv > 0:

1198 dalp1 = -v / dv

1199 if _abs(dalp1) < PI:

1200 s, c = _sincos2(dalp1)

1201 # nalp1 = alp1 + dalp1

1202 s, c = _sincos12(-s, c, salp1, calp1)

1203 if s > 0:

1204 salp1, calp1 = _norm2(s, c)

1205 # in some regimes we don't get quadratic convergence

1206 # because slope -> 0. So use convergence conditions

1207 # based on epsilon instead of sqrt(epsilon)

1208 TOLv = TOL0 if _abs(v) > _TOL016 else _TOL08

1209 continue

1210 TOLv = TOL0

1211 # Either dv was not positive or updated value was outside

1212 # legal range. Use the midpoint of the bracket as the next

1213 # estimate. This mechanism is not needed for the WGS84

1214 # ellipsoid, but it does catch problems with more eccentric

1215 # ellipsoids. Its efficacy is such for the WGS84 test set

1216 # with the starting guess set to alp1 = 90 deg: the WGS84

1217 # test set: mean = 5.21, stdev = 3.93, max = 24 and WGS84

1218 # with random input: mean = 4.74, stdev = 0.99

1219 salp1, calp1 = _norm2((salp1a + salp1b) * HALF,

1220 (calp1a + calp1b) * HALF)

1221 tripb = fsum1f_(calp1a, -calp1, _abs(salp1a - salp1)) < TOLb or \

1222 fsum1f_(calp1b, -calp1, _abs(salp1b - salp1)) < TOLb

1223 else:

1224 v = Fmt.no_convergence(v, TOLv)

1225 raise GeodesicError(v, txt=repr(self)) # self.toRepr()

1226

1227 p.set_(iter=i, trip=tripb) # like .geodsolve._GDictInvoke: iter NOT iteration!

1228 return sig12, salp1, calp1, salp2, calp2, domg12

1229

1230 Polygon = Area # for C{geographiclib} compatibility

1231

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

1233 '''Return this C{GeodesicExact} as string.

1234

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

1236 for further details.

1237

1238 @return: C{GeodesicExact} (C{str}).

1239 '''

1240 t = GeodesicExact.caps, GeodesicExact.ellipsoid

1241 return self._instr(props=t, C4order=self.C4order, **prec_sep_name)

1242

1243

1244 class GeodesicLineExact(_GeodesicLineExact):

1245 '''A pure Python version of I{Karney}'s C++ class U{GeodesicLineExact

1246 <https://GeographicLib.SourceForge.io/C++/doc/classGeographicLib_1_1GeodesicLineExact.html>},

1247 modeled after I{Karney}'s Python class U{geodesicline.GeodesicLine<https://GitHub.com/

1248 geographiclib/geographiclib-python>}.

1249 '''

1250

1251 def __init__(self, geodesic, lat1, lon1, azi1, caps=Caps.STANDARD, **name):

1252 '''New L{GeodesicLineExact} instance, allowing points to be found along

1253 a geodesic starting at C{(B{lat1}, B{lon1})} with azimuth B{C{azi1}}.

1254

1255 @arg geodesic: The geodesic to use (L{GeodesicExact}).

1256 @arg lat1: Latitude of the first point (C{degrees}).

1257 @arg lon1: Longitude of the first point (C{degrees}).

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

1259 @kwarg caps: Bit-or'ed combination of L{Caps<pygeodesy.karney.Caps>} values

1260 specifying the capabilities the L{GeodesicLineExact} instance

1261 should possess, i.e., which quantities can bereturned by methods

1262 L{GeodesicLineExact.Position} and L{GeodesicLineExact.ArcPosition}.

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

1264

1265 @raise TypeError: Invalid B{C{geodesic}}.

1266 '''

1267 _xinstanceof(GeodesicExact, geodesic=geodesic)

1268 if (caps & Caps.LINE_OFF): # copy to avoid updates

1269 geodesic = geodesic.copy(deep=False, name=_UNDER_(NN, geodesic.name))

1270# _update_all(geodesic)

1271 _GeodesicLineExact.__init__(self, geodesic, lat1, lon1, azi1, caps, **name)

1272

1273

1274 def _Astroid(x, y):

1275 '''(INTERNAL) Solve M{k^4 + 2 * k^3 - (x^2 + y^2 - 1)

1276 * k^2 - (2 * k + 1) * y^2 = 0} for positive root k.

1277 '''

1278 p = x**2

1279 q = y**2

1280 r = fsumf_(_1_0, q, p, _N_2_0)

1281 if r > 0 or q:

1282 # avoid possible division by zero when r = 0

1283 # by multiplying s and t by r^3 and r, resp.

1284 S = p * q / _4_0 # S = r^3 * s

1285 if r:

1286 r = r / _6_0 # /= chokes PyChecker

1287 r3 = r**3

1288 T3 = r3 + S

1289 # discriminant of the quadratic equation for T3 is

1290 # zero on the evolute curve p^(1/3) + q^(1/3) = 1

1291 d = (r3 + T3) * S

1292 if d < 0:

1293 # T is complex, but u is defined for a real result

1294 a = atan2(sqrt(-d), -T3) / _3_0

1295 # There are 3 possible cube roots, choose the one which

1296 # avoids cancellation. Note d < 0 implies that r < 0.

1297 u = (cos(a) * _2_0 + _1_0) * r

1298 else:

1299 # pick the sign on the sqrt to maximize abs(T3) to

1300 # minimize loss of precision due to cancellation.

1301 if d:

1302 T3 += _copysign(sqrt(d), T3) # T3 = (r * t)^3

1303 # _cbrt always returns the real root, _cbrt(-8) = -2

1304 u = _cbrt(T3) # T = r * t

1305 if u: # T can be zero; but then r2 / T -> 0

1306 u += r**2 / u

1307 u += r

1308 elif S: # d == T3**2 == S**2: sqrt(d) == abs(S) == abs(T3)

1309 u = _cbrt(S * _2_0) # == T3 + _copysign(abs(S), T3)

1310 else:

1311 u = _0_0

1312 v = hypot(u, y) # sqrt(u**2 + q)

1313 # avoid loss of accuracy when u < 0

1314 u = (q / (v - u)) if u < 0 else (v + u)

1315 w = (u - q) / (v + v) # positive?

1316 # rearrange expression for k to avoid loss of accuracy due to

1317 # subtraction, division by 0 impossible because u > 0, w >= 0

1318 k = u / (sqrt(w**2 + u) + w) # guaranteed positive

1319

1320 else: # q == 0 && r <= 0

1321 # y = 0 with |x| <= 1. Handle this case directly, for

1322 # y small, positive root is k = abs(y) / sqrt(1 - x^2)

1323 k = _0_0

1324

1325 return k

1326

1327

1328 def _C4coeffs(nC4): # in .geodesicx.__main__

1329 '''(INTERNAL) Get the C{C4_24}, C{_27} or C{_30} series coefficients.

1330 '''

1331 try: # from pygeodesy.geodesicx._C4_xx import _coeffs_xx as _coeffs

1332 _C4_xx = _DOT_(typename(_MODS.geodesicx), _UNDER_('_C4', nC4))

1333 _coeffs = _MODS.getattr(_C4_xx, _UNDER_('_coeffs', nC4))

1334 except (AttributeError, ImportError, TypeError) as x:

1335 raise GeodesicError(nC4=nC4, cause=x)

1336 n = _xnC4(nC4=nC4)

1337 if len(_coeffs) != n: # double check

1338 raise GeodesicError(_coeffs=len(_coeffs), _xnC4=n, nC4=nC4)

1339 return _coeffs

1340

1341

1342 __all__ += _ALL_DOCS(GeodesicExact, GeodesicLineExact)

1343

1344# **) MIT License

1345#

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

1347#

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

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

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

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

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

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

1354#

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

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

1357#

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

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

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

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

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

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

1364# OTHER DEALINGS IN THE SOFTWARE.