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