Coverage for pygeodesy/geod3solve.py: 91%
144 statements
« prev ^ index » next coverage.py v7.10.7, created at 2026-03-25 15:01 -0400
« prev ^ index » next coverage.py v7.10.7, created at 2026-03-25 15:01 -0400
2# -*- coding: utf-8 -*-
4u'''Wrapper to invoke I{Karney}'s U{Geod3Solve
5<https://GeographicLib.SourceForge.io/C++/doc/Geod3Solve.1.html>} utility
6as a C{triaxial} geodesic, but intended I{mainly for testing purposes}.
8Set env variable C{PYGEODESY_GEOD3SOLVE} to the (fully qualified) path of
9the C{Geod3Solve} executable or use property L{Geodesic3Solve.Geod3Solve
10<geod3solve._Geodesic3SolveBase.Geod3Solve>}.
11'''
13from pygeodesy.angles import Ang, Deg, isAng, hypot
14from pygeodesy.basics import _xinstanceof, _MODS # typename
15from pygeodesy.constants import _0_0, _0_5, _360_0, _over
16# from pygeodesy.ellipses import Ellipse # _MODS
17from pygeodesy.errors import GeodesicError, _xkwds_get
18# from pygeodesy.fmath import hypot # from .angles
19from pygeodesy.interns import NN, _a12_, _DMAIN_, _s12_, _UNDER_, _UNUSED_
20from pygeodesy.karney import Caps, _GTuple, _Xables, sincos2d
21# from pygeodesy.lazily import _ALL_DOCS, _ALL_LAZY, _ALL_MODS as _MODS
22from pygeodesy.props import Property, Property_RO, property_RO
23from pygeodesy.solveBase import _Solve3Base, _ALL_DOCS, _ALL_LAZY
24from pygeodesy.triaxials.triaxial3 import Triaxial3, Triaxial3s
25from pygeodesy.units import Degrees, Meter
26# from pygeodesy.utily import sincos2d # from .karney
28__all__ = _ALL_LAZY.geod3solve
29__version__ = '26.02.12'
31_Triaxial3_WGS84 = Triaxial3s.WGS84_3r # a=6378172, b=6378102, c=6356752
34class Geodesic3Error(GeodesicError):
35 '''Error raised for issues in L{geod3solve<pygeodesy.geod3solve>}.
36 '''
37 pass
40class Geod3Solve8Tuple(_GTuple):
41 '''8-Tuple C{(bet1, omg1, alp1, bet2, omg2, alp2, s12, a12)} with C{ellipsoidal}
42 latitudes C{bet1} and C{bet2}, C{ellipsoidal} longitudes C{omg1} and C{omg2},
43 forward azimuths C{alp1} and C{alp2} in bearings from North, distance C{s12} in
44 C{meter}, conventionally and I{approximate} arc length C{a12} in degrees, see
45 U{Geod3Solve<https://GeographicLib.SourceForge.io/C++/doc/Geod3Solve.1.html>}.
46 '''
47 # from Geod3Solve --help option -f ... bet1 omg1 alp1 bet2 omg2 alp2 s12
48 _Names_ = ('bet1', 'omg1', 'alp1', 'bet2', 'omg2', 'alp2', _s12_, _a12_)
49 _Units_ = ( Deg, Deg, Deg, Deg, Deg, Deg, Meter, Deg)
51# @Property_RO
52# def A12(self):
53# '''Approximate arc C{A12} as C{Deg}.
54# '''
55# t = self
56# d = t.s12 or _0_0
57# if d:
58# a = hypot(Deg(t.bet2 - t.bet1).degrees,
59# Deg(t.omg2 - t.omg1).degrees)
60# d = (-a) if d < 0 else a
61# return Deg(d)
64class _Geodesic3SolveBase(_Solve3Base):
65 '''(INTERNAL) Base class for L{Geodesic3Solve} and L{GeodesicLine3Solve}.
66 '''
67 _a12x = Geod3Solve8Tuple._Names_.index(_a12_) # last
68 _Error = Geodesic3Error
69 _Names_Direct = _Names_Distance = \
70 _Names_Inverse = Geod3Solve8Tuple._Names_[:_a12x] # 7 only, always
71 _triaxial3 = _Triaxial3_WGS84
72 _Xable_name = _Xables.Geod3Solve.__name__ # typename
73 _Xable_path = _Xables.Geod3Solve()
74# assert _a12x == len(Geod3Solve8Tuple._Names_) - 1
75 del _a12x
77 @Property_RO
78 def a(self):
79 '''Get the triaxial's I{major} radius, semi-axis (C{meter}).
80 '''
81 return self._triaxial3.a
83 @Property_RO
84 def b(self):
85 '''Get the triaxial's I{middle} radius, semi-axis (C{meter}).
86 '''
87 return self._triaxial3.b
89 @Property_RO
90 def c(self):
91 '''Get the triaxial's I{minor} radius, semi-axis (C{meter}).
92 '''
93 return self._triaxial3.c
95 @Property_RO
96 def _cmdBasic(self):
97 '''(INTERNAL) Get the basic C{GeodSolve} cmd (C{tuple}).
98 '''
99 return (self.Geod3Solve, '-f') + (self._t_option +
100 self._p_option +
101 self._u_option)
103 @property_RO
104 def _e_option(self):
105 return ()
107 _mpd = _E_option = _e_option
108 flattening = f = ellipsoid = datum = None
110 @Property
111 def Geod3Solve(self):
112 '''Get the U{Geod3Solve<https://GeographicLib.SourceForge.io/C++/doc/Geod3Solve.1.html>}
113 executable (C{filename}).
114 '''
115 return self._Xable_path
117 @Geod3Solve.setter # PYCHOK setter!
118 def Geod3Solve(self, path):
119 '''Set the U{Geod3Solve<https://GeographicLib.SourceForge.io/C++/doc/Geod3Solve.1.html>}
120 executable (C{filename}), the (fully qualified) path to the C{Geod3Solve} executable.
122 @raise GeodesicError: Invalid B{C{path}}, B{C{path}} doesn't exist or
123 isn't the C{Geod3Solve} executable.
124 '''
125 self._setXable(path)
127 @property_RO
128 def _t_option(self):
129 return ('-t',) + self._toStdin(self._triaxial3._abc3)
131 def toStr(self, **prec_sep): # PYCHOK signature
132 '''Return this C{Geodesic3Solve} as string.
134 @kwarg prec_sep: See L{toStr<pygeodesy.solveBase._SolveBase.toStr>}.
136 @return: Geodesic3Solve items (C{str}).
137 '''
138 return _Solve3Base.toStr(self, Geod3Solve=self.Geod3Solve, **prec_sep)
140 @Property_RO
141 def _u_option(self):
142 return ('-u',) if self.unroll else ()
145class Geodesic3Solve(_Geodesic3SolveBase):
146 '''Wrapper to invoke I{Karney}'s U{Geod3Solve<https://GeographicLib.SourceForge.io/C++/doc/Geod3Solve.1.html>}
147 as a C{triaxial} version of I{Karney}'s Python class U{Geodesic<https://GeographicLib.SourceForge.io/C++/doc/
148 python/code.html#geographiclib.geodesic.Geodesic>}.
150 @note: Use property C{GeodSolve} or env variable C{PYGEODESY_GEOD3SOLVE} to specify the (fully
151 qualified) path to the C{Geod3Solve} executable.
153 @note: This C{geodesic} is intended I{for testing purposes only}, it invokes the C{GeodSolve}
154 executable for I{every} method call.
155 '''
157 def __init__(self, triaxial3=_Triaxial3_WGS84, path=NN, **name):
158 '''New C{Solve} instance.
160 @arg triaxial3: A triaxial (L{Triaxial3}), default C{Triaxial3s.WGS84_3r}.
161 @kwarg path: Optionally, the (fully qualified) path to the C{GeodSolve}
162 or C{RhumbSolve} executable (C{filename}).
163 @kwarg name: Optional C{B{name}=NN} (C{str}).
165 @raise TypeError: Invalid B{C{a_ellipsoid}} or B{C{f}}.
166 '''
167 _xinstanceof(Triaxial3, triaxial3=triaxial3)
168 if self._triaxial3 != triaxial3:
169 self._triaxial3 = triaxial3
170 if name:
171 self.name = name
172 if path:
173 self._setXable(path)
175 def _a12d(self, r):
176 '''(INTERNAL) Add arc C{a12} in degrees to C{GDict}.
177 '''
178 d = r.s12
179 if d:
180 a, b, c = self.triaxial3.abc3
181 E = _MODS.ellipses.Ellipse
182 z = _toAzi(r.alp1) + _toAzi(r.alp2)
183 s, z = sincos2d(z * _0_5)
184 d *= hypot(_over(s, E(a, b).perimeter2k), # azimuth!
185 _over(z, E(b, c).perimeter2k)) * _360_0
186 r[_a12_] = d or _0_0
187 return r
189 def Direct(self, bet1, omg1, alp1, s12, outmask=_UNUSED_, **unit): # PYCHOK unused
190 '''Return the C{Direct} result at distance C{s12}.
191 '''
192 bet1, omg1, alp1 = _toDegrees(bet1, omg1, alp1, **unit)
193 r = self._GDictDirect(bet1, omg1, alp1, False, s12)
194 return self._a12d(r)
196 def DirectLine(self, bet1, omg1, alp1, caps=Caps.ALL, **unit_name):
197 '''Set up a L{GeodesicLine3Solve} to compute several points
198 on a single geodesic.
200 @arg bet1: Ellipsoidal Latitude of the first point (C{Ang} or B{C{unit}}).
201 @arg omg1: Ellipsoidal Longitude of the first point (C{Ang} or B{C{unit}}).
202 @arg alp1: Azimuth at the first point (compass C{degrees}, C{Ang} or C{unit}).
203 @kwarg caps: Desired capabilities for the L{GeodesicLine3Solve} instance.
204 @kwarg unit_name: Optional C{B{name}=NN} (C{str}) and scalar C{B{unit}=}L{Degrees}
205 (or L{Degrees}).
207 @return: A L{GeodesicLine3Solve} instance.
209 @see: C++ U{GeodesicExact.Line
210 <https://GeographicLib.SourceForge.io/C++/doc/classGeographicLib_1_1GeodesicExact.html>}
211 and Python U{Geodesic.Line<https://GeographicLib.SourceForge.io/Python/doc/code.html>}.
212 '''
213 return GeodesicLine3Solve(self, bet1, omg1, alp1, caps=caps, **unit_name)
215 Line = DirectLine # ArcDirectLine
217 def Inverse(self, bet1, omg1, bet2, omg2, outmask=_UNUSED_, **unit): # PYCHOK unused
218 '''Return the C{Inverse} result.
219 '''
220 r = self._GDictInverse(*_toDegrees(bet1, omg1, bet2, omg2, **unit))
221 return self._a12d(r)
223 def InverseLine(self, bet1, omg1, bet2, omg2, caps=Caps.ALL, **unit_name):
224 '''Set up a L{GeodesicLine3Solve} to compute several points
225 on a single geodesic.
227 @arg bet1: Latitude of the first point (C{Ang} or B{C{unit}}).
228 @arg omg1: Longitude of the first point (C{Ang} or B{C{unit}}).
229 @arg bet2: Latitude of the second point (C{Ang} or B{C{unit}}).
230 @arg omg2: Longitude of the second point (C{Ang} or B{C{unit}}).
231 @kwarg caps: Desired capabilities for the L{GeodesicLine3Solve} instance.
232 @kwarg unit_name: Optional C{B{name}=NN} (C{str}) and scalar C{B{unit}=}L{Degrees}
233 (or L{Degrees}).
235 @return: A L{GeodesicLine3Solve} instance.
237 @note: Both B{C{bet1}} and B{C{bet2}} should in the range C{[-90, +90]}.
239 @see: C++ U{GeodesicExact.InverseLine
240 <https://GeographicLib.SourceForge.io/C++/doc/classGeographicLib_1_1GeodesicExact.html>} and
241 Python U{Geodesic.InverseLine<https://GeographicLib.SourceForge.io/Python/doc/code.html>}.
242 '''
243 r = self.Inverse(bet1, omg1, bet2, omg2, **unit_name)
244 gl = GeodesicLine3Solve(self, r.bet1, r.omg1, r.alp1, caps=caps, **unit_name)
245# gl._a13 = r.a12 # gl.SetArc(r.a12)
246# gl._s13 = r.s12 # gl.SetDistance(r.s12)
247 return gl
249 def toStr(self, **prec_sep_other): # PYCHOK signature
250 '''Return this C{Geodesic3Solve} as string.
252 @kwarg prec_sep: See L{toStr<pygeodesy.solveBase._Solve3Base.toStr>}.
254 @return: Geodesic3Solve items (C{str}).
255 '''
256 return _Solve3Base.toStr(self, Geod3Solve=self.Geod3Solve, **prec_sep_other)
258 @property_RO
259 def triaxial3(self):
260 '''Get the triaxial (C{Triaxial3}).
261 '''
262 return self._triaxial3
265class GeodesicLine3Solve(_Geodesic3SolveBase): # _SolveGDictLineBase):
266 '''Wrapper to invoke I{Karney}'s U{Geod3Solve<https://GeographicLib.SourceForge.io/C++/doc/GeodSolve.3.html>}
267 as a C{triaxial} version of I{Karney}'s Python class U{GeodesicLine<https://GeographicLib.SourceForge.io/
268 C++/doc/python/code.html#geographiclib.geodesicline.GeodesicLine>}.
270 @note: Use property C{GeodSolve} or env variable C{PYGEODESY_GEODSOLVE} to specify the (fully
271 qualified) path to the C{GeodSolve} executable.
273 @note: This C{geodesic} is intended I{for testing purposes only}, it invokes the C{GeodSolve}
274 executable for I{every} method call.
275 '''
276# _a13 = \
277# _s13 = NAN # see GeodesicSolve._InverseLine
278 _solve = None # L{Geodesic3Solve} instance
280 def __init__(self, geodesic3, bet1, omg1, alp1, caps=Caps.ALL, **unit_name):
281 '''New L{GeodesicLine3Solve} instance, allowing points to be found along
282 a geodesic starting at C{(B{bet1}, B{omg1})} with azimuth B{C{alp1}}.
284 @arg geodesic3: The geodesic to use (L{Geodesic3Solve}).
285 @arg bet1: Ellipsoidal latitude of the first point (C{Ang} or B{C{unit}}).
286 @arg omg1: Ellipsoidal longitude of the first point (C{Ang} or B{C{unit}}).
287 @arg alp1: Azimuth at the first point (compass C{degrees}, C{Ang} or C{unit}).
288 @kwarg caps: Bit-or'ed combination of L{Caps<pygeodesy.karney.Caps>} values
289 specifying the capabilities the L{GeodesicLine3Solve} instance
290 should possess, C{B{caps}=Caps.ALL} always. Include C{Caps.LINE_OFF}
291 if updates to the B{C{geodesic3}} should I{not be reflected} in this
292 L{GeodesicLine3Solve} instance.
293 @kwarg unit_name: Optional C{B{name}=NN} (C{str}) and scalar C{B{unit}=}L{Degrees}
294 (or L{Degrees}).
296 @raise Geodesic3Error: Invalid path for the C{Geod3Solve} executable or isn't the
297 C{Geod3Solve} executable, see property C{geodesic3.Geod3Solve}.
299 @raise TypeError: Invalid B{C{geodesic3}}.
300 '''
301 _xinstanceof(Geodesic3Solve, geodesic3=geodesic3)
302 if (caps & Caps.LINE_OFF): # copy to avoid updates
303 geodesic3 = geodesic3.copy(deep=False, name=_UNDER_(NN, geodesic3.name)) # NOT _under!
304 self._bet1, \
305 self._omg1, \
306 self._alp1 = _toDegrees(bet1, omg1, alp1, **unit_name)
307 self._caps = caps | Caps._AZIMUTH_LATITUDE_LONG_UNROLL
308 self._debug = geodesic3._debug & Caps._DEBUG_ALL
309 self._solve = geodesic3
310 try:
311 self.Geod3Solve = geodesic3.Geod3Solve # geodesic or copy of geodesic
312 except GeodesicError:
313 pass
314 if unit_name:
315 name = _xkwds_get(unit_name, name=None)
316 if name:
317 self.name = name
319 @Property_RO
320 def alp1(self):
321 '''Get the azimuth at the first point (compass C{degrees}).
322 '''
323 return self._alp1
325# azi12 = alp1 # like RhumbLineSolve
327# @Property_RO
328# def azi1_sincos2(self):
329# '''Get the sine and cosine of the first point's azimuth (2-tuple C{(sin, cos)}).
330# '''
331# return _sincos2d(self.alp1)
332#
333# azi12_sincos2 = azi1_sincos2
335 @Property_RO
336 def bet1(self):
337 '''Get the I{ellipsoidal} latitude of the first point (C{degrees}).
338 '''
339 return self._bet1
341 @Property_RO
342 def _cmdDistance(self):
343 '''(INTERNAL) Get the C{Geod3Solve} I{-L} cmd (C{tuple}).
344 '''
345 t = self.bet1, self.omg1, self.alp1
346 return self._cmdBasic + ('-L',) + self._toStdin(t)
348 @property_RO
349 def geodesic3(self):
350 '''Get the C{triaxial} geodesic (L{Geodesic3Solve}).
351 '''
352 return self._solve # see .solveBase._SolveLineBase
354 def Intersecant2(self, bet0, omg0, radius, **kwds): # PYCHOK no cover
355 '''B{Not implemented}, throws a C{NotImplementedError} always.'''
356 self._notImplemented(bet0, omg0, radius, **kwds)
358 @Property_RO
359 def omg1(self):
360 '''Get the I{ellipsoidal} longitude of the first point (C{degrees}).
361 '''
362 return self._omg1
364 def PlumbTo(self, bet0, omg0, **kwds): # PYCHOK no cover
365 '''B{Not implemented}, throws a C{NotImplementedError} always.'''
366 self._notImplemented(bet0, omg0, **kwds)
368 def Position(self, s12, outmask=Caps.ALL): # PYCHOK usused
369 '''Find the position on the line given B{C{s12}}.
371 @arg s12: Distance from the first point to the second (C{meter}).
373 @return: A C{GDict} with 7 items C{bet1, omg1, alp1, bet2, omg2, alp2, s12}.
374 '''
375 r = self._GDictInvoke(self._cmdDistance, self._Names_Distance, s12) # ._unCaps(outmask)
376 return self.geodesic3._a12d(r)
378 def toStr(self, **prec_sep_other): # PYCHOK signature
379 '''Return this C{GeodesicLine3Solve} as string.
381 @kwarg prec_sep: See L{toStr<pygeodesy.solveBase._Solve3Base.toStr>}.
383 @return: GeodesicLine3Solve items (C{str}).
384 '''
385 return _Solve3Base.toStr(self, bet1=self.bet1, omg1=self.omg1, alp1=self.alp1,
386 geodesic3=self.geodesic3, **prec_sep_other)
388 @property_RO
389 def triaxial3(self):
390 '''Get the triaxial (C{Triaxial3}).
391 '''
392 return self.geodesic3.triaxial3
395def _toAzi(alp): # as degrees
396 return alp.degrees0 if isAng(alp) else alp
399def _toDegrees(*angs, **unit_name):
400 unit = _xkwds_get(unit_name, unit=Degrees)
401 for ang in angs:
402 if not isAng(ang):
403 ang = Ang.fromScalar(ang, unit=unit)
404 yield ang.degrees
407__all__ += _ALL_DOCS(_Geodesic3SolveBase)
409if __name__ == _DMAIN_:
411 def _main():
412 from pygeodesy import printf
413 from sys import argv
415 gS = Geodesic3Solve(name='Test')
416 gS.verbose = v = '--verbose' in argv # or '-v' in argv
418 if not _Xables.X_OK(gS.Geod3Solve): # not set
419 gS.Geod3Solve = _Xables.Geod3Solve(_Xables.bin_)
420 printf('version: %s', gS.version, nt=v)
422 r = gS.Direct(40.6, -73.8, 51, 5.5e6)
423 printf('Direct: %r', r, nt=v)
425 printf('Inverse: %r', gS.Inverse( 40.6, -73.8, 51.6, -0.5), nt=v)
427 glS = GeodesicLine3Solve(gS, 40.6, -73.8, 51, name='LineTest')
428 printf('Line: %r', glS)
429 p = glS.Position(5.5e6)
430 printf('Position: %r %s', p, p == r)
432 _main()
434# % python3 -m pygeodesy.geod3solve
436# version: /opt/local/bin/Geod3Solve: GeographicLib version 2.7
437# Direct: GDict(a12=49.410276, alp1=51.0, alp2=107.340251, bet1=40.6, bet2=51.895816, omg1=-73.8, omg2=-1.038308, s12=5500000.0)
438# Inverse: GDict(a12=49.816802, alp1=51.235527, alp2=107.918138, bet1=40.6, bet2=51.6, omg1=-73.8, omg2=-0.5, s12=5545275.651379)
439# Line: GeodesicLine3Solve(alp1=51.0, bet1=40.6, geodesic3=Geodesic3Solve(Geod3Solve='/opt/local/bin/Geod3Solve', invokation=3, status=0), invokation=1, omg1=-73.8, status=0)
440# Position: GDict(a12=49.410276, alp1=51.0, alp2=107.340251, bet1=40.6, bet2=51.895816, omg1=-73.8, omg2=-1.038308, s12=5500000.0) True
443# % python3 -m pygeodesy.geod3solve --verbose
445# Geodesic3Solve 'Test'@1: /opt/local/bin/Geod3Solve --version (invoke)
446# Geodesic3Solve 'Test'@1: '/opt/local/bin/Geod3Solve: GeographicLib version 2.7' (0, stdout/-err)
447# Geodesic3Solve 'Test'@1: /opt/local/bin/Geod3Solve: GeographicLib version 2.7 (0)
448# version: /opt/local/bin/Geod3Solve: GeographicLib version 2.7
449#
450# Geodesic3Solve 'Test'@2: /opt/local/bin/Geod3Solve -f -t 6378172.0 6378102.0 6356752.0 -p 10 \ 40.6 -73.8 51.0 5500000.0 (Direct)
451# Geodesic3Solve 'Test'@2: '40.600000000000001 -73.799999999999997 51.000000000000007 51.895816223972680 -1.038308043217667 107.340251322641734 5500000.0000000000' (0, stdout/-err)
452# Geodesic3Solve 'Test'@2: bet1=40.600000000000001, omg1=-73.799999999999997, alp1=51.000000000000007, bet2=51.89581622397268, omg2=-1.038308043217667, alp2=107.340251322641734, s12=5500000.0 (0)
453# Direct: GDict(a12=49.410276, alp1=51.0, alp2=107.340251, bet1=40.6, bet2=51.895816, omg1=-73.8, omg2=-1.038308, s12=5500000.0)
454#
455# Geodesic3Solve 'Test'@3: /opt/local/bin/Geod3Solve -f -t 6378172.0 6378102.0 6356752.0 -p 10 -i \ 40.6 -73.8 51.6 -0.5 (Inverse)
456# Geodesic3Solve 'Test'@3: '40.600000000000001 -73.799999999999997 51.235527494379824 51.600000000000001 -0.500000000000000 107.918137616344865 5545275.6513788253' (0, stdout/-err)
457# Geodesic3Solve 'Test'@3: bet1=40.600000000000001, omg1=-73.799999999999997, alp1=51.235527494379824, bet2=51.600000000000001, omg2=-0.5, alp2=107.918137616344865, s12=5545275.6513788253 (0)
458# Inverse: GDict(a12=49.816802, alp1=51.235527, alp2=107.918138, bet1=40.6, bet2=51.6, omg1=-73.8, omg2=-0.5, s12=5545275.651379)
459#
460# Line: GeodesicLine3Solve(alp1=51.0, bet1=40.6, geodesic3=Geodesic3Solve(Geod3Solve='/opt/local/bin/Geod3Solve', invokation=3, status=0), invokation=1, omg1=-73.8, status=0)
461# Position: GDict(a12=49.410276, alp1=51.0, alp2=107.340251, bet1=40.6, bet2=51.895816, omg1=-73.8, omg2=-1.038308, s12=5500000.0) True
464# Examples <https://GeographicLib.SourceForge.io/C++/doc/Geod3Solve.1.html>
466# % echo 40:38:23N 073:46:44W-19.43W X 01:21:33N 103:59:22E-19.43W | tr X '\n' | Cart3Convert -G | Cart3Convert -E -r | tr '\n' ' ' | Geod3Solve -i -p 0 -f
467# 40.57193 -54.38111 3.20824 1.35529 123.41971 177.48319 15347602
468# % echo 40:38:23N 073:46:44W-19.43W X 01:21:33N 103:59:22E-19.43W | tr X '\n' | Cart3Convert -G | Cart3Convert -E -r | tr '\n' ' ' | Geod3Solve -i -p 0
469# 3.20824 177.48319 15347602
470# % echo 40:38:23N 073:46:44W-19.43W X 01:21:33N 103:59:22E-19.43W | tr X '\n' | Cart3Convert -G | Cart3Convert -E -r | tr '\n' ' ' | Geod3Solve -i -p 9
471# 3.20824419242752 177.48319457984906 15347602.214888915
473# % Geod3Solve -L 40.57193 -54.38111 3.20824
474# 15347602
475# 1.35529159 123.41971119 177.48319768
476# 15347602
477# 1.35529159 123.41971119 177.48319768
478# % Geod3Solve -L 40.57193 -54.38111 3.20824 -f
479# 15347602
480# 40.57193000 -54.38111000 3.20824000 1.35529159 123.41971119 177.48319768 15347602.000
481# % Geod3Solve -L 40.57193 -54.38111 3.20824 -f -p 9
482# 15347602
483# 40.57193000000000 -54.38111000000000 3.20824000000000 1.35529159010289 123.41971119123770 177.48319768195134 15347602.000000000
485# **) MIT License
486#
487# Copyright (C) 2025-2026 -- mrJean1 at Gmail -- All Rights Reserved.
488#
489# Permission is hereby granted, free of charge, to any person obtaining a
490# copy of this software and associated documentation files (the "Software"),
491# to deal in the Software without restriction, including without limitation
492# the rights to use, copy, modify, merge, publish, distribute, sublicense,
493# and/or sell copies of the Software, and to permit persons to whom the
494# Software is furnished to do so, subject to the following conditions:
495#
496# The above copyright notice and this permission notice shall be included
497# in all copies or substantial portions of the Software.
498#
499# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
500# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
501# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
502# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
503# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
504# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
505# OTHER DEALINGS IN THE SOFTWARE.