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