1
2# -*- coding: utf-8 -*-
3
4u'''A Python version of I{Karney}'s C++ class U{GeodesicLineExact
5<https://GeographicLib.SourceForge.io/html/classGeographicLib_1_1GeodesicLineExact.html>}.
6
7Copyright (C) Charles Karney (2012-2021) <Charles@Karney.com>
8and licensed under the MIT/X11 License.  For more information,
9see U{GeographicLib<https://GeographicLib.SourceForge.io>}.
10'''
11# make sure int/int division yields float quotient
12from __future__ import division
13
14# A copy of comments from Karney's C{GeodesicLineExact.cpp}:
15#
16# This is a reformulation of the geodesic problem.  The
17# notation is as follows:
18# - at a general point (no suffix or 1 or 2 as suffix)
19#   - phi = latitude
20#   - beta = latitude on auxiliary sphere
21#   - omega = longitude on auxiliary sphere
22#   - lambda = longitude
23#   - alpha = azimuth of great circle
24#   - sigma = arc length along great circle
25#   - s = distance
26#   - tau = scaled distance (= sigma at multiples of PI/2)
27# - at northwards equator crossing
28#   - beta = phi = 0
29#   - omega = lambda = 0
30#   - alpha = alpha0
31#   - sigma = s = 0
32# - a 12 suffix means a difference, e.g., s12 = s2 - s1.
33# - s and c prefixes mean sin and cos
34
35# from pygeodesy.basics import copysign0  # from .fmath
36from pygeodesy.fmath import copysign0, fsum_, hypot, norm2
37from pygeodesy.geodesicx.gxbases import _all_caps, _ALL_DOCS, Caps, \
38                                        _coSeries, _GeodesicBase, \
39                                        _sincos12, _TINY
40from pygeodesy.interns import NAN, NN, PI_2, _COMMASPACE_, \
41                             _name_, _0_0, _1_0, _90_0, _180_0
42# from pygeodesy.lazily import _ALL_DOCS  # from .geodesicx.bases
43from pygeodesy.karney import _around, _fix90, GDict, _norm180
44from pygeodesy.props import Property_RO, _update_all
45from pygeodesy.streprs import pairs
46from pygeodesy.utily import atan2d, sincos2, sincos2d
47
48from math import atan2, degrees, floor, radians
49
50__all__ = ()
51__version__ = '21.06.30'
52
53_glXs = []  # instances of C{[_]GeodesicLineExact}
54
55
56def _update_glXs(gX_glX):  # see .C4Order, ._ef_reset, .GdistDirect
57    '''(INTERNAL) Zap cached/memoized C{Property}s or remove.
58    '''
59    if _glXs:  # can become empty, even None
60        if isinstance(gX_glX, _GeodesicLineExact):
61            _glXs.remove(gX_glX)  # most recent
62        else:  # PYCHOK no cover
63            for glX in _glXs:  # XXX use weakref
64                glX._update(glX._gX is gX_glX)
65
66
67class _GeodesicLineExact(_GeodesicBase):
68    '''(INTERNAL) Base class for L{GeodesicLineExact}.
69    '''
70    _a13   = _s13 = NAN
71    _azi1  = _0_0
72    _caps  =  0  # not initilized
73    _cchi1 =  NAN
74    _dn1   =  NAN
75    _gX    =  None  # Exact only
76    _k2    =  NAN
77    _lat1  = _lon1 = _0_0
78    _salp0 = _calp0 = NAN
79    _salp1 = _calp1 = NAN
80    _somg1 = _comg1 = NAN
81    _ssig1 = _csig1 = NAN
82
83    def __init__(self, gX, lat1, lon1, azi1, caps, _debug, *salp1_calp1, **name):
84        '''(INTERNAL) New C{[_]GeodesicLineExact} instance.
85        '''
86        if salp1_calp1:
87            salp1, calp1 = salp1_calp1
88        else:
89            azi1 = _norm180(azi1)
90            # guard against salp0 underflow,
91            # also -0 is converted to +0
92            salp1, calp1 = sincos2d(_around(azi1))
93
94        self._gX    = gX  # Exact only
95        self._lat1  = lat1 = _fix90(lat1)
96        self._lon1  = lon1
97        self._azi1  = azi1
98        self._salp1 = salp1
99        self._calp1 = calp1
100        if _debug:  # PYCHOK no cover
101            caps |= Caps._DEBUG_LINE & _debug
102        # allow lat, azimuth and unrolling of lon
103        self._caps = caps | Caps._LINE
104        self._caps_DISTANCE_IN = caps & (Caps._OUTMASK & Caps.DISTANCE_IN)
105
106        name = name.get(_name_, NN) or gX.name
107        if name:
108            self.name = name
109
110        sbet1, cbet1 = sincos2d(_around(lat1))
111        sbet1 *= gX.f1
112        # Ensure cbet1 = +epsilon at poles
113        sbet1, cbet1 = norm2(sbet1, cbet1)
114        cbet1 = max(_TINY, cbet1)
115        self._dn1 = gX._dn(sbet1, cbet1)
116
117        # Evaluate alp0 from sin(alp1) * cos(bet1) = sin(alp0),
118        self._salp0 = cbet1 * salp1  # alp0 in [0, pi/2 - |bet1|]
119        # Alt: calp0 = hypot(sbet1, calp1 * cbet1).  The following
120        # is slightly better (consider the case salp1 = 0).
121        self._calp0 = hypot(calp1, salp1 * sbet1)
122        self._k2 = gX._eF_reset_k2(self._calp0)
123        # Evaluate sig with tan(bet1) = tan(sig1) * cos(alp1).
124        # sig = 0 is nearest northward crossing of equator.
125        # With bet1 = 0, alp1 = pi/2, we have sig1 = 0 (equatorial line).
126        # With bet1 =  pi/2, alp1 = -pi, sig1 =  pi/2
127        # With bet1 = -pi/2, alp1 =  0 , sig1 = -pi/2
128        # Evaluate omg1 with tan(omg1) = sin(alp0) * tan(sig1).
129        # With alp0 in (0, pi/2], quadrants for sig and omg coincide.
130        # No atan2(0,0) ambiguity at poles since cbet1 = +epsilon.
131        # With alp0 = 0, omg1 = 0 for alp1 = 0, omg1 = pi for alp1 = pi.
132        self._somg1 = sbet1 * self._salp0
133        self._comg1 = c = (cbet1 * calp1) if (sbet1 or calp1) else _1_0
134        # Without normalization we have schi1 = somg1.
135        self._cchi1 = gX.f1 * self._dn1 * c
136        self._ssig1, self._csig1 = norm2(sbet1, c)  # sig1 in (-pi, pi]
137        # norm2(somg1, comg1)  # no need to normalize!
138        # norm2(schi1?, cchi1)  # no need to normalize!
139
140        _glXs.append(self)
141        # no need to pre-compute other attrs based on _Caps.X.  All are
142        # Property_RO's, computed once and cached/memoized until reset
143        # when C4Order is changed or Elliptic function reset.
144
145    def __del__(self):  # XXX use weakref?
146        while _glXs:  # may be None
147            try:  # PYCHOK no cover
148                _glXs.remove(self)
149            except ValueError:
150                break
151        _update_all(self)
152        self._gX = None
153
154    def _update(self, updated, *attrs):
155        if updated:
156            _update_all(self, *attrs)
157
158    @Property_RO
159    def a13(self):
160        '''Get (spherical) arc length from the first to the reference point (C{degrees}).
161
162           @see: Method L{SetArc}.
163        '''
164        return self._a13
165
166    @Property_RO
167    def _A4_e2a2(self):
168        '''(INTERNAL) Cached/memoized.
169        '''
170        return self._calp0 * self._salp0 * self._gX._e2a2
171
172    def ArcPosition(self, a12, outmask=Caps.STANDARD):
173        '''Find the position on the line given B{C{a12}}.
174
175           @arg a12: Spherical arc length from the first point to the
176                     second point (C{degrees}).
177           @kwarg outmask: Bit-or'ed combination of L{Caps} values specifying
178                           the quantities to be returned.
179
180           @return: A C{dict} with up to 12 items C{lat1, lon1, azi1, lat2,
181                    lon2, azi2, m12, a12, s12, M12, M21, S12} with C{lat1},
182                    C{lon1}, C{azi1} and arc length C{a12} always included,
183                    except when C{a12=NAN}.
184
185           @note: By default, C{B{outmask}=STANDARD}, meaning thc C{lat1},
186                  C{lon1}, C{azi1}, C{lat2}, C{lon2}, C{azi2}, C{s12} and
187                  C{a12} entries are returned, except when C{a12=NAN}.
188        '''
189        return self._GDictPosition(True, a12, outmask)
190
191    @Property_RO
192    def azi0(self):
193        '''Get the I{equatorial azimuth}, the azimuth of this geodesic line
194           as it crosses the equator in a northward direction (C{degrees90}).
195        '''
196        t = self.azi0_sincos2
197        return NAN if NAN in t else atan2d(*t)
198
199    @Property_RO
200    def azi0_sincos2(self):
201        '''Get the sine and cosine of the I{equatorial azimuth} (2-tuple C{(sin, cos)}).
202        '''
203        return self._salp0, self._calp0
204
205    @Property_RO
206    def azi1(self):
207        '''Get the azimuth at the first point (compass C{degrees}).
208        '''
209        return self._azi1
210
211    @Property_RO
212    def azi1_sincos2(self):
213        '''Get the sine and cosine of the first point's azimuth (2-tuple C{(sin, cos)}).
214        '''
215        return self._salp1, self._calp1
216
217    @Property_RO
218    def _B41(self):
219        '''(INTERNAL) Cached/memoized.
220        '''
221        return _coSeries(self._C4a, self._ssig1, self._csig1)
222
223    @Property_RO
224    def _C4a(self):
225        '''(INTERNAL) Cached/memoized.
226        '''
227        return self._gX._C4f_k2(self._k2)
228
229    @Property_RO
230    def caps(self):
231        '''Get the capabilities (bit-or'ed C{Caps}).
232        '''
233        return self._caps
234
235    def caps_(self, caps):
236        '''Check the available capabilities.
237
238           @arg caps: Bit-or'ed combination of L{Caps} values
239                      for all capabilities to be checked.
240
241           @return: C{True} if I{all} B{C{caps}} are available,
242                    C{False} otherwise (C{bool}).
243        '''
244        return _all_caps(self.caps, caps)
245
246    @Property_RO
247    def _D0_k2(self):
248        '''(INTERNAL) Cached/memoized.
249        '''
250        return self._eF.cD / PI_2 * self._k2
251
252    @Property_RO
253    def _D1(self):
254        '''(INTERNAL) Cached/memoized.
255        '''
256        return self._eF.deltaD(self._ssig1, self._csig1, self._dn1)
257
258    @Property_RO
259    def _E0_b(self):
260        '''(INTERNAL) Cached/memoized.
261        '''
262        return self._eF.cE / PI_2 * self._gX.b
263
264    @Property_RO
265    def _E1(self):
266        '''(INTERNAL) Cached/memoized.
267        '''
268        return self._eF.deltaE(self._ssig1, self._csig1, self._dn1)
269
270    @Property_RO
271    def _eF(self):
272        '''(INTERNAL) Cached/memoized.
273        '''
274        return self._gX._eF  # .geodesic._eF
275
276    def _GDictPosition(self, arcmode, s12_a12, outmask):  # MCCABE 17
277        '''(INTERNAL) Generate a new position along the geodesic.
278
279           @return: A C{dict} with up to 12 items C{lat1, lon1, azi1, lat2,
280                    lon2, azi2, m12, a12, s12, M12, M21, S12} with C{lat1},
281                    C{lon1}, C{azi1} and arc length C{a12} always included,
282                    except when C{a12=NAN}.
283        '''
284        r = GDict(a12=NAN, s12=NAN)  # note both a12 and s12, always
285        if not (arcmode or self._caps_DISTANCE_IN):  # PYCHOK no cover
286            return r  # Uninitialized or impossible distance requested
287
288        if self.debug:  # PYCHOK no cover
289            outmask |= Caps._DEBUG_LINE & self._debug
290        outmask &= self._caps & Caps._OUTMASK
291
292        gX = self._gX
293        eF = self._eF
294
295        if arcmode:  # PYCHOK no cover
296            # s12_a12 is spherical arc length
297            E2 = _0_0
298            sig12 = radians(s12_a12)
299            ssig12, csig12 = sincos2(sig12)
300            a  = abs(s12_a12)
301            a -= floor(a / _180_0) * _180_0
302            if a == _0_0:
303                ssig12 = _0_0
304            elif a == _90_0:
305                csig12 = _0_0
306        else:  # s12_a12 is distance
307            t = s12_a12 / self._E0_b
308            s, c = sincos2(t)  # tau12
309            # tau2 = tau1 + tau12
310            E2 = -eF.deltaEinv(*_sincos12(-s, c, *self._stau1_ctau1))
311            sig12 = fsum_(self._E1, -E2, t)
312            ssig12, csig12 = sincos2(sig12)
313
314        salp0, calp0 = self._salp0, self._calp0
315        ssig1, csig1 = self._ssig1, self._csig1
316
317        # sig2 = sig1 + sig12
318        ssig2, csig2 = _sincos12(-ssig12, csig12, ssig1, csig1)
319        dn2 = eF.fDelta(ssig2, csig2)
320        # sin(bet2) = cos(alp0) * sin(sig2)
321        sbet2 = calp0 * ssig2
322        # Alt: cbet2 = hypot(csig2, salp0 * ssig2);
323        cbet2 = hypot(salp0, calp0 * csig2)
324        if not cbet2:  # salp0 = 0, csig2 = 0, break degeneracy
325            cbet2 = csig2 = _TINY
326        # tan(alp0) = cos(sig2) * tan(alp2)
327        salp2 = salp0
328        calp2 = calp0 * csig2  # no need to normalize
329
330        if (outmask & Caps.DISTANCE):
331            if arcmode:
332                E2 = eF.deltaE(ssig2, csig2, dn2)
333                # AB1 = _E0 * (E2 - _E1)
334                # s12 = _b * (_E0 * sig12 + AB1)
335                #     = _b * _E0 * (sig12 + (E2 - _E1))
336                #     = _b * _E0 * (E2 - _E1 + sig12)
337                s12 = self._E0_b * fsum_(E2, -self._E1, sig12)
338            else:
339                s12 = s12_a12
340            r.set_(s12=s12)
341
342        if (outmask & Caps._DEBUG_LINE):  # PYCHOK no cover
343            r.set_(sig12=sig12, dn2=dn2, E0_b=self._E0_b, E1=self._E1, E2=E2,
344                   e2=gX.e2, f1=gX.f1, eFk2=eF.k2, eFa2=eF.alpha2)
345
346        if (outmask & Caps.LONGITUDE):
347            schi1 = self._somg1
348            cchi1 = self._cchi1
349            schi2 = ssig2 * salp0
350            cchi2 = gX.f1 * dn2 * csig2  # schi2 = somg2 without normalization
351            lam12 = salp0 * self._H0_e2_f1 * fsum_(eF.deltaH(ssig2, csig2, dn2),
352                                                  -self._H1, sig12)
353            if (outmask & Caps.LONG_UNROLL):
354                E = copysign0(_1_0, salp0)  # east-going?
355                tchi1 = E * schi1
356                tchi2 = E * schi2
357                chi12 = E * fsum_(atan2(ssig1, csig1), -atan2(ssig2, csig2),
358                                  atan2(tchi2, cchi2), -atan2(tchi1, cchi1), sig12)
359                lon2 = self.lon1 + degrees(chi12 - lam12)
360            else:
361                chi12 = atan2(*_sincos12(schi1, cchi1, schi2, cchi2))
362                lon2 = _norm180(self._lon1_norm180 + _norm180(degrees(chi12 - lam12)))
363            r.set_(lon2=lon2)
364            if (outmask & Caps._DEBUG_LINE):  # PYCHOK no cover
365                r.set_(chi12=chi12, lam12=lam12, H0_e2_f1=self._H0_e2_f1, H1=self._H1,
366                       ssig2=ssig2, csig2=csig2)
367
368        if (outmask & Caps.LATITUDE):
369            r.set_(lat2=atan2d(sbet2, gX.f1 * cbet2))
370
371        if (outmask & Caps.AZIMUTH):
372            r.set_(azi2=atan2d(salp2, calp2, reverse=outmask & Caps.REVERSE2))
373
374        if (outmask & Caps._REDUCEDLENGTH_GEODESICSCALE):
375            dn1 = self._dn1
376            J12 = self._D0_k2 * fsum_(eF.deltaD(ssig2, csig2, dn2), -self._D1, sig12)
377            if (outmask & Caps._DEBUG_LINE):  # PYCHOK no cover
378                r.set_(dn1=dn1, D0_k2=self._D0_k2, D1=self._D1,
379                       J12=J12, ssig1=ssig1, csig1=csig1, b=gX.b)
380            if (outmask & Caps.REDUCEDLENGTH):
381                # Add parens around (_csig1 * ssig2) and (_ssig1 * csig2) to ensure
382                # accurate cancellation in the case of coincident points.
383                r.set_(m12=gX.b * fsum_(dn2 * (csig1 * ssig2),
384                                       -dn1 * (ssig1 * csig2),
385                                       -J12 * (csig1 * csig2)))
386            if (outmask & Caps.GEODESICSCALE):
387                t = self._k2 * (ssig2 - ssig1) * (ssig2 + ssig1) / (dn2 + dn1)
388                r.set_(M12=csig12 + (t * ssig2 - csig2 * J12) * ssig1 / dn1,
389                       M21=csig12 - (t * ssig1 - csig1 * J12) * ssig2 / dn2)
390
391        if (outmask & Caps.AREA):
392            B42 = _coSeries(self._C4a, ssig2, csig2)
393            if calp0 and salp0:
394                # tan(alp) = tan(alp0) * sec(sig)
395                # tan(alp2-alp1) = (tan(alp2) - tan(alp1)) / (tan(alp2) * tan(alp1) + 1)
396                # = calp0 * salp0 * (csig1 - csig2) / (salp0^2 + calp0^2 * csig1 * csig2)
397                # If csig12 > 0, write
398                #   csig1 - csig2 = ssig12 * (csig1 * ssig12 / (1 + csig12) + ssig1)
399                # else
400                #   csig1 - csig2 = csig1 * (1 - csig12) + ssig12 * ssig1
401                # No need to normalize
402                salp12 = (((ssig12 * csig1 / (_1_0 + csig12) + ssig1) * ssig12) if csig12 > 0 else
403                                    (csig1 * (_1_0 - csig12) + ssig1  * ssig12)) * salp0 * calp0
404                calp12 = salp0**2 + calp0**2 * csig1 * csig2
405            else:
406                # alp12 = alp2 - alp1, used in atan2 so no need to normalize
407                salp12, calp12 = _sincos12(self._salp1, self._calp1, salp2, calp2)
408                # We used to include some patch up code that purported to deal
409                # with nearly meridional geodesics properly.  However, this turned
410                # out to be wrong once salp1 = -0 was allowed (via InverseLine).
411                # In fact, the calculation of {s,c}alp12 was already correct
412                # (following the IEEE rules for handling signed zeros).  So,
413                # the patch up code was unnecessary (as well as dangerous).
414            A4_e2a2 = self._A4_e2a2
415            r.set_(S12=fsum_(A4_e2a2 * B42,
416                            -A4_e2a2 * self._B41, gX.c2 * atan2(salp12, calp12)))
417            if (outmask & Caps._DEBUG_LINE):  # PYCHOK no cover
418                r.set_(salp12=salp12, calp12=calp12, B42=B42, A4_e2a2=A4_e2a2,
419                       salp0=salp0,   calp0=calp0,   B41=self._B41, c2=gX.c2)
420
421        r.set_(a12=s12_a12 if arcmode else degrees(sig12),
422               lat1=self.lat1,  # == _fix90(lat1)
423               lon1=self.lon1 if (outmask & Caps.LONG_UNROLL) else self._lon1_norm180,
424               azi1=_norm180(self.azi1))
425        return r
426
427    def _GenPosition(self, arcmode, s12_a12, outmask):
428        '''(INTERNAL) Generate a new position along the geodesic.
429
430           @return: L{Direct9Tuple}C{(a12, lat2, lon2, azi2,
431                                      s12, m12,  M12,  M21, S12)}.
432        '''
433        r = self._GDictPosition(arcmode, s12_a12, outmask)
434        return r.toDirect9Tuple()
435
436    def _GenSet(self, arcmode, s13_a13):
437        '''(INTERNAL) Aka C++ C{GenSetDistance}.
438        '''
439        if arcmode:
440            self.SetArc(s13_a13)
441        else:
442            self.SetDistance(s13_a13)
443        return self  # for gx.GeodesicExact.InverseLine and ._GenDirectLine
444
445    @Property_RO
446    def geodesic(self):
447        '''Get the I{exact} geodesic (L{GeodesicExact}).
448        '''
449        return self._gX
450
451    @Property_RO
452    def _H0_e2_f1(self):
453        '''(INTERNAL) Cached/memoized.
454        '''
455        return self._eF.cH / PI_2 * self._gX._e2_f1
456
457    @Property_RO
458    def _H1(self):
459        '''(INTERNAL) Cached/memoized.
460        '''
461        return self._eF.deltaH(self._ssig1, self._csig1, self._dn1)
462
463    @Property_RO
464    def lat1(self):
465        '''Get the latitude of the first point (C{degrees}).
466        '''
467        return self._lat1
468
469    @Property_RO
470    def lon1(self):
471        '''Get the longitude of the first point (C{degrees}).
472        '''
473        return self._lon1
474
475    @Property_RO
476    def _lon1_norm180(self):
477        '''(INTERNAL) Cached/memoized.
478        '''
479        return _norm180(self._lon1)
480
481    def Position(self, s12, outmask=Caps.STANDARD):
482        '''Find the position on the line given B{C{s12}}.
483
484           @arg s12: Distance from the first point to the second C({meter}).
485           @kwarg outmask: Bit-or'ed combination of L{Caps} values specifying
486                           the quantities to be returned.
487
488           @return: A C{dict} with up to 12 items C{lat1, lon1, azi1, lat2,
489                    lon2, azi2, m12, a12, s12, M12, M21, S12} with C{lat1},
490                    C{lon1}, C{azi1} and arc length C{a12} always included,
491                    except when C{a12=NAN}.
492
493           @note: By default, C{B{outmask}=STANDARD}, meaning thc C{lat1},
494                  C{lon1}, C{azi1}, C{lat2}, C{lon2}, C{azi2}, C{s12} and
495                  C{a12} entries are returned, except when C{a12=NAN}.
496
497           @note: This L{GeodesicLineExact} instance must have been
498                  constructed with capability C{Caps.DISTANCE_IN} set.
499        '''
500        return self._GDictPosition(False, s12, outmask)
501
502    @Property_RO
503    def s13(self):
504        '''Get the distance from the first to the reference point C({meter}).
505
506           @see: Method L{SetDistance}.
507        '''
508        return self._s13
509
510    def SetArc(self, a13):
511        '''Set reference point 3 in terms of distance to the first point.
512
513           @arg a13: Spherical arc length from the first to the reference
514                     point (C{degrees}).
515
516           @return: The distance C{s13} (C{meter}) between the first and
517                    the reference point.
518        '''
519        self._a13 = a13
520        # In case the GeodesicLineExact doesn't have the DISTANCE capability.
521        self._s13 = s13 = self._GDictPosition(True, a13, Caps.DISTANCE).s12
522        return s13
523
524    def SetDistance(self, s13):
525        '''Set reference point 3 in terms of distance to the first point.
526
527           @arg s13: Distance from the first to the reference point C({meter}).
528
529           @return: The arc length C{a13} (C{degrees}) between the first
530                    and the reference point or C{NAN}.
531        '''
532        self._s13 = s13
533        # _a13 will be_NAN if the GeodesicLineExact
534        # doesn't have the DISTANCE_IN capability
535        self._a13 = a13 = self._GDictPosition(False, s13, 0).a12
536        return a13
537
538    @Property_RO
539    def _stau1_ctau1(self):
540        '''(INTERNAL) Cached/memoized.
541        '''
542        s, c = sincos2(self._E1)
543        # tau1 = sig1 + B11
544        return _sincos12(-s, c, self._ssig1, self._csig1)
545        # unnecessary because Einv inverts E
546        # return -self._gX._eF.deltaEinv(stau1, ctau1)
547
548    def toStr(self, prec=6, sep=_COMMASPACE_, **unused):  # PYCHOK signature
549        '''Return this C{GeodesicExactLine} as string.
550
551           @kwarg prec: The C{float} precision, number of decimal digits (0..9).
552                        Trailing zero decimals are stripped for B{C{prec}} values
553                        of 1 and above, but kept for negative B{C{prec}} values.
554           @kwarg sep: Optional separator to join (C{str}).
555
556           @return: C{GeodesicExactLine} (C{str}).
557        '''
558        d = dict(geodesic=self.geodesic,
559                 lat1=self.lat1, lon1=self.lon1, azi1=self.azi1,
560                 a13=self.a13, s13=self.s13)
561        return sep.join(pairs(d, prec=prec))
562
563
564__all__ += _ALL_DOCS(_GeodesicLineExact)
565
566# **) MIT License
567#
568# Copyright (C) 2016-2021 -- mrJean1 at Gmail -- All Rights Reserved.
569#
570# Permission is hereby granted, free of charge, to any person obtaining a
571# copy of this software and associated documentation files (the "Software"),
572# to deal in the Software without restriction, including without limitation
573# the rights to use, copy, modify, merge, publish, distribute, sublicense,
574# and/or sell copies of the Software, and to permit persons to whom the
575# Software is furnished to do so, subject to the following conditions:
576#
577# The above copyright notice and this permission notice shall be included
578# in all copies or substantial portions of the Software.
579#
580# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
581# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
582# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
583# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
584# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
585# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
586# OTHER DEALINGS IN THE SOFTWARE.
587