1
2# -*- coding: utf-8 -*-
3
4u'''Wrapper to invoke I{Karney}'s U{GeodSolve
5<https://GeographicLib.SourceForge.io/html/GeodSolve.1.html>} utility
6as an (exact) geodesic, but intended I{for testing purposes only}.
7
8Set env variable C{PYGEODESY_GEODSOLVE} to the (fully qualified) path
9of the C{GeodSolve} executable.
10'''
11
12from pygeodesy.basics import map2, _xinstanceof
13from pygeodesy.datums import _ellipsoidal_datum
14from pygeodesy.ellipsoids import Ellipsoids, Ellipsoid2
15from pygeodesy.geodesicx.gxbases import _all_caps, Caps, _GeodesicBase
16from pygeodesy.interns import DIG, NN, _0_, _COMMASPACE_, _SPACE_
17from pygeodesy.interns import _not_  # PYCHOK used!
18from pygeodesy.karney import GDict, GeodesicError, GeodSolve12Tuple
19from pygeodesy.lazily import _ALL_DOCS, _ALL_LAZY, printf, _sys_version_info2
20from pygeodesy.lazily import _getenv  # PYCHOK used!
21from pygeodesy.named import callername
22from pygeodesy.namedTuples import Destination3Tuple, Distance3Tuple
23from pygeodesy.props import Property, Property_RO, property_RO
24from pygeodesy.streprs import Fmt, fstr, fstrzs, pairs, strs
25from pygeodesy.units import Precision_
26from pygeodesy.utily import sincos2d, unroll180, wrap360
27
28from subprocess import PIPE as _PIPE, Popen as _Popen, STDOUT as _STDOUT
29
30__all__ = _ALL_LAZY.geodsolve
31__version__ = '21.09.07'
32
33_PYGEODESY_GEODSOLVE_ = 'PYGEODESY_GEODSOLVE'  # PYCHOK used!
34
35_len_N  = len(GeodSolve12Tuple._Names_)
36_SLASH_ = '/'
37_stdin_ = 'stdin'
38
39
40def _cmd_stdin_(cmd, stdin):  # PYCHOK no cover
41    '''(INTERNAL) Cmd line, stdin and caller as sC{str}.
42    '''
43    c = Fmt.PAREN(callername(up=3))
44    t = (c,) if stdin is None else (_SLASH_, str(stdin), c)
45    return _SPACE_.join(cmd + t)
46
47
48class _GeodesicSolveBase(_GeodesicBase):
49    '''(NTERNAL) Base class for L{GeodesicSolve} and L{GeodesicLineSolve}.
50    '''
51    _E          =  Ellipsoids.WGS84
52    _Exact      =  True
53    _GeodSolve  = _getenv(_PYGEODESY_GEODSOLVE_, _PYGEODESY_GEODSOLVE_)
54    _invokation =  0
55    _prec       =  Precision_(prec=DIG)
56    _reverse2   =  False
57    _status     =  None
58    _text_True  =  dict() if _sys_version_info2 < (3, 7) else dict(text=True)
59    _unroll     =  False
60    _verbose    =  False
61
62    @Property_RO
63    def a(self):
64        '''Get the I{equatorial} radius, semi-axis (C{meter}).
65        '''
66        return self.ellipsoid.a
67
68    @Property_RO
69    def _b_option(self):
70        return ('-b',) if self.reverse2 else ()
71
72    @Property_RO
73    def _cmdBasic(self):
74        '''(INTERNAL) Get the C{GeodSolve} basic cmd (C{tuple}).
75        '''
76        return (self.GeodSolve,) + self._b_option \
77                                 + self._e_option \
78                                 + self._E_option \
79                                 + self._p_option \
80                                 + self._u_option + ('-f',)
81
82    @Property_RO
83    def _e_option(self):
84        if self.ellipsoid is Ellipsoids.WGS84:
85            return ()  # default
86        a, f = strs((self.a, self.f), fmt=Fmt.F, prec=DIG + 3)  # not .G!
87        return ('-e', a, f)
88
89    @Property_RO
90    def _E_option(self):
91        return ('-E',) if self.Exact else ()
92
93    @Property_RO
94    def ellipsoid(self):
95        '''Get the ellipsoid (C{Ellipsoid}).
96        '''
97        return self._E
98
99    @Property
100    def Exact(self):
101        '''Get the C{GeodesicExact} usage (C{bool}).
102        '''
103        return self._Exact
104
105    @Exact.setter  # PYCHOK setter!
106    def Exact(self, Exact):
107        '''Set the C{GeodesicExact} usage (C{bool}).
108
109           @arg Exact: If C{True} use C{GeodesicExact},
110                       otherwise use C{Geodesic} (C{bool}).
111        '''
112        Exact = bool(Exact)
113        self._update(Exact != self.Exact)
114        self._Exact = Exact
115
116    @Property_RO
117    def f(self):
118        '''Get the ellipsoid's I{flattening} (C{float}), M{(a - b) / a}, C{0} for spherical, negative for prolate.
119        '''
120        return self.ellipsoid.f
121
122    def _GDictInvoke(self, cmd, floats, *args):
123        '''(INTERNAL) Invoke C{GeodSolve}, return C{GDict}.
124        '''
125        i = fstr(args, prec=DIG, fmt=Fmt.F, sep=_SPACE_) if args else None  # not Fmt.G!
126        t = self._invoke(cmd, stdin=i).lstrip().split()  # 12-/+ tuple
127        if len(t) > _len_N:  # instrumented?
128            # unzip the name=value pairs to names and values
129            n, v = zip(*(p.split('=') for p in t[:-_len_N]))
130            v += tuple(t[-_len_N:])
131            n += GeodSolve12Tuple._Names_
132        else:
133            n, v = GeodSolve12Tuple._Names_, t
134        if self.verbose:  # PYCHOK no cover
135            self._print(_COMMASPACE_.join(map(Fmt.EQUAL, n, map(fstrzs, v))))
136        if floats:
137            v = map(float, v)
138        return GDict(zip(n, v))
139
140    @Property
141    def GeodSolve(self):
142        '''Get the U{GeodSolve<https://GeographicLib.SourceForge.io/html/GeodSolve.1.html>}
143           executable (C{filename}).
144        '''
145        return self._GeodSolve
146
147    @GeodSolve.setter  # PYCHOK setter!
148    def GeodSolve(self, path):
149        '''Set the U{GeodSolve<https://GeographicLib.SourceForge.io/html/GeodSolve.1.html>}
150           executable (C{filename}).
151
152           @arg path: The (fully qualified) path to the C{GeodSolve} executable (C{str}).
153
154           @raise GeodesicError: Invalid B{C{path}}, B{C{path}} doesn't exist or
155                                 isn't the C{GeodSolve} executable.
156        '''
157        hold = self.GeodSolve
158        self._update(path != hold)
159        self._GeodSolve = path
160        try:
161            self.version  # test path and ...
162            if self.status:  # ... return code
163                raise GeodesicError(GeodSolve=path, status=self.status, txt=_not_(_0_))
164            hold = path
165        finally:  # restore in case of error
166            self._update(hold != self.GeodSolve)
167            self._GeodSolve = hold
168
169    @property_RO
170    def invokation(self):
171        '''Get the most recent C{GeodSolve} invokation number (C{int}).
172        '''
173        return self._invokation
174
175    def invoke(self, *options, **stdin):
176        '''Invoke the C{GeodSolve} executable and return the result.
177
178           @arg options: No, one or several C{GeodSolve} command line
179                         options (C{str}s).
180           @kwarg stdin: Optional input to pass to C{GeodSolve.stdin} (C{str}).
181
182           @return: The C{GeodSolve.stdout} and C{.stderr} output (C{str}).
183
184           @raise GeodesicError: On any error, including a non-zero return
185                                 code from C{GeodSolve}.
186
187           @note: The C{GeodSolve} return code is in L{status}.
188        '''
189        c = (self.GeodSolve,) + map2(str, options)
190        i = stdin.get(_stdin_, None)
191        r = self._invoke(c, stdin=i)
192        s = self.status
193        if s:
194            raise GeodesicError(cmd=_cmd_stdin_(c, i), status=s,
195                                txt=_not_(_0_))
196        if self.verbose:  # PYCHOK no cover
197            self._print(r)
198        return r
199
200    def _invoke(self, cmd, stdin=None):
201        '''(INTERNAL) Invoke the C{GeodSolve} executable, with the
202           given B{C{cmd}} line and optional input to B{C{stdin}}.
203        '''
204        self._invokation += 1
205        self._status = t = None
206        if self.verbose:  # PYCHOK no cover
207            t = _cmd_stdin_(cmd, stdin)
208            self._print(t)
209        try:
210            p = _Popen(cmd, creationflags=0,
211                          # executable   =sys.executable,
212                          # shell        =True,
213                            stdin        =_PIPE,
214                            stdout       =_PIPE,  # PYCHOK kwArgs
215                            stderr       =_STDOUT,
216                          **self._text_True)
217            # invoke and write to stdin
218            r = p.communicate(stdin)[0]
219            if isinstance(r, bytes):  # Python 3+
220                r = r.decode('utf-8')
221
222            if len(r) < 6 or r[:5] in ('Error', 'ERROR'):
223                raise ValueError(r)
224
225            self._status = p.returncode
226        except (IOError, OSError, TypeError, ValueError) as x:
227            raise GeodesicError(cmd=t or _cmd_stdin_(cmd, stdin),
228                                txt=str(x))
229        return r.strip()
230
231    @Property_RO
232    def _p_option(self):
233        return '-p', str(self.prec - 5)  # -p is distance prec
234
235    @Property
236    def prec(self):
237        '''Get the precision, number of decimal digits (C{int}).
238        '''
239        return self._prec
240
241    @prec.setter  # PYCHOK setter!
242    def prec(self, prec):
243        '''Set the precision for C{angles} in C{degrees}, like C{lat},
244           C{lon}, C{azimuth} and C{arc}.
245
246           @arg prec: Number of decimal digits (C{int}, C{0}..L{DIG}).
247
248           @note: The precision for C{distance = B{prec} - 5} or up to
249                  10 decimal digits for C{nanometer} and for C{area =
250                  B{prec} - 12} or at most C{millimeter} I{squared}.
251        '''
252        prec = Precision_(prec=prec, high=DIG)
253        self._update(prec != self.prec)
254        self._prec = prec
255
256    def _print(self, line):  # PYCHOK no cover
257        '''(INTERNAL) Print a status line.
258        '''
259        if self.status is not None:
260            line = _SPACE_(line, Fmt.PAREN(self.status))
261        printf('%s %d: %s', self.named2, self.invokation, line)
262
263    @Property
264    def reverse2(self):
265        '''Get the C{azi2} direction (C{bool}).
266        '''
267        return self._reverse2
268
269    @reverse2.setter  # PYCHOK setter!
270    def reverse2(self, reverse2):
271        '''Set the direction for C{azi2} (C{bool}).
272
273           @arg reverse2: If C{True} reverse C{azi2} (C{bool}).
274        '''
275        reverse2 = bool(reverse2)
276        self._update(reverse2 != self.reverse2)
277        self._reverse2 = reverse2
278
279    @property_RO
280    def status(self):
281        '''Get the most recent C{GeodSolve} return code (C{int}, C{str})
282           or C{None}.
283        '''
284        return self._status
285
286    def toStr(self, prec=6, sep=_COMMASPACE_, **unused):  # PYCHOK signature
287        '''Return this C{GeodesicSolve} as string.
288
289           @kwarg prec: The C{float} precision, number of decimal digits (0..9).
290                        Trailing zero decimals are stripped for B{C{prec}} values
291                        of 1 and above, but kept for negative B{C{prec}} values.
292           @kwarg sep: Optional separator to join (C{str}).
293
294           @return: GeodesicSolve items (C{str}).
295        '''
296        d = dict(ellipsoid=self.ellipsoid, GeodSolve=self.GeodSolve,
297                 invokation=self.invokation, status=self.status)
298        return sep.join(pairs(d, prec=prec))
299
300    @Property
301    def unroll(self):
302        '''Get the C{lon2} unroll'ing (C{bool}).
303        '''
304        return self._unroll
305
306    @unroll.setter  # PYCHOK setter!
307    def unroll(self, unroll):
308        '''Set unroll'ing for C{lon2} (C{bool}).
309
310           @arg unroll: If C{True} unroll C{lon2},
311                        otherwise don't (C{bool}).
312        '''
313        unroll = bool(unroll)
314        self._update(unroll != self.unroll)
315        self._unroll = unroll
316
317    @Property_RO
318    def _u_option(self):
319        return '-u' if self.unroll else ()
320
321    @Property
322    def verbose(self):
323        '''Get the C{verbose} option (C{bool}).
324        '''
325        return self._verbose
326
327    @verbose.setter  # PYCHOK setter!
328    def verbose(self, verbose):
329        '''Set the C{verbose} option.
330
331          @arg verbose: Print a message around each
332                        C{GeodSolve} invokation (C{bool}).
333        '''
334        self._verbose = bool(verbose)
335
336    @property_RO
337    def version(self):
338        '''Get the result of C{"GeodSolve --version"}.
339        '''
340        return self.invoke('--version')
341
342
343class GeodesicSolve(_GeodesicSolveBase):
344    '''Wrapper to invoke I{Karney}'s U{GeodSolve<https://GeographicLib.SourceForge.io/html/GeodSolve.1.html>}
345       as an C{Exact} version of I{Karney}'s Python class U{Geodesic<https://GeographicLib.SourceForge.io/html/
346       python/code.html#geographiclib.geodesic.Geodesic>}.
347
348       @note: Use property C{GeodSolve} or env variable C{PYGEODESY_GEODSOLVE} to specify the (fully
349              qualified) path to the C{GeodSolve} executable.
350
351       @note: This C{geodesic} is intended I{for testing purposes only}, it invokes the C{GeodSolve}
352              executable for I{every} method call.
353    '''
354    def __init__(self, a_ellipsoid=Ellipsoids.WGS84, f=None, name=NN):
355        '''New L{GeodesicSolve} instance.
356
357           @arg a_ellipsoid: An ellipsoid (L{Ellipsoid}) or datum (L{datum}), or
358                             the equatorial radius of the ellipsoid (C{meter}).
359           @arg f: The flattening of the ellipsoid (C{scalar}) if B{C{a_ellipsoid}}
360                   is specified as C{meter}.
361           @kwarg name: Optional name (C{str}).
362        '''
363        if a_ellipsoid in (GeodesicSolve._E, None):
364            pass  # ignore f, default WGS84
365        elif f is None:
366            self._E = _ellipsoidal_datum(a_ellipsoid, name=name, raiser=True).ellipsoid
367        else:
368            self._E =  Ellipsoid2(a_ellipsoid, f, name=name)
369
370        if name:
371            self.name = name
372
373    def Area(self, polyline=False, name=NN):
374        '''Set up an L{GeodesicAreaExact} to compute area and
375           perimeter of a polygon.
376
377           @kwarg polyline: If C{True} perimeter only, otherwise
378                            area and perimeter (C{bool}).
379           @kwarg name: Optional name (C{str}).
380
381           @return: A L{GeodesicAreaExact} instance.
382
383           @note: The B{C{debug}} setting is passed as C{verbose}
384                  to the returned L{GeodesicAreaExact} instance.
385        '''
386        from pygeodesy.geodesicx.gxarea import GeodesicAreaExact
387        gaX = GeodesicAreaExact(self, polyline=polyline,
388                                      name=name or self.name)
389        if self.verbose or self.debug:  # PYCHOK no cover
390            gaX.verbose = True
391        return gaX
392
393    Polygon = Area  # for C{geographiclib} compatibility
394
395    @Property_RO
396    def _cmdDirect(self):
397        '''(INTERNAL) Get the C{GeodSolve} I{Direct} cmd (C{tuple}).
398        '''
399        return self._cmdBasic
400
401    @Property_RO
402    def _cmdInverse(self):
403        '''(INTERNAL) Get the C{GeodSolve} I{Inverse} cmd (C{tuple}).
404        '''
405        return self._cmdBasic + ('-i',)
406
407    def Direct(self, lat1, lon1, azi1, s12, *unused):
408        '''Return the C{Direct} result.
409        '''
410        return self._GDictInvoke(self._cmdDirect, True, lat1, lon1, azi1, s12)
411
412    def Direct3(self, lat1, lon1, azi1, s12):  # PYCHOK outmask
413        '''Return the destination lat, lon and reverse azimuth
414           (final bearing) in C{degrees}.
415
416           @return: L{Destination3Tuple}C{(lat, lon, final)}.
417        '''
418        d = self._GDictInvoke(self._cmdDirect, False, lat1, lon1, azi1, s12)
419        return Destination3Tuple(float(d.lat2), float(d.lon2), wrap360(d.azi2))
420
421    def _GDictDirect(self, lat, lon, azi, arcmode, s12_a12, *unused):  # for .geodesicx.gxarea
422        '''(INTERNAL) Get C{_GenDirect}-like result as C{GDict}.
423        '''
424        if arcmode:
425            raise GeodesicError(arcmode=arcmode, txt=str(NotImplemented))
426        return self._GDictInvoke(self._cmdDirect, True, lat, lon, azi, s12_a12)
427
428    def _GDictInverse(self, lat1, lon1, lat2, lon2, *unused):  # for .geodesicx.gxarea
429        '''(INTERNAL) Get C{_GenInverse}-like result as C{GDict}, but
430           I{without} C{_SALPs_CALPs_}.
431        '''
432        return self._GDictInvoke(self._cmdInverse, True, lat1, lon1, lat2, lon2)
433
434    def Inverse(self, lat1, lon1, lat2, lon2, *unused):
435        '''Return the C{Inverse} result.
436        '''
437        return self._GDictInvoke(self._cmdInverse, True, lat1, lon1, lat2, lon2)
438
439    def Inverse1(self, lat1, lon1, lat2, lon2, wrap=False):
440        '''Return the non-negative, I{angular} distance in C{degrees}.
441        '''
442        # see .FrechetKarney.distance, .HausdorffKarney._distance
443        # and .HeightIDWkarney._distances
444        _, lon2 = unroll180(lon1, lon2, wrap=wrap)  # self.LONG_UNROLL
445        d = self._GDictInvoke(self._cmdInverse, False, lat1, lon1, lat2, lon2)
446        # XXX self.DISTANCE needed for 'a12'?
447        return abs(float(d.a12))
448
449    def Inverse3(self, lat1, lon1, lat2, lon2):  # PYCHOK outmask
450        '''Return the distance in C{meter} and the forward and
451           reverse azimuths (initial and final bearing) in C{degrees}.
452
453           @return: L{Distance3Tuple}C{(distance, initial, final)}.
454        '''
455        d = self._GDictInvoke(self._cmdInverse, False, lat1, lon1, lat2, lon2)
456        return Distance3Tuple(float(d.s12), wrap360(d.azi1), wrap360(d.azi2))
457
458    def Line(self, lat1, lon1, azi1, caps=Caps.ALL):
459        '''Set up an L{GeodesicLineSolve} to compute several points
460           on a single geodesic.
461
462           @arg lat1: Latitude of the first point (C{degrees}).
463           @arg lon1: Longitude of the first point (C{degrees}).
464           @arg azi1: Azimuth at the first point (compass C{degrees}).
465           @kwarg caps: Bit-or'ed combination of L{Caps} values specifying
466                        the capabilities the L{GeodesicLineSolve} instance
467                        should possess, always C{Caps.ALL}.
468
469           @return: A L{GeodesicLineSolve} instance.
470
471           @note: If the point is at a pole, the azimuth is defined by keeping
472                  B{C{lon1}} fixed, writing C{B{lat1} = ±(90 − ε)}, and taking
473                  the limit C{ε → 0+}.
474
475           @see: C++ U{GeodesicExact.Line
476                 <https://GeographicLib.SourceForge.io/html/classGeographicLib_1_1GeodesicExact.html>}
477                 and Python U{Geodesic.Line<https://GeographicLib.SourceForge.io/html/python/code.html>}.
478        '''
479        return GeodesicLineSolve(self, lat1, lon1, azi1, caps=caps, name=self.name)
480
481    _Line = Line
482
483
484class GeodesicLineSolve(_GeodesicSolveBase):
485    '''Wrapper to invoke I{Karney}'s U{GeodSolve<https://GeographicLib.SourceForge.io/html/GeodSolve.1.html>}
486       as an C{Exact} version of I{Karney}'s Python class U{GeodesicLine<https://GeographicLib.SourceForge.io/html/
487       python/code.html#geographiclib.geodesicline.GeodesicLine>}.
488
489       @note: Use property C{GeodSolve} or env variable C{PYGEODESY_GEODSOLVE} to specify the (fully
490              qualified) path to the C{GeodSolve} executable.
491
492       @note: This C{geodesic} is intended I{for testing purposes only}, it invokes the C{GeodSolve}
493              executable for I{every} method call.
494    '''
495    _caps =  0
496    _gS   =  None  # Solve only
497    _lla1 = ()
498
499    def __init__(self, geodesic, lat1, lon1, azi1, caps=Caps.ALL, name=NN):
500        '''New L{GeodesicLineSolve} instance, allowing points to be found along
501           a geodesic starting at C{(B{lat1}, B{lon1})} with azimuth B{C{azi1}}.
502
503           @arg geodesic: The geodesic to use (L{GeodesicSolve}).
504           @arg lat1: Latitude of the first point (C{degrees}).
505           @arg lon1: Longitude of the first point (C{degrees}).
506           @arg azi1: Azimuth at the first points (compass C{degrees}).
507           @kwarg caps: Bit-or'ed combination of L{Caps} values specifying
508                        the capabilities the L{GeodesicLineSolve} instance
509                        should possess, always C{Caps.ALL}.
510           @kwarg name: Optional name (C{str}).
511
512           @raise TypeError: Invalid B{C{geodesic}}.
513        '''
514        _xinstanceof(GeodesicSolve, geodesic=geodesic)
515
516        self._gS   = gS = geodesic
517        self._lla1 = lat1, lon1, azi1
518        self._caps = caps | Caps._LINE
519
520        n = name or gS.name
521        if n:
522            self.name = n
523
524        self._debug   = gS._debug
525
526        self.Exact    = gS.Exact
527        self.prec     = gS.prec
528        self.reverse2 = gS.reverse2
529        self.unroll   = gS.unroll
530        self.verbose  = gS.verbose
531        try:
532            self.GeodSolve = gS.GeodSolve
533        except GeodesicError:
534            pass
535
536    def ArcPosition(self, a12, *unused):
537        '''Find the position on the line given B{C{a12}}.
538
539           @arg a12: Spherical arc length from the first point to the
540                     second point (C{degrees}).
541
542           @return: A C{dict} with 12 items C{lat1, lon1, azi1, lat2, lon2,
543                    azi2, m12, a12, s12, M12, M21, S12}.
544        '''
545        return self._GDictInvoke(self._cmdArc, True, a12)
546
547    @Property_RO
548    def azi1(self):
549        '''Get the azimuth at the first point (compass C{degrees}).
550        '''
551        return self._lla1[2]
552
553    @Property_RO
554    def azi1_sincos2(self):
555        '''Get the sine and cosine of the first point's azimuth (2-tuple C{(sin, cos)}).
556        '''
557        return sincos2d(self.azi1)
558
559    @Property_RO
560    def caps(self):
561        '''Get the capabilities (bit-or'ed C{Caps}).
562        '''
563        return self._caps
564
565    def caps_(self, caps):
566        '''Check the available capabilities.
567
568           @arg caps: Bit-or'ed combination of L{Caps} values
569                      for all capabilities to be checked.
570
571           @return: C{True} if I{all} B{C{caps}} are available,
572                    C{False} otherwise (C{bool}).
573        '''
574        return _all_caps(self.caps, caps)
575
576    @Property_RO
577    def _cmdArc(self):
578        '''(INTERNAL) Get the C{GeodSolve} I{-a -L} cmd (C{tuple}).
579        '''
580        return self._cmdDistance + ('-a',)
581
582    @Property_RO
583    def _cmdDistance(self):
584        '''(INTERNAL) Get the C{GeodSolve} I{-L} cmd (C{tuple}).
585        '''
586        return self._cmdBasic + ('-L',) + strs(self._lla1, prec=DIG, fmt=Fmt.F)
587
588    @Property_RO
589    def ellipsoid(self):
590        '''Get the ellipsoid (C{Ellipsoid}).
591        '''
592        return self._gS.ellipsoid
593
594    @Property_RO
595    def lat1(self):
596        '''Get the latitude of the first point (C{degrees}).
597        '''
598        return self._lla1[0]
599
600    @Property_RO
601    def lon1(self):
602        '''Get the longitude of the first point (C{degrees}).
603        '''
604        return self._lla1[1]
605
606    def Position(self, s12, *unused):
607        '''Find the position on the line given B{C{s12}}.
608
609           @arg s12: Distance from the first point to the second C({meter}).
610
611           @return: A C{dict} with 12 items C{lat1, lon1, azi1, lat2, lon2,
612                    azi2, m12, a12, s12, M12, M21, S12}, possibly C{a12=NAN}.
613        '''
614        return self._GDictInvoke(self._cmdDistance, True, s12)
615
616
617__all__ += _ALL_DOCS(_GeodesicSolveBase)
618
619if __name__ == '__main__':
620
621    gS = GeodesicSolve(name='Test')
622    if gS.GeodSolve in (_PYGEODESY_GEODSOLVE_, None):  # not set
623        gS.GeodSolve = '/opt/local/Cellar/geographiclib/1.51/bin/GeodSolve'  # HomeBrew
624    # gS.verbose = True
625    printf('version: %s', gS.version)
626
627    r = gS.Direct(40.6, -73.8, 51, 5.5e6)
628    printf('Direct: %r', r, nl=1)  # GDict(M12=0.650911, M21=0.651229, S12=39735075134877.09375, a12=49.475527, azi1=51.0, azi2=107.189397, lat1=40.6, lat2=51.884565, lon1=-73.8, lon2=-1.141173, m12=4844148.703101, s12=5500000.0)
629    printf('Direct3: %r', gS.Direct3(40.6, -73.8, 51, 5.5e6))  # Destination3Tuple(lat=51.884565, lon=-1.141173, final=107.189397)
630
631    printf('Inverse: %r',  gS.Inverse( 40.6, -73.8, 51.6, -0.5), nl=1)  # GDict(M12=0.64473, M21=0.645046, S12=40041368848742.53125, a12=49.94131, azi1=51.198883, azi2=107.821777, lat1=40.6, lat2=51.6, lon1=-73.8, lon2=-0.5, m12=4877684.602706, s12=5551759.400319)
632    printf('Inverse1: %r', gS.Inverse1(40.6, -73.8, 51.6, -0.5))  # 49.94131021789904
633    printf('Inverse3: %r', gS.Inverse3(40.6, -73.8, 51.6, -0.5))  # Distance3Tuple(distance=5551759.400319, initial=51.198883, final=107.821777)
634
635    glS = GeodesicLineSolve(gS, 40.6, -73.8, 51)
636    p = glS.Position(5.5e6)
637    printf('Position:    %s  %r', p == r, p, nl=1)
638    p = glS.ArcPosition(49.475527)
639    printf('ArcPosition: %s %r', p == r, p)
640
641# % python3 -m pygeodesy.geodsolve
642# version: /opt/local/Cellar/geographiclib/1.51/bin/GeodSolve: GeographicLib version 1.51
643#
644# Direct: GDict(M12=0.650911, M21=0.651229, S12=39735075134877.09375, a12=49.475527, azi1=51.0, azi2=107.189397, lat1=40.6, lat2=51.884565, lon1=-73.8, lon2=-1.141173, m12=4844148.703101, s12=5500000.0)
645# Direct3: Destination3Tuple(lat=51.884565, lon=-1.141173, final=107.189397)
646#
647# Inverse: GDict(M12=0.64473, M21=0.645046, S12=40041368848742.53125, a12=49.94131, azi1=51.198883, azi2=107.821777, lat1=40.6, lat2=51.6, lon1=-73.8, lon2=-0.5, m12=4877684.602706, s12=5551759.400319)
648# Inverse1: 49.94131021789904
649# Inverse3: Distance3Tuple(distance=5551759.400319, initial=51.198883, final=107.821777)
650#
651# Position:    True  GDict(M12=0.650911, M21=0.651229, S12=39735075134877.09375,  a12=49.475527, azi1=51.0, azi2=107.189397, lat1=40.6, lat2=51.884565, lon1=-73.8, lon2=-1.141173, m12=4844148.703101, s12=5500000.0)
652# ArcPosition: False GDict(M12=0.650911, M21=0.651229, S12=39735074737272.734375, a12=49.475527, azi1=51.0, azi2=107.189397, lat1=40.6, lat2=51.884565, lon1=-73.8, lon2=-1.141174, m12=4844148.669561, s12=5499999.948497)
653
654# **) MIT License
655#
656# Copyright (C) 2016-2021 -- mrJean1 at Gmail -- All Rights Reserved.
657#
658# Permission is hereby granted, free of charge, to any person obtaining a
659# copy of this software and associated documentation files (the "Software"),
660# to deal in the Software without restriction, including without limitation
661# the rights to use, copy, modify, merge, publish, distribute, sublicense,
662# and/or sell copies of the Software, and to permit persons to whom the
663# Software is furnished to do so, subject to the following conditions:
664#
665# The above copyright notice and this permission notice shall be included
666# in all copies or substantial portions of the Software.
667#
668# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
669# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
670# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
671# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
672# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
673# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
674# OTHER DEALINGS IN THE SOFTWARE.
675