Coverage for pygeodesy/solveBase.py: 95%

219 statements  

« prev     ^ index     » next       coverage.py v7.2.2, created at 2024-06-10 14:08 -0400

1 

2# -*- coding: utf-8 -*- 

3 

4u'''(INTERNAL) Private base classes for L{pygeodesy.geodsolve} and L{pygeodesy.rhumb.solve}. 

5''' 

6 

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 

22 

23from subprocess import PIPE as _PIPE, Popen as _Popen, STDOUT as _STDOUT 

24 

25__all__ = _ALL_LAZY.solveBase 

26__version__ = '24.06.05' 

27 

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 

35 

36 

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) 

44 

45 

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() 

53 

54 

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 

70 

71 @property_RO 

72 def _cmdBasic(self): # PYCHOK no cover 

73 '''(INTERNAL) I{Must be overloaded}.''' 

74 notOverloaded(self, underOK=True) 

75 

76 @property 

77 def Exact(self): 

78 '''Get the Solve's C{exact} setting (C{bool}). 

79 ''' 

80 return self._Exact 

81 

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 

91 

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) 

113 

114 @property_RO 

115 def invokation(self): 

116 '''Get the most recent C{Solve} invokation number (C{int}). 

117 ''' 

118 return self._invokation 

119 

120 def invoke(self, *options, **stdin): 

121 '''Invoke the C{Solve} executable and return the result. 

122 

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}). 

126 

127 @return: The C{Solve.stdout} and C{.stderr} output (C{str}). 

128 

129 @raise GeodesicError: On any error, including a non-zero return 

130 code from C{GeodSolve}. 

131 

132 @raise RhumbError: On any error, including a non-zero return code 

133 from C{RhumbSolve}. 

134 

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 

147 

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 

165 

166 @Property_RO 

167 def _mpd(self): # meter per degree 

168 return self.ellipsoid._Lpd 

169 

170 @property_RO 

171 def _p_option(self): 

172 return '-p', str(self.prec - 5) # -p is distance prec 

173 

174 @Property 

175 def prec(self): 

176 '''Get the precision, number of (decimal) digits (C{int}). 

177 ''' 

178 return self._prec 

179 

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}). 

184 

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 

193 

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) 

200 

201 @Property 

202 def reverse2(self): 

203 '''Get the C{azi2} direction (C{bool}). 

204 ''' 

205 return self._reverse2 

206 

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 

215 

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 

233 

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 

240 

241 @Property 

242 def unroll(self): 

243 '''Get the C{lon2} unroll'ing (C{bool}). 

244 ''' 

245 return self._unroll 

246 

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 

255 

256 @property 

257 def verbose(self): 

258 '''Get the C{verbose} option (C{bool}). 

259 ''' 

260 return self._verbose 

261 

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) 

268 

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') 

274 

275 

276class _SolveBase(_SolveCapsBase): 

277 '''(NTERNAL) Base class for C{_GeodesicSolveBase} and C{_RhumbSolveBase}. 

278 ''' 

279 _datum = _WGS84 

280 

281 def __init__(self, a_ellipsoid=_EWGS84, f=None, path=NN, **name): 

282 '''New C{Solve} instance. 

283 

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}). 

292 

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) 

300 

301 @Property_RO 

302 def a(self): 

303 '''Get the I{equatorial} radius, semi-axis (C{meter}). 

304 ''' 

305 return self.ellipsoid.a 

306 

307 @Property_RO 

308 def _cmdDirect(self): 

309 '''(INTERNAL) Get the C{Solve} I{Direct} cmd (C{tuple}). 

310 ''' 

311 return self._cmdBasic 

312 

313 @Property_RO 

314 def _cmdInverse(self): 

315 '''(INTERNAL) Get the C{Solve} I{Inverse} cmd (C{tuple}). 

316 ''' 

317 return self._cmdBasic + ('-i',) 

318 

319 @property_RO 

320 def datum(self): 

321 '''Get the datum (C{Datum}). 

322 ''' 

323 return self._datum 

324 

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) 

329 

330 @Property_RO 

331 def ellipsoid(self): 

332 '''Get the ellipsoid (C{Ellipsoid}). 

333 ''' 

334 return self.datum.ellipsoid 

335 

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) 

343 

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 

350 

351 f = flattening 

352 

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) 

361 

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) 

369 

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) 

374 

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)) 

384 

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)) 

391 

392 

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 

399 

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 

404 

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 

409 

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 

417 

418 t = strs(_lla3(**self._lla1), prec=DIG, fmt=Fmt.F) # self._solve.prec 

419 return self._cmdBasic + ('-L',) + t 

420 

421 @property_RO 

422 def datum(self): 

423 '''Get the datum (C{Datum}). 

424 ''' 

425 return self._solve.datum 

426 

427 @property_RO 

428 def ellipsoid(self): 

429 '''Get the ellipsoid (C{Ellipsoid}). 

430 ''' 

431 return self._solve.ellipsoid 

432 

433 @Property_RO 

434 def lat1(self): 

435 '''Get the latitude of the first point (C{degrees}). 

436 ''' 

437 return self._lla1.lat1 

438 

439 @Property_RO 

440 def lon1(self): 

441 '''Get the longitude of the first point (C{degrees}). 

442 ''' 

443 return self._lla1.lon1 

444 

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)) 

452 

453 

454__all__ += _ALL_DOCS(_SolveBase, _SolveLineBase, _SolveCapsBase) 

455 

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.