Coverage for pygeodesy/solveBase.py: 91%
243 statements
« prev ^ index » next coverage.py v7.6.1, created at 2025-09-09 13:03 -0400
« prev ^ index » next coverage.py v7.6.1, created at 2025-09-09 13:03 -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, _isin, map2, _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, _popen2, 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
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
25__all__ = _ALL_LAZY.solveBase
26__version__ = '25.09.02'
28_ERROR_ = 'ERROR'
31def _cmd_stdin_(cmd, stdin): # PYCHOK no cover
32 '''(INTERNAL) Cmd line, stdin and caller as sC{str}.
33 '''
34 if stdin is not None:
35 cmd += _BACKSLASH_, str(stdin)
36 cmd += Fmt.PAREN(callername(up=3)),
37 return _SPACE_.join(cmd)
40# def _float_int(r):
41# '''(INTERNAL) Convert result into C{float} or C{int}.
42# '''
43# f = float(r)
44# i = int(f)
45# return i if float(i) == f else f # PYCHOK inconsistent
48class _SolveCapsBase(_CapsBase):
49 '''(NTERNAL) Base class for C{_SolveBase} and C{_LineSolveBase}.
50 '''
51 _datum = _WGS84
52 _Error = None
53 _Exact = True
54 _invokat = _AT_
55 _invokation = 0
56 _linelimit = 0
57 _prec = Precision_(prec=DIG)
58 _prec2stdin = DIG
59 _Xable_name = NN # executable basename
60 _Xable_path = NN # executable path
61 _status = None
62 _verbose = False
64 @Property_RO
65 def a(self):
66 '''Get the ellipsoid's I{equatorial} radius, semi-axis (C{meter}).
67 '''
68 return self.ellipsoid.a
70 @Property_RO
71 def b(self):
72 '''Get the ellipsoid's I{polar} radius, semi-axis (C{meter}).
73 '''
74 return self.ellipsoid.b
76 @property_RO
77 def _cmdBasic(self): # PYCHOK no covers '''(INTERNAL) I{Must be overloaded}.'''
78 notOverloaded(self, underOK=True)
80 @property_RO
81 def datum(self):
82 '''Get the datum (C{Datum}).
83 '''
84 return self._datum
86 def _Dict(self, Dict, n, v, floats=True, **unused):
87 if self.verbose: # PYCHOK no cover
88 self._print(_COMMASPACE_.join(map(Fmt.EQUAL, n, map(fstrzs, v))))
89 if floats:
90 v = map(float, v) # _float_int, see Intersectool._XDistInvoke
91 return Dict(_zip(n, v)) # strict=True
93 def _DictInvoke2(self, cmd, args, Names, Dict, **floats_R):
94 '''(INTERNAL) Invoke C{Solve}, return results as C{Dict}.
95 '''
96 N = len(Names)
97 if N < 1:
98 raise _AssertionError(cmd=cmd, Names=Names)
99 i = fstr(args, prec=self._prec2stdin, fmt=Fmt.F, sep=_SPACE_) if args else None # NOT Fmt.G!
100 t = self._invoke(cmd, stdin=i, **floats_R).lstrip().split() # 12-/++ tuple
101 if _xkwds_get(floats_R, _R=None): # == '-R' in cmd
102 return self._Dicts(Dict, Names, t, **floats_R), True
103 elif len(t) > N: # PYCHOK no cover
104 # unzip instrumented name=value pairs to names and values
105 n, v = _zip(*(p.split(_EQUAL_) for p in t[:-N])) # strict=True
106 v += tuple(t[-N:])
107 n += Names
108 else:
109 n, v = Names, t
110 r = self._Dict(Dict, n, t, **floats_R)
111 return self._iter2tion(r, **r), None
113 def _Dicts(self, Dict, Names, t, **floats_R):
114 i, N = 0, len(Names)
115 for x in range(0, len(t), N):
116 if t[x] == 'nan':
117 break
118 X = self._Dict(Dict, Names, t[x:x + N], **floats_R)
119 yield X.set_(iteration=i)
120 i += 1
122 @Property_RO
123 def _E_option(self):
124 return ('-E',) if self.Exact else ()
126 @property
127 def Exact(self):
128 '''Get the Solve's C{exact} setting (C{bool}).
129 '''
130 return self._Exact
132 @Exact.setter # PYCHOK setter!
133 def Exact(self, Exact):
134 '''Set the Solve's C{exact} setting (C{bool}),
135 if C{True} use I{exact} version.
136 '''
137 Exact = bool(Exact)
138 if self._Exact != Exact:
139 _update_all(self)
140 self._Exact = Exact
142 @Property_RO
143 def ellipsoid(self):
144 '''Get the ellipsoid (C{Ellipsoid}).
145 '''
146 return self.datum.ellipsoid
148 @Property_RO
149 def _e_option(self):
150 E = self.ellipsoid
151 if E is _EWGS84:
152 return () # default
153 a, f = strs(E.a_f, fmt=Fmt.F, prec=DIG + 3) # not .G!
154 return ('-e', a, f)
156 @Property_RO
157 def flattening(self):
158 '''Get the C{ellipsoid}'s I{flattening} (C{scalar}), M{(a - b) / a},
159 C{0} for spherical, negative for prolate.
160 '''
161 return self.ellipsoid.f
163 f = flattening
165 def invokat(self, *prefix):
166 '''Get and set the invokation number C{"@"} prefix (C{str}).
168 @return: Previous prefix (C{str}).
169 '''
170 p = self._invokat
171 if prefix:
172 set._invokat = str(prefix[0])
173 return p
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 r, s = _popen2(cmd, stdin)
220 if len(r) < 6 or _isin(r[:5], _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), 'stdout/-err')
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, *suffix): # 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 s = _COMMASPACE_(self.status, *suffix)
278 line = _SPACE_(line, Fmt.PAREN(s))
279 p = NN(self.named2, self._invokat, self.invokation)
280 printf(_COLONSPACE_(p, line))
282 def _setXable(self, path, **Xable_path):
283 '''(INTERNAL) Set the executable C{path}.
284 '''
285 hold = self._Xable_path
286 if hold != path:
287 _update_all(self)
288 self._Xable_path = path
289 try:
290 _ = self.version # test path and ...
291 if self.status: # ... return code
292 S_p = Xable_path or {self._Xable_name: _enquote(path)}
293 raise self._Error(status=self.status, txt_not_=_0_, **S_p)
294 hold = path
295 finally: # restore in case of error
296 if self._Xable_path != hold:
297 _update_all(self)
298 self._Xable_path = hold
300 @property_RO
301 def status(self):
302 '''Get the most recent C{Solve} return code (C{int}, C{str})
303 or C{None}.
304 '''
305 return self._status
307 @property
308 def verbose(self):
309 '''Get the C{verbose} option (C{bool}).
310 '''
311 return self._verbose
313 @verbose.setter # PYCHOK setter!
314 def verbose(self, verbose):
315 '''Set the C{verbose} option (C{bool}), C{True} prints
316 a message around each C{RhumbSolve} invokation.
317 '''
318 self._verbose = bool(verbose)
320 @Property_RO
321 def version(self):
322 '''Get the result of C{"GeodSolve --version"} or C{"RhumbSolve --version"}.
323 '''
324 return self.invoke('--version')
327class _SolveBase(_SolveCapsBase):
328 '''(INTERNAL) Base class for C{_SolveBase} and C{_SolveLineBase}.
329 '''
330 _Names_Direct = \
331 _Names_Inverse = ()
332 _reverse2 = False
333 _unroll = False
335 @Property
336 def reverse2(self):
337 '''Get the C{azi2} direction (C{bool}).
338 '''
339 return self._reverse2
341 @reverse2.setter # PYCHOK setter!
342 def reverse2(self, reverse2):
343 '''Set the direction for C{azi2} (C{bool}), if C{True} reverse C{azi2}.
344 '''
345 reverse2 = bool(reverse2)
346 if self._reverse2 != reverse2:
347 _update_all(self)
348 self._reverse2 = reverse2
350 @Property
351 def unroll(self):
352 '''Get the C{lon2} unroll'ing (C{bool}).
353 '''
354 return self._unroll
356 @unroll.setter # PYCHOK setter!
357 def unroll(self, unroll):
358 '''Set unroll'ing for C{lon2} (C{bool}), if C{True} unroll C{lon2}, otherwise don't.
359 '''
360 unroll = bool(unroll)
361 if self._unroll != unroll:
362 _update_all(self)
363 self._unroll = unroll
366class _SolveGDictBase(_SolveBase):
367 '''(NTERNAL) Base class for C{_GeodesicSolveBase} and C{_RhumbSolveBase}.
368 '''
370 def __init__(self, a_ellipsoid=_EWGS84, f=None, path=NN, **name):
371 '''New C{Solve} instance.
373 @arg a_ellipsoid: An ellipsoid (L{Ellipsoid}) or datum (L{Datum}) or
374 the equatorial radius of the ellipsoid (C{scalar},
375 conventionally in C{meter}), see B{C{f}}.
376 @arg f: The flattening of the ellipsoid (C{scalar}) if B{C{a_ellipsoid}}
377 is specified as C{scalar}.
378 @kwarg path: Optionally, the (fully qualified) path to the C{GeodSolve}
379 or C{RhumbSolve} executable (C{filename}).
380 @kwarg name: Optional C{B{name}=NN} (C{str}).
382 @raise TypeError: Invalid B{C{a_ellipsoid}} or B{C{f}}.
383 '''
384 _earth_datum(self, a_ellipsoid, f=f, **name)
385 if name:
386 self.name = name
387 if path:
388 self._setXable(path)
390 def ArcDirect(self, lat1, lon1, azi1, a12, outmask=_UNUSED_): # PYCHOK unused
391 '''Return the C{Direct} result at C{a12} degrees.
392 '''
393 return self._GDictDirect(lat1, lon1, azi1, True, a12)
395 @Property_RO
396 def _cmdDirect(self):
397 '''(INTERNAL) Get the C{Solve} I{Direct} cmd (C{tuple}).
398 '''
399 return self._cmdBasic
401 @Property_RO
402 def _cmdInverse(self):
403 '''(INTERNAL) Get the C{Solve} I{Inverse} cmd (C{tuple}).
404 '''
405 return self._cmdBasic + ('-i',)
407 def Direct(self, lat1, lon1, azi1, s12, outmask=_UNUSED_): # PYCHOK unused
408 '''Return the C{Direct} result at distance C{s12}.
409 '''
410 return self._GDictDirect(lat1, lon1, azi1, False, s12)
412 def _GDictDirect(self, lat, lon, azi, arcmode, s12_a12, outmask=_UNUSED_, **floats): # PYCHOK for .geodesicx.gxarea
413 '''(INTERNAL) Get C{_GenDirect}-like result as C{GDict}.
414 '''
415 if arcmode:
416 raise self._Error(arcmode=arcmode, txt=str(NotImplemented))
417 return self._GDictInvoke(self._cmdDirect, self._Names_Direct,
418 lat, lon, azi, s12_a12, **floats)
420 def _GDictInverse(self, lat1, lon1, lat2, lon2, outmask=_UNUSED_, **floats): # PYCHOK for .geodesicx.gxarea
421 '''(INTERNAL) Get C{_GenInverse}-like result as C{GDict}, but I{without} C{_SALP_CALPs_}.
422 '''
423 return self._GDictInvoke(self._cmdInverse, self._Names_Inverse,
424 lat1, lon1, lat2, lon2, **floats)
426 def _GDictInvoke(self, cmd, Names, *args, **floats):
427 '''(INTERNAL) Invoke C{Solve}, return results as C{Dict}.
428 '''
429 return self._DictInvoke2(cmd, args, Names, GDict, **floats)[0] # _R
431 def Inverse(self, lat1, lon1, lat2, lon2, outmask=_UNUSED_): # PYCHOK unused
432 '''Return the C{Inverse} result.
433 '''
434 return self._GDictInverse(lat1, lon1, lat2, lon2)
436 def Inverse1(self, lat1, lon1, lat2, lon2, wrap=False):
437 '''Return the non-negative, I{angular} distance in C{degrees}.
438 '''
439 # see .FrechetKarney.distance, .HausdorffKarney._distance
440 # and .HeightIDWkarney._distances
441 _, lon2 = unroll180(lon1, lon2, wrap=wrap) # self.LONG_UNROLL
442 r = self._GDictInverse(lat1, lon1, lat2, lon2, floats=False)
443 # XXX self.DISTANCE needed for 'a12'?
444 return abs(float(r.a12))
446 def _toStr(self, prec=6, sep=_COMMASPACE_, **Solve): # PYCHOK signature
447 '''(INTERNAL) Return this C{_Solve} as string..
448 '''
449 d = dict(ellipsoid=self.ellipsoid, invokation=self.invokation,
450 status=self.status, **Solve)
451 return sep.join(pairs(d, prec=prec))
454class _SolveGDictLineBase(_SolveGDictBase):
455 '''(NTERNAL) Base class for C{GeodesicLineSolve} and C{RhumbLineSolve}.
456 '''
457# _caps = 0
458# _lla1 = {}
459 _solve = None # L{GeodesicSolve} or L{RhumbSolve} instance
461 def __init__(self, solve, lat1, lon1, caps, **azi_name):
462 name, azi = _name2__(azi_name, _or_nameof=solve)
463 if name:
464 self.name = name
466 self._caps = caps | Caps._AZIMUTH_LATITUDE_LONG_UNROLL
467 self._debug = solve._debug & Caps._DEBUG_ALL
468 self._lla1 = GDict(lat1=lat1, lon1=lon1, **azi)
469 self._solve = solve
471 @Property_RO
472 def _cmdDistance(self):
473 '''(INTERNAL) Get the C{GeodSolve} I{-L} cmd (C{tuple}).
474 '''
475 def _lla3(lat1=0, lon1=0, **azi):
476 _, azi = _xkwds_item2(azi)
477 return lat1, lon1, azi
479 t = strs(_lla3(**self._lla1), prec=DIG, fmt=Fmt.F) # self._solve.prec
480 return self._cmdBasic + ('-L',) + t
482 @property_RO
483 def datum(self):
484 '''Get the datum (C{Datum}).
485 '''
486 return self._solve.datum
488 @property_RO
489 def ellipsoid(self):
490 '''Get the ellipsoid (C{Ellipsoid}).
491 '''
492 return self._solve.ellipsoid
494 @Property_RO
495 def lat1(self):
496 '''Get the latitude of the first point (C{degrees}).
497 '''
498 return self._lla1.lat1
500 @Property_RO
501 def lon1(self):
502 '''Get the longitude of the first point (C{degrees}).
503 '''
504 return self._lla1.lon1
506 def _toStr(self, prec=6, sep=_COMMASPACE_, **solve): # PYCHOK signature
507 '''(INTERNAL) Return this C{_LineSolve} as string..
508 '''
509 d = dict(ellipsoid=self.ellipsoid, invokation=self._solve.invokation,
510 lat1=self.lat1, lon1=self.lon1,
511 status=self._solve.status, **solve)
512 return sep.join(pairs(d, prec=prec))
515__all__ += _ALL_DOCS(_SolveBase, _SolveCapsBase, _SolveGDictBase, _SolveGDictLineBase)
517# **) MIT License
518#
519# Copyright (C) 2016-2025 -- mrJean1 at Gmail -- All Rights Reserved.
520#
521# Permission is hereby granted, free of charge, to any person obtaining a
522# copy of this software and associated documentation files (the "Software"),
523# to deal in the Software without restriction, including without limitation
524# the rights to use, copy, modify, merge, publish, distribute, sublicense,
525# and/or sell copies of the Software, and to permit persons to whom the
526# Software is furnished to do so, subject to the following conditions:
527#
528# The above copyright notice and this permission notice shall be included
529# in all copies or substantial portions of the Software.
530#
531# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
532# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
533# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
534# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
535# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
536# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
537# OTHER DEALINGS IN THE SOFTWARE.