Coverage for pygeodesy/solveBase.py: 92%

247 statements  

« prev     ^ index     » next       coverage.py v7.6.0, created at 2024-08-02 18:24 -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 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 

24 

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

26 

27__all__ = _ALL_LAZY.solveBase 

28__version__ = '24.07.11' 

29 

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 

37 

38 

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) 

46 

47 

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 

54 

55 

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

63 

64 

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 

80 

81 @Property_RO 

82 def a(self): 

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

84 ''' 

85 return self.ellipsoid.a 

86 

87 @property_RO 

88 def _cmdBasic(self): # PYCHOK no cover 

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

90 notOverloaded(self, underOK=True) 

91 

92 @property_RO 

93 def datum(self): 

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

95 ''' 

96 return self._datum 

97 

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 

104 

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 

124 

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 

133 

134 @Property_RO 

135 def _E_option(self): 

136 return ('-E',) if self.Exact else () 

137 

138 @property 

139 def Exact(self): 

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

141 ''' 

142 return self._Exact 

143 

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 

153 

154 @Property_RO 

155 def ellipsoid(self): 

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

157 ''' 

158 return self.datum.ellipsoid 

159 

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) 

167 

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 

174 

175 f = flattening 

176 

177 def invokat(self, *prefix): 

178 '''Get and set the invokation number C{"@"} prefix (C{str}). 

179 

180 @return: Previous prefix (C{str}). 

181 ''' 

182 p = self._invokat 

183 if prefix: 

184 set._invokat = str(prefix[0]) 

185 return p 

186 

187 @property_RO 

188 def invokation(self): 

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

190 ''' 

191 return self._invokation 

192 

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

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

195 

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

199 

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

201 

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

203 code from C{GeodSolve}. 

204 

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

206 from C{RhumbSolve}. 

207 

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 

220 

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 

240 

241 def linelimit(self, *limit): 

242 '''Set and get the print line length limit. 

243 

244 @arg limit: New line limit (C{int}) or C{0} 

245 or C{None} for unlimited. 

246 

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 

254 

255 @Property_RO 

256 def _mpd(self): # meter per degree 

257 return self.ellipsoid._Lpd 

258 

259 @property_RO 

260 def _p_option(self): 

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

262 

263 @Property 

264 def prec(self): 

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

266 ''' 

267 return self._prec 

268 

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

273 

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 

282 

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

293 

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 

311 

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 

318 

319 @property 

320 def verbose(self): 

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

322 ''' 

323 return self._verbose 

324 

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) 

331 

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

337 

338 

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 

346 

347 @Property 

348 def reverse2(self): 

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

350 ''' 

351 return self._reverse2 

352 

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 

361 

362 @Property 

363 def unroll(self): 

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

365 ''' 

366 return self._unroll 

367 

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 

376 

377 

378class _SolveGDictBase(_SolveBase): 

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

380 ''' 

381 

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

383 '''New C{Solve} instance. 

384 

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

393 

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) 

401 

402 @Property_RO 

403 def _cmdDirect(self): 

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

405 ''' 

406 return self._cmdBasic 

407 

408 @Property_RO 

409 def _cmdInverse(self): 

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

411 ''' 

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

413 

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) 

418 

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) 

426 

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) 

432 

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 

437 

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) 

442 

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

452 

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

459 

460 

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 

467 

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 

472 

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 

477 

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 

485 

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

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

488 

489 @property_RO 

490 def datum(self): 

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

492 ''' 

493 return self._solve.datum 

494 

495 @property_RO 

496 def ellipsoid(self): 

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

498 ''' 

499 return self._solve.ellipsoid 

500 

501 @Property_RO 

502 def lat1(self): 

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

504 ''' 

505 return self._lla1.lat1 

506 

507 @Property_RO 

508 def lon1(self): 

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

510 ''' 

511 return self._lla1.lon1 

512 

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

520 

521 

522__all__ += _ALL_DOCS(_SolveBase, _SolveCapsBase, _SolveGDictBase, _SolveGDictLineBase) 

523 

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.