Coverage for pygeodesy/solveBase.py: 95%

203 statements  

« prev     ^ index     » next       coverage.py v7.2.2, created at 2023-04-05 15:46 -0400

1 

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

3 

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

5''' 

6 

7from pygeodesy.basics import map2, ub2str, _zip 

8from pygeodesy.constants import DIG 

9from pygeodesy.errors import _AssertionError, _xkwds_get 

10from pygeodesy.interns import NN, _0_, _BACKSLASH_, _COMMASPACE_, _enquote, \ 

11 _EQUAL_, _Error_, _not_, _SPACE_ 

12from pygeodesy.karney import Caps, _CapsBase, _a_ellipsoid, _EWGS84, GDict, \ 

13 Precision_, unroll180 

14from pygeodesy.lazily import _ALL_DOCS, printf, _sys_version_info2 

15from pygeodesy.named import callername, notOverloaded 

16from pygeodesy.props import Property, Property_RO, property_RO, _update_all 

17from pygeodesy.streprs import Fmt, fstr, fstrzs, pairs, strs 

18# from pygeodesy.units import Precision_ # from .karney 

19# from pygeodesy.utily import unroll180 # from .karney 

20 

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

22 

23__all__ = () # nothing public 

24__version__ = '22.10.04' 

25 

26_ERROR_ = 'ERROR' 

27_text_True = dict() if _sys_version_info2 < (3, 7) else dict(text=True) 

28 

29 

30def _cmd_stdin_(cmd, stdin): # PYCHOK no cover 

31 '''(INTERNAL) Cmd line, stdin and caller as sC{str}. 

32 ''' 

33 c = Fmt.PAREN(callername(up=3)) 

34 t = (c,) if stdin is None else (_BACKSLASH_, str(stdin), c) 

35 return _SPACE_.join(cmd + t) 

36 

37 

38def _popen2(cmd, stdin=None): # in .mgrs, .test.base, .test.testMgrs 

39 '''(INTERNAL) Invoke C{B{cmd} tuple} and return C{exitcode} 

40 and all output to C{stdout/-err}. 

41 ''' 

42 p = _Popen(cmd, creationflags=0, 

43 # executable=sys.executable, shell=True, 

44 stdin=_PIPE, stdout=_PIPE, stderr=_STDOUT, 

45 **_text_True) # PYCHOK kwArgs 

46 r = p.communicate(stdin)[0] 

47 return p.returncode, ub2str(r).strip() 

48 

49 

50class _SolveLineSolveBase(_CapsBase): 

51 '''(NTERNAL) Base class for C{_Solve} and C{_LineSolve}. 

52 ''' 

53 _E = _EWGS84 

54 _Error = None 

55 _Exact = True 

56 _invokation = 0 

57 _Names_Direct = \ 

58 _Names_Inverse = () 

59 _prec = Precision_(prec=DIG) 

60 _reverse2 = False 

61 _Solve_name = NN # executable basename 

62 _Solve_path = NN # executable path 

63 _status = None 

64 _unroll = False 

65 _verbose = False 

66 

67 @Property_RO 

68 def a(self): 

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

70 ''' 

71 return self.ellipsoid.a 

72 

73 @property_RO 

74 def _cmdBasic(self): # PYCHOK no cover 

75 '''(INTERNAL) I{Must be overloaded}, see function C{notOverloaded}. 

76 ''' 

77 notOverloaded(self) 

78 

79 @Property_RO 

80 def ellipsoid(self): 

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

82 ''' 

83 return self._E 

84 

85 @Property_RO 

86 def _e_option(self): 

87 E = self.ellipsoid 

88 if E is _EWGS84: 

89 return () # default 

90 a, f = strs(E.a_f, fmt=Fmt.F, prec=DIG + 3) # not .G! 

91 return ('-e', a, f) 

92 

93 @property 

94 def Exact(self): 

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

96 ''' 

97 return self._Exact 

98 

99 @Exact.setter # PYCHOK setter! 

100 def Exact(self, Exact): 

101 '''Set the Solve's C{exact} setting (C{bool}), 

102 if C{True} use I{exact} version. 

103 ''' 

104 Exact = bool(Exact) 

105 if self._Exact != Exact: 

106 _update_all(self) 

107 self._Exact = Exact 

108 

109 @Property_RO 

110 def f(self): 

111 '''Get the ellipsoid's I{flattening} (C{float}), M{(a - b) / a}, C{0} for spherical, negative for prolate. 

112 ''' 

113 return self.ellipsoid.f 

114 

115 def _GDictInvoke(self, cmd, floats, Names, *args): 

116 '''(INTERNAL) Invoke C{Solve}, return results as C{GDict}. 

117 ''' 

118 N = len(Names) 

119 if N < 1: 

120 raise _AssertionError(cmd=cmd, Names=Names) 

121 i = fstr(args, prec=DIG, fmt=Fmt.F, sep=_SPACE_) if args else None # not Fmt.G! 

122 t = self._invoke(cmd, stdin=i).lstrip().split() # 12-/+ tuple 

123 if len(t) > N: # PYCHOK no cover 

124 # unzip instrumented name=value pairs to names and values 

125 n, v = _zip(*(p.split(_EQUAL_) for p in t[:-N])) # strict=True 

126 v += tuple(t[-N:]) 

127 n += Names 

128 else: 

129 n, v = Names, t 

130 if self.verbose: # PYCHOK no cover 

131 self._print(_COMMASPACE_.join(map(Fmt.EQUAL, n, map(fstrzs, v)))) 

132 if floats: 

133 v = map(float, v) 

134 r = GDict(_zip(n, v)) # strict=True 

135 return self._iter2tion(r, r) 

136 

137 @property_RO 

138 def invokation(self): 

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

140 ''' 

141 return self._invokation 

142 

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

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

145 

146 @arg options: No, one or several C{Solve} command line 

147 options (C{str}s). 

148 @kwarg stdin: Optional input to pass to C{Solve.stdin} (C{str}). 

149 

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

151 

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

153 code from C{GeodSolve}. 

154 

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

156 from C{RhumbSolve}. 

157 

158 @note: The C{Solve} return code is in property L{status}. 

159 ''' 

160 c = (self._Solve_path,) + map2(str, options) 

161 i = _xkwds_get(stdin, stdin=None) 

162 r = self._invoke(c, stdin=i) 

163 s = self.status 

164 if s: 

165 raise self._Error(cmd=_cmd_stdin_(c, i), status=s, 

166 txt=_not_(_0_)) 

167 if self.verbose: # PYCHOK no cover 

168 self._print(r) 

169 return r 

170 

171 def _invoke(self, cmd, stdin=None): 

172 '''(INTERNAL) Invoke the C{Solve} executable, with the 

173 given B{C{cmd}} line and optional input to B{C{stdin}}. 

174 ''' 

175 self._invokation += 1 

176 self._status = t = None 

177 if self.verbose: # PYCHOK no cover 

178 t = _cmd_stdin_(cmd, stdin) 

179 self._print(t) 

180 try: # invoke and write to stdin 

181 s, r = _popen2(cmd, stdin) 

182 if len(r) < 6 or r[:5] in (_Error_, _ERROR_): 

183 raise ValueError(r) 

184 except (IOError, OSError, TypeError, ValueError) as x: 

185 raise self._Error(cmd=t or _cmd_stdin_(cmd, stdin), cause=x) 

186 self._status = s 

187 return r 

188 

189 @property_RO 

190 def _p_option(self): 

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

192 

193 @Property 

194 def prec(self): 

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

196 ''' 

197 return self._prec 

198 

199 @prec.setter # PYCHOK setter! 

200 def prec(self, prec): 

201 '''Set the precision for C{angles} in C{degrees}, like C{lat}, C{lon}, 

202 C{azimuth} and C{arc} in number of decimal digits (C{int}, C{0}..L{DIG}). 

203 

204 @note: The precision for C{distance = B{prec} - 5} or up to 

205 10 decimal digits for C{nanometer} and for C{area = 

206 B{prec} - 12} or at most C{millimeter} I{squared}. 

207 ''' 

208 prec = Precision_(prec=prec, high=DIG) 

209 if self._prec != prec: 

210 _update_all(self) 

211 self._prec = prec 

212 

213 def _print(self, line): # PYCHOK no cover 

214 '''(INTERNAL) Print a status line. 

215 ''' 

216 if self.status is not None: 

217 line = _SPACE_(line, Fmt.PAREN(self.status)) 

218 printf('%s %d: %s', self.named2, self.invokation, line) 

219 

220 @Property 

221 def reverse2(self): 

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

223 ''' 

224 return self._reverse2 

225 

226 @reverse2.setter # PYCHOK setter! 

227 def reverse2(self, reverse2): 

228 '''Set the direction for C{azi2} (C{bool}), if C{True} reverse C{azi2}. 

229 ''' 

230 reverse2 = bool(reverse2) 

231 if self._reverse2 != reverse2: 

232 _update_all(self) 

233 self._reverse2 = reverse2 

234 

235 def _setSolve(self, path, **Solve_path): 

236 '''(INTERNAL) Set the executable C{path}. 

237 ''' 

238 hold = self._Solve_path 

239 if hold != path: 

240 _update_all(self) 

241 self._Solve_path = path 

242 try: 

243 _ = self.version # test path and ... 

244 if self.status: # ... return code 

245 S_p = Solve_path or {self._Solve_name: _enquote(path)} 

246 raise self._Error(status=self.status, txt=_not_(_0_), **S_p) 

247 hold = path 

248 finally: # restore in case of error 

249 if self._Solve_path != hold: 

250 _update_all(self) 

251 self._Solve_path = hold 

252 

253 @property_RO 

254 def status(self): 

255 '''Get the most recent C{Solve} return code (C{int}, C{str}) 

256 or C{None}. 

257 ''' 

258 return self._status 

259 

260 @Property 

261 def unroll(self): 

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

263 ''' 

264 return self._unroll 

265 

266 @unroll.setter # PYCHOK setter! 

267 def unroll(self, unroll): 

268 '''Set unroll'ing for C{lon2} (C{bool}), if C{True} unroll C{lon2}, otherwise don't. 

269 ''' 

270 unroll = bool(unroll) 

271 if self._unroll != unroll: 

272 _update_all(self) 

273 self._unroll = unroll 

274 

275 @property 

276 def verbose(self): 

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

278 ''' 

279 return self._verbose 

280 

281 @verbose.setter # PYCHOK setter! 

282 def verbose(self, verbose): 

283 '''Set the C{verbose} option (C{bool}), C{True} prints 

284 a message around each C{RhumbSolve} invokation. 

285 ''' 

286 self._verbose = bool(verbose) 

287 

288 @Property_RO 

289 def version(self): 

290 '''Get the result of C{"GeodSolve --version"} or C{"RhumbSolve --version"}. 

291 ''' 

292 return self.invoke('--version') 

293 

294 

295class _SolveBase(_SolveLineSolveBase): 

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

297 ''' 

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

299 '''New C{Solve} instance. 

300 

301 @arg a_ellipsoid: An ellipsoid (L{Ellipsoid}) or datum (L{Datum}) or 

302 the equatorial radius of the ellipsoid (C{scalar}, 

303 conventionally in C{meter}), see B{C{f}}. 

304 @arg f: The flattening of the ellipsoid (C{scalar}) if B{C{a_ellipsoid}} 

305 is specified as C{scalar}. 

306 @kwarg path: Optionally, the (fully qualified) path to the C{GeodSolve} 

307 or C{RhumbSolve} executable (C{filename}). 

308 @kwarg name: Optional name (C{str}). 

309 

310 @raise TypeError: Invalid B{C{a_ellipsoid}} or B{C{f}}. 

311 ''' 

312 if a_ellipsoid not in (self._E, None): # NOT self.ellipsoid 

313 self._E = _a_ellipsoid(a_ellipsoid, f, name=name) 

314 if name: 

315 self.name = name 

316 if path: 

317 self._setSolve(path) 

318 

319 @Property_RO 

320 def _cmdDirect(self): 

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

322 ''' 

323 return self._cmdBasic 

324 

325 @Property_RO 

326 def _cmdInverse(self): 

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

328 ''' 

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

330 

331 def Direct(self, lat1, lon1, azi1, s12, *unused): 

332 '''Return the C{Direct} result. 

333 ''' 

334 return self._GDictDirect(lat1, lon1, azi1, False, s12) 

335 

336 def _GDictDirect(self, lat, lon, azi, arcmode, s12_a12, *unused, **floats): # for .geodesicx.gxarea 

337 '''(INTERNAL) Get C{_GenDirect}-like result as C{GDict}. 

338 ''' 

339 if arcmode: 

340 raise self._Error(arcmode=arcmode, txt=str(NotImplemented)) 

341 floats = _xkwds_get(floats, floats=True) 

342 return self._GDictInvoke(self._cmdDirect, floats, self._Names_Direct, 

343 lat, lon, azi, s12_a12) 

344 

345 def _GDictInverse(self, lat1, lon1, lat2, lon2, *unused, **floats): # for .geodesicx.gxarea 

346 '''(INTERNAL) Get C{_GenInverse}-like result as C{GDict}, but 

347 I{without} C{_SALPs_CALPs_}. 

348 ''' 

349 floats = _xkwds_get(floats, floats=True) 

350 return self._GDictInvoke(self._cmdInverse, floats, self._Names_Inverse, 

351 lat1, lon1, lat2, lon2) 

352 

353 def Inverse(self, lat1, lon1, lat2, lon2, *unused): 

354 '''Return the C{Inverse} result. 

355 ''' 

356 return self._GDictInverse(lat1, lon1, lat2, lon2) 

357 

358 def Inverse1(self, lat1, lon1, lat2, lon2, wrap=False): 

359 '''Return the non-negative, I{angular} distance in C{degrees}. 

360 ''' 

361 # see .FrechetKarney.distance, .HausdorffKarney._distance 

362 # and .HeightIDWkarney._distances 

363 _, lon2 = unroll180(lon1, lon2, wrap=wrap) # self.LONG_UNROLL 

364 r = self._GDictInverse(lat1, lon1, lat2, lon2, floats=False) 

365 # XXX self.DISTANCE needed for 'a12'? 

366 return abs(float(r.a12)) 

367 

368 def _toStr(self, prec=6, sep=_COMMASPACE_, **Solve): # PYCHOK signature 

369 '''(INTERNAL) Return this C{_Solve} as string.. 

370 ''' 

371 d = dict(ellipsoid=self.ellipsoid, invokation=self.invokation, 

372 status=self.status, **Solve) 

373 return sep.join(pairs(d, prec=prec)) 

374 

375 

376class _LineSolveBase(_SolveLineSolveBase): 

377 '''(NTERNAL) Base class for C{GeodesicLineSolve} and C{RhumbLineSolve}. 

378 ''' 

379# _caps = 0 

380# _lla1 = {} 

381 _solve = None # L{GeodesicSolve} or L{RhumbSolve} instance 

382 

383 def __init__(self, solve, lat1, lon1, caps, name, **azi): 

384 self._caps = caps | Caps._LINE 

385 self._debug = solve._debug & Caps._DEBUG_ALL 

386 self._lla1 = GDict(lat1=lat1, lon1=lon1, **azi) 

387 self._solve = solve 

388 

389 n = name or solve.name 

390 if n: 

391 self.name = n 

392 

393 @Property_RO 

394 def _cmdDistance(self): 

395 '''(INTERNAL) Get the C{GeodSolve} I{-L} cmd (C{tuple}). 

396 ''' 

397 def _lla3(lat1=0, lon1=0, **azi): 

398 _, azi = azi.popitem() 

399 return lat1, lon1, azi 

400 

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

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

403 

404 @property_RO 

405 def ellipsoid(self): 

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

407 ''' 

408 return self._solve.ellipsoid 

409 

410 @Property_RO 

411 def lat1(self): 

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

413 ''' 

414 return self._lla1.lat1 

415 

416 @Property_RO 

417 def lon1(self): 

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

419 ''' 

420 return self._lla1.lon1 

421 

422 def _toStr(self, prec=6, sep=_COMMASPACE_, **solve): # PYCHOK signature 

423 '''(INTERNAL) Return this C{_LineSolve} as string.. 

424 ''' 

425 d = dict(ellipsoid=self.ellipsoid, invokation=self._solve.invokation, 

426 lat1=self.lat1, lon1=self.lon1, 

427 status=self._solve.status, **solve) 

428 return sep.join(pairs(d, prec=prec)) 

429 

430 

431__all__ += _ALL_DOCS(_SolveBase, _LineSolveBase, _SolveLineSolveBase) 

432 

433# **) MIT License 

434# 

435# Copyright (C) 2016-2023 -- mrJean1 at Gmail -- All Rights Reserved. 

436# 

437# Permission is hereby granted, free of charge, to any person obtaining a 

438# copy of this software and associated documentation files (the "Software"), 

439# to deal in the Software without restriction, including without limitation 

440# the rights to use, copy, modify, merge, publish, distribute, sublicense, 

441# and/or sell copies of the Software, and to permit persons to whom the 

442# Software is furnished to do so, subject to the following conditions: 

443# 

444# The above copyright notice and this permission notice shall be included 

445# in all copies or substantial portions of the Software. 

446# 

447# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 

448# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 

449# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 

450# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR 

451# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 

452# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 

453# OTHER DEALINGS IN THE SOFTWARE.