Coverage for pygeodesy/solveBase.py: 94%

216 statements  

« prev     ^ index     » next       coverage.py v7.2.2, created at 2024-05-25 12:04 -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_get, _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, _unlazy 

17from pygeodesy.named import callername, 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.05.20' 

27 

28_ERROR_ = 'ERROR' 

29_text_True = dict() if _unlazy else dict(text=True) 

30 

31 

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

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

34 ''' 

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

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

37 return _SPACE_.join(cmd + t) 

38 

39 

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

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

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

43 ''' 

44 p = _Popen(cmd, creationflags=0, 

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

46 stdin=_PIPE, stdout=_PIPE, stderr=_STDOUT, 

47 **_text_True) # PYCHOK kwArgs 

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

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

50 

51 

52class _SolveLineSolveBase(_CapsBase): 

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

54 ''' 

55 _Error = None 

56 _Exact = True 

57 _invokation = 0 

58 _Names_Direct = \ 

59 _Names_Inverse = () 

60 _prec = Precision_(prec=DIG) 

61 _reverse2 = False 

62 _Solve_name = NN # executable basename 

63 _Solve_path = NN # executable path 

64 _status = None 

65 _unroll = False 

66 _verbose = False 

67 

68 @property_RO 

69 def _cmdBasic(self): # PYCHOK no cover 

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

71 notOverloaded(self, underOK=True) 

72 

73 @property 

74 def Exact(self): 

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

76 ''' 

77 return self._Exact 

78 

79 @Exact.setter # PYCHOK setter! 

80 def Exact(self, Exact): 

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

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

83 ''' 

84 Exact = bool(Exact) 

85 if self._Exact != Exact: 

86 _update_all(self) 

87 self._Exact = Exact 

88 

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

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

91 ''' 

92 N = len(Names) 

93 if N < 1: 

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

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

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

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

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

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

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

101 n += Names 

102 else: 

103 n, v = Names, t 

104 if self.verbose: # PYCHOK no cover 

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

106 if floats: 

107 v = map(float, v) 

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

109 return self._iter2tion(r, **r) 

110 

111 @property_RO 

112 def invokation(self): 

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

114 ''' 

115 return self._invokation 

116 

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

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

119 

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

121 options (C{str}s). 

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

123 

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

125 

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

127 code from C{GeodSolve}. 

128 

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

130 from C{RhumbSolve}. 

131 

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

133 ''' 

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

135 i = _xkwds_get(stdin, stdin=None) 

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

137 s = self.status 

138 if s: 

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

140 txt_not_=_0_) 

141 if self.verbose: # PYCHOK no cover 

142 self._print(r) 

143 return r 

144 

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

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

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

148 ''' 

149 self._invokation += 1 

150 self._status = t = None 

151 if self.verbose: # PYCHOK no cover 

152 t = _cmd_stdin_(cmd, stdin) 

153 self._print(t) 

154 try: # invoke and write to stdin 

155 s, r = _popen2(cmd, stdin) 

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

157 raise ValueError(r) 

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

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

160 self._status = s 

161 return r 

162 

163 @Property_RO 

164 def _mpd(self): # meter per degree 

165 return self.ellipsoid._Lpd 

166 

167 @property_RO 

168 def _p_option(self): 

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

170 

171 @Property 

172 def prec(self): 

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

174 ''' 

175 return self._prec 

176 

177 @prec.setter # PYCHOK setter! 

178 def prec(self, prec): 

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

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

181 

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

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

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

185 ''' 

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

187 if self._prec != prec: 

188 _update_all(self) 

189 self._prec = prec 

190 

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

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

193 ''' 

194 if self.status is not None: 

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

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

197 

198 @Property 

199 def reverse2(self): 

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

201 ''' 

202 return self._reverse2 

203 

204 @reverse2.setter # PYCHOK setter! 

205 def reverse2(self, reverse2): 

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

207 ''' 

208 reverse2 = bool(reverse2) 

209 if self._reverse2 != reverse2: 

210 _update_all(self) 

211 self._reverse2 = reverse2 

212 

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

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

215 ''' 

216 hold = self._Solve_path 

217 if hold != path: 

218 _update_all(self) 

219 self._Solve_path = path 

220 try: 

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

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

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

224 raise self._Error(status=self.status, txt_not_=_0_, **S_p) 

225 hold = path 

226 finally: # restore in case of error 

227 if self._Solve_path != hold: 

228 _update_all(self) 

229 self._Solve_path = hold 

230 

231 @property_RO 

232 def status(self): 

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

234 or C{None}. 

235 ''' 

236 return self._status 

237 

238 @Property 

239 def unroll(self): 

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

241 ''' 

242 return self._unroll 

243 

244 @unroll.setter # PYCHOK setter! 

245 def unroll(self, unroll): 

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

247 ''' 

248 unroll = bool(unroll) 

249 if self._unroll != unroll: 

250 _update_all(self) 

251 self._unroll = unroll 

252 

253 @property 

254 def verbose(self): 

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

256 ''' 

257 return self._verbose 

258 

259 @verbose.setter # PYCHOK setter! 

260 def verbose(self, verbose): 

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

262 a message around each C{RhumbSolve} invokation. 

263 ''' 

264 self._verbose = bool(verbose) 

265 

266 @Property_RO 

267 def version(self): 

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

269 ''' 

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

271 

272 

273class _SolveBase(_SolveLineSolveBase): 

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

275 ''' 

276 _datum = _WGS84 

277 

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

279 '''New C{Solve} instance. 

280 

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

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

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

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

285 is specified as C{scalar}. 

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

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

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

289 

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

291 ''' 

292 _earth_datum(self, a_ellipsoid, f=f, name=name) 

293 if name: 

294 self.name = name 

295 if path: 

296 self._setSolve(path) 

297 

298 @Property_RO 

299 def a(self): 

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

301 ''' 

302 return self.ellipsoid.a 

303 

304 @Property_RO 

305 def _cmdDirect(self): 

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

307 ''' 

308 return self._cmdBasic 

309 

310 @Property_RO 

311 def _cmdInverse(self): 

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

313 ''' 

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

315 

316 @property_RO 

317 def datum(self): 

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

319 ''' 

320 return self._datum 

321 

322 def Direct(self, lat1, lon1, azi1, s12, outmask=_UNUSED_): # PYCHOK unused 

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

324 ''' 

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

326 

327 @Property_RO 

328 def ellipsoid(self): 

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

330 ''' 

331 return self.datum.ellipsoid 

332 

333 @Property_RO 

334 def _e_option(self): 

335 E = self.ellipsoid 

336 if E is _EWGS84: 

337 return () # default 

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

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

340 

341 @Property_RO 

342 def flattening(self): 

343 '''Get the C{ellipsoid}'s I{flattening} (C{scalar}), M{(a - b) / a}, 

344 C{0} for spherical, negative for prolate. 

345 ''' 

346 return self.ellipsoid.f 

347 

348 f = flattening 

349 

350 def _GDictDirect(self, lat, lon, azi, arcmode, s12_a12, outmask=_UNUSED_, **floats): # PYCHOK for .geodesicx.gxarea 

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

352 ''' 

353 if arcmode: 

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

355 floats = _xkwds_get(floats, floats=True) 

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

357 lat, lon, azi, s12_a12) 

358 

359 def _GDictInverse(self, lat1, lon1, lat2, lon2, outmask=_UNUSED_, **floats): # PYCHOK for .geodesicx.gxarea 

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

361 I{without} C{_SALPs_CALPs_}. 

362 ''' 

363 floats = _xkwds_get(floats, floats=True) 

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

365 lat1, lon1, lat2, lon2) 

366 

367 def Inverse(self, lat1, lon1, lat2, lon2, outmask=_UNUSED_): # PYCHOK unused 

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

369 ''' 

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

371 

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

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

374 ''' 

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

376 # and .HeightIDWkarney._distances 

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

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

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

380 return abs(float(r.a12)) 

381 

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

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

384 ''' 

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

386 status=self.status, **Solve) 

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

388 

389 

390class _SolveLineBase(_SolveLineSolveBase): 

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

392 ''' 

393# _caps = 0 

394# _lla1 = {} 

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

396 

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

398 self._caps = caps | Caps._LINE 

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

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

401 self._solve = solve 

402 

403 n = name or solve.name 

404 if n: 

405 self.name = n 

406 

407 @Property_RO 

408 def _cmdDistance(self): 

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

410 ''' 

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

412 _, azi = _xkwds_item2(azi) 

413 return lat1, lon1, azi 

414 

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

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

417 

418 @property_RO 

419 def datum(self): 

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

421 ''' 

422 return self._solve.datum 

423 

424 @property_RO 

425 def ellipsoid(self): 

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

427 ''' 

428 return self._solve.ellipsoid 

429 

430 @Property_RO 

431 def lat1(self): 

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

433 ''' 

434 return self._lla1.lat1 

435 

436 @Property_RO 

437 def lon1(self): 

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

439 ''' 

440 return self._lla1.lon1 

441 

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

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

444 ''' 

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

446 lat1=self.lat1, lon1=self.lon1, 

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

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

449 

450 

451__all__ += _ALL_DOCS(_SolveBase, _SolveLineBase, _SolveLineSolveBase) 

452 

453# **) MIT License 

454# 

455# Copyright (C) 2016-2024 -- mrJean1 at Gmail -- All Rights Reserved. 

456# 

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

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

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

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

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

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

463# 

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

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

466# 

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

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

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

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

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

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

473# OTHER DEALINGS IN THE SOFTWARE.