Coverage for pygeodesy/solveBase.py: 93%
243 statements
« prev ^ index » next coverage.py v7.2.2, created at 2024-07-10 09:25 -0400
« prev ^ index » next coverage.py v7.2.2, created at 2024-07-10 09:25 -0400
2# -*- coding: utf-8 -*-
4u'''(INTERNAL) Private base classes for L{pygeodesy.geodsolve} and L{pygeodesy.rhumb.solve}.
5'''
7from pygeodesy.basics import clips, 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_get1, \
12 _xkwds_item2
13from pygeodesy.internals import _enquote, printf
14from pygeodesy.interns import NN, _0_, _BACKSLASH_, _COMMASPACE_, \
15 _EQUAL_, _Error_, _SPACE_, _UNUSED_
16from pygeodesy.karney import Caps, _CapsBase, GDict
17from pygeodesy.lazily import _ALL_DOCS, _ALL_LAZY, _sys_version_info2
18from pygeodesy.named import callername, _name2__, notOverloaded
19from pygeodesy.props import Property, Property_RO, property_RO, _update_all
20from pygeodesy.streprs import Fmt, fstr, fstrzs, pairs, strs
21from pygeodesy.units import Precision_
22from pygeodesy.utily import unroll180, wrap360 # PYCHOK shared
24from subprocess import PIPE as _PIPE, Popen as _Popen, STDOUT as _STDOUT
26__all__ = _ALL_LAZY.solveBase
27__version__ = '24.07.08'
29_ERROR_ = 'ERROR'
30_Popen_kwds = dict(creationflags=0,
31 # executable=sys.executable, shell=True,
32 stdin=_PIPE, stdout=_PIPE, stderr=_STDOUT)
33if _sys_version_info2 > (3, 6):
34 _Popen_kwds.update(text=True)
35del _PIPE, _STDOUT, _sys_version_info2 # _ALL_LAZY
38def _cmd_stdin_(cmd, stdin): # PYCHOK no cover
39 '''(INTERNAL) Cmd line, stdin and caller as sC{str}.
40 '''
41 if stdin is not None:
42 cmd += _BACKSLASH_, str(stdin)
43 cmd += Fmt.PAREN(callername(up=3)),
44 return _SPACE_.join(cmd)
47# def _float_int(r):
48# '''(INTERNAL) Convert result into C{float} or C{int}.
49# '''
50# f = float(r)
51# i = int(f)
52# return i if float(i) == f else f # PYCHOK inconsistent
55def _popen2(cmd, stdin=None): # in .mgrs, test.bases, .testMgrs
56 '''(INTERNAL) Invoke C{B{cmd} tuple} and return C{exitcode}
57 and all output from C{stdout/-err}.
58 '''
59 p = _Popen(cmd, **_Popen_kwds) # PYCHOK kwArgs
60 r = p.communicate(stdin)[0] # stdout + NL + stderr
61 return p.returncode, ub2str(r).strip()
64class _SolveCapsBase(_CapsBase):
65 '''(NTERNAL) Base class for C{_SolveBase} and C{_LineSolveBase}.
66 '''
67 _datum = _WGS84
68 _Error = None
69 _Exact = True
70 _invokation = 0
71 _linelimit = 0
72 _prec = Precision_(prec=DIG)
73 _prec2stdin = DIG
74 _Xable_name = NN # executable basename
75 _Xable_path = NN # executable path
76 _status = None
77 _verbose = False
79 @Property_RO
80 def a(self):
81 '''Get the I{equatorial} radius, semi-axis (C{meter}).
82 '''
83 return self.ellipsoid.a
85 @property_RO
86 def _cmdBasic(self): # PYCHOK no cover
87 '''(INTERNAL) I{Must be overloaded}.'''
88 notOverloaded(self, underOK=True)
90 @property_RO
91 def datum(self):
92 '''Get the datum (C{Datum}).
93 '''
94 return self._datum
96 def _Dict(self, Dict, n, v, floats=True, **unused):
97 if self.verbose: # PYCHOK no cover
98 self._print(_COMMASPACE_.join(map(Fmt.EQUAL, n, map(fstrzs, v))))
99 if floats:
100 v = map(float, v) # _float_int, see Intersectool._XDistInvoke
101 return Dict(_zip(n, v)) # strict=True
103 def _DictInvoke2(self, cmd, Names, Dict, args, **floats_R):
104 '''(INTERNAL) Invoke C{Solve}, return results as C{Dict}.
105 '''
106 N = len(Names)
107 if N < 1:
108 raise _AssertionError(cmd=cmd, Names=Names)
109 i = fstr(args, prec=self._prec2stdin, fmt=Fmt.F, sep=_SPACE_) if args else None # NOT Fmt.G!
110 t = self._invoke(cmd, stdin=i, **floats_R).lstrip().split() # 12-/++ tuple
111 if _xkwds_get(floats_R, _R=None): # == '-R' in cmd
112 return self._Dicts(Dict, Names, t, **floats_R), True
113 elif len(t) > N: # PYCHOK no cover
114 # unzip instrumented name=value pairs to names and values
115 n, v = _zip(*(p.split(_EQUAL_) for p in t[:-N])) # strict=True
116 v += tuple(t[-N:])
117 n += Names
118 else:
119 n, v = Names, t
120 r = self._Dict(Dict, n, t, **floats_R)
121 return self._iter2tion(r, **r), None
123 def _Dicts(self, Dict, Names, t, **floats_R):
124 i, N = 0, len(Names)
125 for x in range(0, len(t), N):
126 if t[x] == 'nan':
127 break
128 X = self._Dict(Dict, Names, t[x:x + N], **floats_R)
129 yield X.set_(iteration=i)
130 i += 1
132 @Property_RO
133 def _E_option(self):
134 return ('-E',) if self.Exact else ()
136 @property
137 def Exact(self):
138 '''Get the Solve's C{exact} setting (C{bool}).
139 '''
140 return self._Exact
142 @Exact.setter # PYCHOK setter!
143 def Exact(self, Exact):
144 '''Set the Solve's C{exact} setting (C{bool}),
145 if C{True} use I{exact} version.
146 '''
147 Exact = bool(Exact)
148 if self._Exact != Exact:
149 _update_all(self)
150 self._Exact = Exact
152 @Property_RO
153 def ellipsoid(self):
154 '''Get the ellipsoid (C{Ellipsoid}).
155 '''
156 return self.datum.ellipsoid
158 @Property_RO
159 def _e_option(self):
160 E = self.ellipsoid
161 if E is _EWGS84:
162 return () # default
163 a, f = strs(E.a_f, fmt=Fmt.F, prec=DIG + 3) # not .G!
164 return ('-e', a, f)
166 @Property_RO
167 def flattening(self):
168 '''Get the C{ellipsoid}'s I{flattening} (C{scalar}), M{(a - b) / a},
169 C{0} for spherical, negative for prolate.
170 '''
171 return self.ellipsoid.f
173 f = flattening
175 @property_RO
176 def invokation(self):
177 '''Get the most recent C{Solve} invokation number (C{int}).
178 '''
179 return self._invokation
181 def invoke(self, *options, **stdin):
182 '''Invoke the C{Solve} executable and return the result.
184 @arg options: No, one or several C{Solve} command line
185 options (C{str}s).
186 @kwarg stdin: Optional input to pass to C{Solve.stdin} (C{str}).
188 @return: The C{Solve.stdout} and C{.stderr} output (C{str}).
190 @raise GeodesicError: On any error, including a non-zero return
191 code from C{GeodSolve}.
193 @raise RhumbError: On any error, including a non-zero return code
194 from C{RhumbSolve}.
196 @note: The C{Solve} return code is in property L{status}.
197 '''
198 c = (self._Xable_path,) + map2(str, options) # map2(_enquote, options)
199 i = _xkwds_get1(stdin, stdin=None)
200 r = self._invoke(c, stdin=i)
201 s = self.status
202 if s:
203 raise self._Error(cmd=_cmd_stdin_(c, i), status=s,
204 txt_not_=_0_)
205 if self.verbose: # PYCHOK no cover
206 self._print(r)
207 return r
209 def _invoke(self, cmd, stdin=None, **unused): # _R=None
210 '''(INTERNAL) Invoke the C{Solve} executable, with the
211 given B{C{cmd}} line and optional input to B{C{stdin}}.
212 '''
213 self._invokation += 1
214 self._status = t = None
215 if self.verbose: # PYCHOK no cover
216 t = _cmd_stdin_(cmd, stdin)
217 self._print(t)
218 try: # invoke and write to stdin
219 s, r = _popen2(cmd, stdin)
220 if len(r) < 6 or r[:5] in (_Error_, _ERROR_):
221 raise ValueError(r)
222 except (IOError, OSError, TypeError, ValueError) as x:
223 raise self._Error(cmd=t or _cmd_stdin_(cmd, stdin), cause=x)
224 self._status = s
225 if self.verbose: # and _R is None: # PYCHOK no cover
226 self._print(repr(r))
227 return r
229 def linelimit(self, *limit):
230 '''Set and get the print line length limit.
232 @arg limit: New line limit (C{int}) or C{0}
233 or C{None} for unlimited.
235 @return: Teh previous limit (C{int}).
236 '''
237 n = self._linelimit
238 if limit:
239 m = int(limit[0] or 0)
240 self._linelimit = max(80, m) if m > 0 else (n if m < 0 else 0)
241 return n
243 @Property_RO
244 def _mpd(self): # meter per degree
245 return self.ellipsoid._Lpd
247 @property_RO
248 def _p_option(self):
249 return '-p', str(self.prec - 5) # -p is distance prec
251 @Property
252 def prec(self):
253 '''Get the precision, number of (decimal) digits (C{int}).
254 '''
255 return self._prec
257 @prec.setter # PYCHOK setter!
258 def prec(self, prec):
259 '''Set the precision for C{angles} in C{degrees}, like C{lat}, C{lon},
260 C{azimuth} and C{arc} in number of decimal digits (C{int}, C{0}..L{DIG}).
262 @note: The precision for C{distance = B{prec} - 5} or up to
263 10 decimal digits for C{nanometer} and for C{area =
264 B{prec} - 12} or at most C{millimeter} I{squared}.
265 '''
266 prec = Precision_(prec=prec, high=DIG)
267 if self._prec != prec:
268 _update_all(self)
269 self._prec = prec
271 def _print(self, line): # PYCHOK no cover
272 '''(INTERNAL) Print a status line.
273 '''
274 if self._linelimit:
275 line = clips(line, limit=self._linelimit, length=True)
276 if self.status is not None:
277 line = _SPACE_(line, Fmt.PAREN(self.status))
278 printf('%s@%d: %s', self.named2, self.invokation, line)
280 def _setXable(self, path, **Xable_path):
281 '''(INTERNAL) Set the executable C{path}.
282 '''
283 hold = self._Xable_path
284 if hold != path:
285 _update_all(self)
286 self._Xable_path = path
287 try:
288 _ = self.version # test path and ...
289 if self.status: # ... return code
290 S_p = Xable_path or {self._Xable_name: _enquote(path)}
291 raise self._Error(status=self.status, txt_not_=_0_, **S_p)
292 hold = path
293 finally: # restore in case of error
294 if self._Xable_path != hold:
295 _update_all(self)
296 self._Xable_path = hold
298 @property_RO
299 def status(self):
300 '''Get the most recent C{Solve} return code (C{int}, C{str})
301 or C{None}.
302 '''
303 return self._status
305 @property
306 def verbose(self):
307 '''Get the C{verbose} option (C{bool}).
308 '''
309 return self._verbose
311 @verbose.setter # PYCHOK setter!
312 def verbose(self, verbose):
313 '''Set the C{verbose} option (C{bool}), C{True} prints
314 a message around each C{RhumbSolve} invokation.
315 '''
316 self._verbose = bool(verbose)
318 @Property_RO
319 def version(self):
320 '''Get the result of C{"GeodSolve --version"} or C{"RhumbSolve --version"}.
321 '''
322 return self.invoke('--version')
325class _SolveBase(_SolveCapsBase):
326 '''(INTERNAL) Base class for C{_SolveBase} and C{_SolveLineBase}.
327 '''
328 _Names_Direct = \
329 _Names_Inverse = ()
330 _reverse2 = False
331 _unroll = False
333 @Property
334 def reverse2(self):
335 '''Get the C{azi2} direction (C{bool}).
336 '''
337 return self._reverse2
339 @reverse2.setter # PYCHOK setter!
340 def reverse2(self, reverse2):
341 '''Set the direction for C{azi2} (C{bool}), if C{True} reverse C{azi2}.
342 '''
343 reverse2 = bool(reverse2)
344 if self._reverse2 != reverse2:
345 _update_all(self)
346 self._reverse2 = reverse2
348 @Property
349 def unroll(self):
350 '''Get the C{lon2} unroll'ing (C{bool}).
351 '''
352 return self._unroll
354 @unroll.setter # PYCHOK setter!
355 def unroll(self, unroll):
356 '''Set unroll'ing for C{lon2} (C{bool}), if C{True} unroll C{lon2}, otherwise don't.
357 '''
358 unroll = bool(unroll)
359 if self._unroll != unroll:
360 _update_all(self)
361 self._unroll = unroll
364class _SolveGDictBase(_SolveBase):
365 '''(NTERNAL) Base class for C{_GeodesicSolveBase} and C{_RhumbSolveBase}.
366 '''
368 def __init__(self, a_ellipsoid=_EWGS84, f=None, path=NN, **name):
369 '''New C{Solve} instance.
371 @arg a_ellipsoid: An ellipsoid (L{Ellipsoid}) or datum (L{Datum}) or
372 the equatorial radius of the ellipsoid (C{scalar},
373 conventionally in C{meter}), see B{C{f}}.
374 @arg f: The flattening of the ellipsoid (C{scalar}) if B{C{a_ellipsoid}}
375 is specified as C{scalar}.
376 @kwarg path: Optionally, the (fully qualified) path to the C{GeodSolve}
377 or C{RhumbSolve} executable (C{filename}).
378 @kwarg name: Optional C{B{name}=NN} (C{str}).
380 @raise TypeError: Invalid B{C{a_ellipsoid}} or B{C{f}}.
381 '''
382 _earth_datum(self, a_ellipsoid, f=f, **name)
383 if name:
384 self.name = name
385 if path:
386 self._setXable(path)
388 @Property_RO
389 def _cmdDirect(self):
390 '''(INTERNAL) Get the C{Solve} I{Direct} cmd (C{tuple}).
391 '''
392 return self._cmdBasic
394 @Property_RO
395 def _cmdInverse(self):
396 '''(INTERNAL) Get the C{Solve} I{Inverse} cmd (C{tuple}).
397 '''
398 return self._cmdBasic + ('-i',)
400 def Direct(self, lat1, lon1, azi1, s12, outmask=_UNUSED_): # PYCHOK unused
401 '''Return the C{Direct} result.
402 '''
403 return self._GDictDirect(lat1, lon1, azi1, False, s12)
405 def _GDictDirect(self, lat, lon, azi, arcmode, s12_a12, outmask=_UNUSED_, **floats): # PYCHOK for .geodesicx.gxarea
406 '''(INTERNAL) Get C{_GenDirect}-like result as C{GDict}.
407 '''
408 if arcmode:
409 raise self._Error(arcmode=arcmode, txt=str(NotImplemented))
410 return self._GDictInvoke(self._cmdDirect, self._Names_Direct,
411 lat, lon, azi, s12_a12, **floats)
413 def _GDictInverse(self, lat1, lon1, lat2, lon2, outmask=_UNUSED_, **floats): # PYCHOK for .geodesicx.gxarea
414 '''(INTERNAL) Get C{_GenInverse}-like result as C{GDict}, but I{without} C{_S_CALPs_}.
415 '''
416 return self._GDictInvoke(self._cmdInverse, self._Names_Inverse,
417 lat1, lon1, lat2, lon2, **floats)
419 def _GDictInvoke(self, cmd, Names, *args, **floats):
420 '''(INTERNAL) Invoke C{Solve}, return results as C{Dict}.
421 '''
422 return self._DictInvoke2(cmd, Names, GDict, args, **floats)[0] # _R
424 def Inverse(self, lat1, lon1, lat2, lon2, outmask=_UNUSED_): # PYCHOK unused
425 '''Return the C{Inverse} result.
426 '''
427 return self._GDictInverse(lat1, lon1, lat2, lon2)
429 def Inverse1(self, lat1, lon1, lat2, lon2, wrap=False):
430 '''Return the non-negative, I{angular} distance in C{degrees}.
431 '''
432 # see .FrechetKarney.distance, .HausdorffKarney._distance
433 # and .HeightIDWkarney._distances
434 _, lon2 = unroll180(lon1, lon2, wrap=wrap) # self.LONG_UNROLL
435 r = self._GDictInverse(lat1, lon1, lat2, lon2, floats=False)
436 # XXX self.DISTANCE needed for 'a12'?
437 return abs(float(r.a12))
439 def _toStr(self, prec=6, sep=_COMMASPACE_, **Solve): # PYCHOK signature
440 '''(INTERNAL) Return this C{_Solve} as string..
441 '''
442 d = dict(ellipsoid=self.ellipsoid, invokation=self.invokation,
443 status=self.status, **Solve)
444 return sep.join(pairs(d, prec=prec))
447class _SolveGDictLineBase(_SolveGDictBase):
448 '''(NTERNAL) Base class for C{GeodesicLineSolve} and C{RhumbLineSolve}.
449 '''
450# _caps = 0
451# _lla1 = {}
452 _solve = None # L{GeodesicSolve} or L{RhumbSolve} instance
454 def __init__(self, solve, lat1, lon1, caps, **azi_name):
455 name, azi = _name2__(azi_name, _or_nameof=solve)
456 if name:
457 self.name = name
459 self._caps = caps | Caps._LINE
460 self._debug = solve._debug & Caps._DEBUG_ALL
461 self._lla1 = GDict(lat1=lat1, lon1=lon1, **azi)
462 self._solve = solve
464 @Property_RO
465 def _cmdDistance(self):
466 '''(INTERNAL) Get the C{GeodSolve} I{-L} cmd (C{tuple}).
467 '''
468 def _lla3(lat1=0, lon1=0, **azi):
469 _, azi = _xkwds_item2(azi)
470 return lat1, lon1, azi
472 t = strs(_lla3(**self._lla1), prec=DIG, fmt=Fmt.F) # self._solve.prec
473 return self._cmdBasic + ('-L',) + t
475 @property_RO
476 def datum(self):
477 '''Get the datum (C{Datum}).
478 '''
479 return self._solve.datum
481 @property_RO
482 def ellipsoid(self):
483 '''Get the ellipsoid (C{Ellipsoid}).
484 '''
485 return self._solve.ellipsoid
487 @Property_RO
488 def lat1(self):
489 '''Get the latitude of the first point (C{degrees}).
490 '''
491 return self._lla1.lat1
493 @Property_RO
494 def lon1(self):
495 '''Get the longitude of the first point (C{degrees}).
496 '''
497 return self._lla1.lon1
499 def _toStr(self, prec=6, sep=_COMMASPACE_, **solve): # PYCHOK signature
500 '''(INTERNAL) Return this C{_LineSolve} as string..
501 '''
502 d = dict(ellipsoid=self.ellipsoid, invokation=self._solve.invokation,
503 lat1=self.lat1, lon1=self.lon1,
504 status=self._solve.status, **solve)
505 return sep.join(pairs(d, prec=prec))
508__all__ += _ALL_DOCS(_SolveBase, _SolveCapsBase, _SolveGDictBase, _SolveGDictLineBase)
510# **) MIT License
511#
512# Copyright (C) 2016-2024 -- mrJean1 at Gmail -- All Rights Reserved.
513#
514# Permission is hereby granted, free of charge, to any person obtaining a
515# copy of this software and associated documentation files (the "Software"),
516# to deal in the Software without restriction, including without limitation
517# the rights to use, copy, modify, merge, publish, distribute, sublicense,
518# and/or sell copies of the Software, and to permit persons to whom the
519# Software is furnished to do so, subject to the following conditions:
520#
521# The above copyright notice and this permission notice shall be included
522# in all copies or substantial portions of the Software.
523#
524# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
525# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
526# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
527# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
528# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
529# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
530# OTHER DEALINGS IN THE SOFTWARE.