Coverage for pygeodesy/solveBase.py: 95%
203 statements
« prev ^ index » next coverage.py v7.2.2, created at 2023-04-05 15:46 -0400
« prev ^ index » next coverage.py v7.2.2, created at 2023-04-05 15:46 -0400
2# -*- coding: utf-8 -*-
4u'''(INTERNAL) Private base classes for L{pygeodesy.geodsolve} and L{pygeodesy.rhumbsolve}.
5'''
7from pygeodesy.basics import map2, ub2str, _zip
8from pygeodesy.constants import DIG
9from pygeodesy.errors import _AssertionError, _xkwds_get
10from pygeodesy.interns import NN, _0_, _BACKSLASH_, _COMMASPACE_, _enquote, \
11 _EQUAL_, _Error_, _not_, _SPACE_
12from pygeodesy.karney import Caps, _CapsBase, _a_ellipsoid, _EWGS84, GDict, \
13 Precision_, unroll180
14from pygeodesy.lazily import _ALL_DOCS, printf, _sys_version_info2
15from pygeodesy.named import callername, notOverloaded
16from pygeodesy.props import Property, Property_RO, property_RO, _update_all
17from pygeodesy.streprs import Fmt, fstr, fstrzs, pairs, strs
18# from pygeodesy.units import Precision_ # from .karney
19# from pygeodesy.utily import unroll180 # from .karney
21from subprocess import PIPE as _PIPE, Popen as _Popen, STDOUT as _STDOUT
23__all__ = () # nothing public
24__version__ = '22.10.04'
26_ERROR_ = 'ERROR'
27_text_True = dict() if _sys_version_info2 < (3, 7) else dict(text=True)
30def _cmd_stdin_(cmd, stdin): # PYCHOK no cover
31 '''(INTERNAL) Cmd line, stdin and caller as sC{str}.
32 '''
33 c = Fmt.PAREN(callername(up=3))
34 t = (c,) if stdin is None else (_BACKSLASH_, str(stdin), c)
35 return _SPACE_.join(cmd + t)
38def _popen2(cmd, stdin=None): # in .mgrs, .test.base, .test.testMgrs
39 '''(INTERNAL) Invoke C{B{cmd} tuple} and return C{exitcode}
40 and all output to C{stdout/-err}.
41 '''
42 p = _Popen(cmd, creationflags=0,
43 # executable=sys.executable, shell=True,
44 stdin=_PIPE, stdout=_PIPE, stderr=_STDOUT,
45 **_text_True) # PYCHOK kwArgs
46 r = p.communicate(stdin)[0]
47 return p.returncode, ub2str(r).strip()
50class _SolveLineSolveBase(_CapsBase):
51 '''(NTERNAL) Base class for C{_Solve} and C{_LineSolve}.
52 '''
53 _E = _EWGS84
54 _Error = None
55 _Exact = True
56 _invokation = 0
57 _Names_Direct = \
58 _Names_Inverse = ()
59 _prec = Precision_(prec=DIG)
60 _reverse2 = False
61 _Solve_name = NN # executable basename
62 _Solve_path = NN # executable path
63 _status = None
64 _unroll = False
65 _verbose = False
67 @Property_RO
68 def a(self):
69 '''Get the I{equatorial} radius, semi-axis (C{meter}).
70 '''
71 return self.ellipsoid.a
73 @property_RO
74 def _cmdBasic(self): # PYCHOK no cover
75 '''(INTERNAL) I{Must be overloaded}, see function C{notOverloaded}.
76 '''
77 notOverloaded(self)
79 @Property_RO
80 def ellipsoid(self):
81 '''Get the ellipsoid (C{Ellipsoid}).
82 '''
83 return self._E
85 @Property_RO
86 def _e_option(self):
87 E = self.ellipsoid
88 if E is _EWGS84:
89 return () # default
90 a, f = strs(E.a_f, fmt=Fmt.F, prec=DIG + 3) # not .G!
91 return ('-e', a, f)
93 @property
94 def Exact(self):
95 '''Get the Solve's C{exact} setting (C{bool}).
96 '''
97 return self._Exact
99 @Exact.setter # PYCHOK setter!
100 def Exact(self, Exact):
101 '''Set the Solve's C{exact} setting (C{bool}),
102 if C{True} use I{exact} version.
103 '''
104 Exact = bool(Exact)
105 if self._Exact != Exact:
106 _update_all(self)
107 self._Exact = Exact
109 @Property_RO
110 def f(self):
111 '''Get the ellipsoid's I{flattening} (C{float}), M{(a - b) / a}, C{0} for spherical, negative for prolate.
112 '''
113 return self.ellipsoid.f
115 def _GDictInvoke(self, cmd, floats, Names, *args):
116 '''(INTERNAL) Invoke C{Solve}, return results as C{GDict}.
117 '''
118 N = len(Names)
119 if N < 1:
120 raise _AssertionError(cmd=cmd, Names=Names)
121 i = fstr(args, prec=DIG, fmt=Fmt.F, sep=_SPACE_) if args else None # not Fmt.G!
122 t = self._invoke(cmd, stdin=i).lstrip().split() # 12-/+ tuple
123 if len(t) > N: # PYCHOK no cover
124 # unzip instrumented name=value pairs to names and values
125 n, v = _zip(*(p.split(_EQUAL_) for p in t[:-N])) # strict=True
126 v += tuple(t[-N:])
127 n += Names
128 else:
129 n, v = Names, t
130 if self.verbose: # PYCHOK no cover
131 self._print(_COMMASPACE_.join(map(Fmt.EQUAL, n, map(fstrzs, v))))
132 if floats:
133 v = map(float, v)
134 r = GDict(_zip(n, v)) # strict=True
135 return self._iter2tion(r, r)
137 @property_RO
138 def invokation(self):
139 '''Get the most recent C{Solve} invokation number (C{int}).
140 '''
141 return self._invokation
143 def invoke(self, *options, **stdin):
144 '''Invoke the C{Solve} executable and return the result.
146 @arg options: No, one or several C{Solve} command line
147 options (C{str}s).
148 @kwarg stdin: Optional input to pass to C{Solve.stdin} (C{str}).
150 @return: The C{Solve.stdout} and C{.stderr} output (C{str}).
152 @raise GeodesicError: On any error, including a non-zero return
153 code from C{GeodSolve}.
155 @raise RhumbError: On any error, including a non-zero return code
156 from C{RhumbSolve}.
158 @note: The C{Solve} return code is in property L{status}.
159 '''
160 c = (self._Solve_path,) + map2(str, options)
161 i = _xkwds_get(stdin, stdin=None)
162 r = self._invoke(c, stdin=i)
163 s = self.status
164 if s:
165 raise self._Error(cmd=_cmd_stdin_(c, i), status=s,
166 txt=_not_(_0_))
167 if self.verbose: # PYCHOK no cover
168 self._print(r)
169 return r
171 def _invoke(self, cmd, stdin=None):
172 '''(INTERNAL) Invoke the C{Solve} executable, with the
173 given B{C{cmd}} line and optional input to B{C{stdin}}.
174 '''
175 self._invokation += 1
176 self._status = t = None
177 if self.verbose: # PYCHOK no cover
178 t = _cmd_stdin_(cmd, stdin)
179 self._print(t)
180 try: # invoke and write to stdin
181 s, r = _popen2(cmd, stdin)
182 if len(r) < 6 or r[:5] in (_Error_, _ERROR_):
183 raise ValueError(r)
184 except (IOError, OSError, TypeError, ValueError) as x:
185 raise self._Error(cmd=t or _cmd_stdin_(cmd, stdin), cause=x)
186 self._status = s
187 return r
189 @property_RO
190 def _p_option(self):
191 return '-p', str(self.prec - 5) # -p is distance prec
193 @Property
194 def prec(self):
195 '''Get the precision, number of (decimal) digits (C{int}).
196 '''
197 return self._prec
199 @prec.setter # PYCHOK setter!
200 def prec(self, prec):
201 '''Set the precision for C{angles} in C{degrees}, like C{lat}, C{lon},
202 C{azimuth} and C{arc} in number of decimal digits (C{int}, C{0}..L{DIG}).
204 @note: The precision for C{distance = B{prec} - 5} or up to
205 10 decimal digits for C{nanometer} and for C{area =
206 B{prec} - 12} or at most C{millimeter} I{squared}.
207 '''
208 prec = Precision_(prec=prec, high=DIG)
209 if self._prec != prec:
210 _update_all(self)
211 self._prec = prec
213 def _print(self, line): # PYCHOK no cover
214 '''(INTERNAL) Print a status line.
215 '''
216 if self.status is not None:
217 line = _SPACE_(line, Fmt.PAREN(self.status))
218 printf('%s %d: %s', self.named2, self.invokation, line)
220 @Property
221 def reverse2(self):
222 '''Get the C{azi2} direction (C{bool}).
223 '''
224 return self._reverse2
226 @reverse2.setter # PYCHOK setter!
227 def reverse2(self, reverse2):
228 '''Set the direction for C{azi2} (C{bool}), if C{True} reverse C{azi2}.
229 '''
230 reverse2 = bool(reverse2)
231 if self._reverse2 != reverse2:
232 _update_all(self)
233 self._reverse2 = reverse2
235 def _setSolve(self, path, **Solve_path):
236 '''(INTERNAL) Set the executable C{path}.
237 '''
238 hold = self._Solve_path
239 if hold != path:
240 _update_all(self)
241 self._Solve_path = path
242 try:
243 _ = self.version # test path and ...
244 if self.status: # ... return code
245 S_p = Solve_path or {self._Solve_name: _enquote(path)}
246 raise self._Error(status=self.status, txt=_not_(_0_), **S_p)
247 hold = path
248 finally: # restore in case of error
249 if self._Solve_path != hold:
250 _update_all(self)
251 self._Solve_path = hold
253 @property_RO
254 def status(self):
255 '''Get the most recent C{Solve} return code (C{int}, C{str})
256 or C{None}.
257 '''
258 return self._status
260 @Property
261 def unroll(self):
262 '''Get the C{lon2} unroll'ing (C{bool}).
263 '''
264 return self._unroll
266 @unroll.setter # PYCHOK setter!
267 def unroll(self, unroll):
268 '''Set unroll'ing for C{lon2} (C{bool}), if C{True} unroll C{lon2}, otherwise don't.
269 '''
270 unroll = bool(unroll)
271 if self._unroll != unroll:
272 _update_all(self)
273 self._unroll = unroll
275 @property
276 def verbose(self):
277 '''Get the C{verbose} option (C{bool}).
278 '''
279 return self._verbose
281 @verbose.setter # PYCHOK setter!
282 def verbose(self, verbose):
283 '''Set the C{verbose} option (C{bool}), C{True} prints
284 a message around each C{RhumbSolve} invokation.
285 '''
286 self._verbose = bool(verbose)
288 @Property_RO
289 def version(self):
290 '''Get the result of C{"GeodSolve --version"} or C{"RhumbSolve --version"}.
291 '''
292 return self.invoke('--version')
295class _SolveBase(_SolveLineSolveBase):
296 '''(NTERNAL) Base class for C{_GeodesicSolveBase} and C{_RhumbSolveBase}.
297 '''
298 def __init__(self, a_ellipsoid=_EWGS84, f=None, path=NN, name=NN):
299 '''New C{Solve} instance.
301 @arg a_ellipsoid: An ellipsoid (L{Ellipsoid}) or datum (L{Datum}) or
302 the equatorial radius of the ellipsoid (C{scalar},
303 conventionally in C{meter}), see B{C{f}}.
304 @arg f: The flattening of the ellipsoid (C{scalar}) if B{C{a_ellipsoid}}
305 is specified as C{scalar}.
306 @kwarg path: Optionally, the (fully qualified) path to the C{GeodSolve}
307 or C{RhumbSolve} executable (C{filename}).
308 @kwarg name: Optional name (C{str}).
310 @raise TypeError: Invalid B{C{a_ellipsoid}} or B{C{f}}.
311 '''
312 if a_ellipsoid not in (self._E, None): # NOT self.ellipsoid
313 self._E = _a_ellipsoid(a_ellipsoid, f, name=name)
314 if name:
315 self.name = name
316 if path:
317 self._setSolve(path)
319 @Property_RO
320 def _cmdDirect(self):
321 '''(INTERNAL) Get the C{Solve} I{Direct} cmd (C{tuple}).
322 '''
323 return self._cmdBasic
325 @Property_RO
326 def _cmdInverse(self):
327 '''(INTERNAL) Get the C{Solve} I{Inverse} cmd (C{tuple}).
328 '''
329 return self._cmdBasic + ('-i',)
331 def Direct(self, lat1, lon1, azi1, s12, *unused):
332 '''Return the C{Direct} result.
333 '''
334 return self._GDictDirect(lat1, lon1, azi1, False, s12)
336 def _GDictDirect(self, lat, lon, azi, arcmode, s12_a12, *unused, **floats): # for .geodesicx.gxarea
337 '''(INTERNAL) Get C{_GenDirect}-like result as C{GDict}.
338 '''
339 if arcmode:
340 raise self._Error(arcmode=arcmode, txt=str(NotImplemented))
341 floats = _xkwds_get(floats, floats=True)
342 return self._GDictInvoke(self._cmdDirect, floats, self._Names_Direct,
343 lat, lon, azi, s12_a12)
345 def _GDictInverse(self, lat1, lon1, lat2, lon2, *unused, **floats): # for .geodesicx.gxarea
346 '''(INTERNAL) Get C{_GenInverse}-like result as C{GDict}, but
347 I{without} C{_SALPs_CALPs_}.
348 '''
349 floats = _xkwds_get(floats, floats=True)
350 return self._GDictInvoke(self._cmdInverse, floats, self._Names_Inverse,
351 lat1, lon1, lat2, lon2)
353 def Inverse(self, lat1, lon1, lat2, lon2, *unused):
354 '''Return the C{Inverse} result.
355 '''
356 return self._GDictInverse(lat1, lon1, lat2, lon2)
358 def Inverse1(self, lat1, lon1, lat2, lon2, wrap=False):
359 '''Return the non-negative, I{angular} distance in C{degrees}.
360 '''
361 # see .FrechetKarney.distance, .HausdorffKarney._distance
362 # and .HeightIDWkarney._distances
363 _, lon2 = unroll180(lon1, lon2, wrap=wrap) # self.LONG_UNROLL
364 r = self._GDictInverse(lat1, lon1, lat2, lon2, floats=False)
365 # XXX self.DISTANCE needed for 'a12'?
366 return abs(float(r.a12))
368 def _toStr(self, prec=6, sep=_COMMASPACE_, **Solve): # PYCHOK signature
369 '''(INTERNAL) Return this C{_Solve} as string..
370 '''
371 d = dict(ellipsoid=self.ellipsoid, invokation=self.invokation,
372 status=self.status, **Solve)
373 return sep.join(pairs(d, prec=prec))
376class _LineSolveBase(_SolveLineSolveBase):
377 '''(NTERNAL) Base class for C{GeodesicLineSolve} and C{RhumbLineSolve}.
378 '''
379# _caps = 0
380# _lla1 = {}
381 _solve = None # L{GeodesicSolve} or L{RhumbSolve} instance
383 def __init__(self, solve, lat1, lon1, caps, name, **azi):
384 self._caps = caps | Caps._LINE
385 self._debug = solve._debug & Caps._DEBUG_ALL
386 self._lla1 = GDict(lat1=lat1, lon1=lon1, **azi)
387 self._solve = solve
389 n = name or solve.name
390 if n:
391 self.name = n
393 @Property_RO
394 def _cmdDistance(self):
395 '''(INTERNAL) Get the C{GeodSolve} I{-L} cmd (C{tuple}).
396 '''
397 def _lla3(lat1=0, lon1=0, **azi):
398 _, azi = azi.popitem()
399 return lat1, lon1, azi
401 t = strs(_lla3(**self._lla1), prec=DIG, fmt=Fmt.F) # self._solve.prec
402 return self._cmdBasic + ('-L',) + t
404 @property_RO
405 def ellipsoid(self):
406 '''Get the ellipsoid (C{Ellipsoid}).
407 '''
408 return self._solve.ellipsoid
410 @Property_RO
411 def lat1(self):
412 '''Get the latitude of the first point (C{degrees}).
413 '''
414 return self._lla1.lat1
416 @Property_RO
417 def lon1(self):
418 '''Get the longitude of the first point (C{degrees}).
419 '''
420 return self._lla1.lon1
422 def _toStr(self, prec=6, sep=_COMMASPACE_, **solve): # PYCHOK signature
423 '''(INTERNAL) Return this C{_LineSolve} as string..
424 '''
425 d = dict(ellipsoid=self.ellipsoid, invokation=self._solve.invokation,
426 lat1=self.lat1, lon1=self.lon1,
427 status=self._solve.status, **solve)
428 return sep.join(pairs(d, prec=prec))
431__all__ += _ALL_DOCS(_SolveBase, _LineSolveBase, _SolveLineSolveBase)
433# **) MIT License
434#
435# Copyright (C) 2016-2023 -- mrJean1 at Gmail -- All Rights Reserved.
436#
437# Permission is hereby granted, free of charge, to any person obtaining a
438# copy of this software and associated documentation files (the "Software"),
439# to deal in the Software without restriction, including without limitation
440# the rights to use, copy, modify, merge, publish, distribute, sublicense,
441# and/or sell copies of the Software, and to permit persons to whom the
442# Software is furnished to do so, subject to the following conditions:
443#
444# The above copyright notice and this permission notice shall be included
445# in all copies or substantial portions of the Software.
446#
447# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
448# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
449# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
450# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
451# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
452# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
453# OTHER DEALINGS IN THE SOFTWARE.