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