Coverage for pygeodesy/solveBase.py: 91%

247 statements  

« prev     ^ index     » next       coverage.py v7.10.7, created at 2026-03-25 15:01 -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, _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 

24 

25__all__ = _ALL_LAZY.solveBase 

26__version__ = '25.12.06' 

27 

28_ERROR_ = 'ERROR' 

29 

30 

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) 

38 

39 

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 

46 

47 

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) # -5 stdout 

58 _prec2stdin = DIG - 1 

59 _Xable_name = NN # executable basename 

60 _Xable_path = NN # executable path 

61 _status = None 

62 _verbose = False 

63 

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 

69 

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 

75 

76 @property_RO 

77 def _cmdBasic(self): # PYCHOK no covers '''(INTERNAL) I{Must be overloaded}.''' 

78 notOverloaded(self, underOK=True) 

79 

80 @property_RO 

81 def datum(self): 

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

83 ''' 

84 return self._datum 

85 

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 

92 

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 

112 

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 

121 

122 @Property_RO 

123 def _E_option(self): 

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

125 

126 @property 

127 def Exact(self): 

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

129 ''' 

130 return self._Exact 

131 

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 

141 

142 @Property_RO 

143 def ellipsoid(self): 

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

145 ''' 

146 return self.datum.ellipsoid 

147 

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) 

155 

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 

162 

163 f = flattening 

164 

165 def invokat(self, *prefix): 

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

167 

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

169 ''' 

170 p = self._invokat 

171 if prefix: 

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

173 return p 

174 

175 @property_RO 

176 def invokation(self): 

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

178 ''' 

179 return self._invokation 

180 

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

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

183 

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

187 

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

189 

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

191 code from C{GeodSolve}. 

192 

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

194 from C{RhumbSolve}. 

195 

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 

208 

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 

228 

229 def linelimit(self, *limit): 

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

231 

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

233 or C{None} for unlimited. 

234 

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 

242 

243 @Property_RO 

244 def _mpd(self): # meter per degree 

245 return self.ellipsoid._Lpd 

246 

247 @property_RO 

248 def _p_option(self): 

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

250 

251 @Property 

252 def prec(self): 

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

254 ''' 

255 return self._prec 

256 

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

261 

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 

270 

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

281 

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 

299 

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 

306 

307 def _toStdin(self, floats): 

308 '''(INTERNAL) Convert C{floats} to strings. 

309 ''' 

310 return strs(floats, prec=self._prec2stdin, fmt=Fmt.F) # not .G! 

311 

312 @property 

313 def verbose(self): 

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

315 ''' 

316 return self._verbose 

317 

318 @verbose.setter # PYCHOK setter! 

319 def verbose(self, verbose): 

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

321 a message around each C{RhumbSolve} invokation. 

322 ''' 

323 self._verbose = bool(verbose) 

324 

325 @Property_RO 

326 def version(self): 

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

328 ''' 

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

330 

331 

332class _SolveBase(_SolveCapsBase): 

333 '''(INTERNAL) Base class for C{_SolveBase} and C{_SolveLineBase}. 

334 ''' 

335 _Names_Direct = \ 

336 _Names_Inverse = () 

337 _reverse2 = False 

338 _unroll = False 

339 

340 @Property 

341 def reverse2(self): 

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

343 ''' 

344 return self._reverse2 

345 

346 @reverse2.setter # PYCHOK setter! 

347 def reverse2(self, reverse2): 

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

349 ''' 

350 reverse2 = bool(reverse2) 

351 if self._reverse2 != reverse2: 

352 _update_all(self) 

353 self._reverse2 = reverse2 

354 

355 def toStr(self, prec=6, sep=_COMMASPACE_, **other): # PYCHOK signature 

356 '''Return this instance as string. 

357 

358 @kwarg prec_sep: Keyword argumens C{B{prec}=6} and C{B{sep}=", "} 

359 for the C{float} C{prec}ision, number of decimal digits 

360 (0..9) and the C{sep}arator string to join. Trailing 

361 zero decimals are stripped for B{C{prec}} values of 1 

362 and above, but kept for negative B{C{prec}} values. 

363 ''' 

364 return sep.join(pairs(other, prec=prec)) 

365 

366 @Property 

367 def unroll(self): 

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

369 ''' 

370 return self._unroll 

371 

372 @unroll.setter # PYCHOK setter! 

373 def unroll(self, unroll): 

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

375 ''' 

376 unroll = bool(unroll) 

377 if self._unroll != unroll: 

378 _update_all(self) 

379 self._unroll = unroll 

380 

381 

382class _Solve3Base(_SolveBase): 

383 '''(NTERNAL) Base class for C{_Geodesic[3]SolveBase}. 

384 ''' 

385 

386 @Property_RO 

387 def _cmdDirect(self): 

388 '''(INTERNAL) Get the C{[3]Solve} I{Direct} cmd (C{tuple}). 

389 ''' 

390 return self._cmdBasic 

391 

392 @Property_RO 

393 def _cmdInverse(self): 

394 '''(INTERNAL) Get the C{[3]Solve} I{Inverse} cmd (C{tuple}). 

395 ''' 

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

397 

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

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

400 ''' 

401 if arcmode: 

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

403 return self._GDictInvoke(self._cmdDirect, self._Names_Direct, 

404 lat, lon, azi, s12_a12, **floats) 

405 

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

407 '''(INTERNAL) Get C{_GenInverse}-like result as C{GDict}, but I{without} C{_SALP_CALPs_}. 

408 ''' 

409 return self._GDictInvoke(self._cmdInverse, self._Names_Inverse, 

410 lat1, lon1, lat2, lon2, **floats) 

411 

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

413 '''(INTERNAL) Invoke C{Solve}, return results as C{Dict}. 

414 ''' 

415 return self._DictInvoke2(cmd, args, Names, GDict, **floats)[0] # _R 

416 

417 def toStr(self, **prec_sep_other): # PYCHOK signature 

418 '''Return this instance as string. 

419 

420 @kwarg prec_sep: See L{toStr<pygeodesy.solveBase._SolveBase.toStr>}. 

421 ''' 

422 return _SolveBase.toStr(self, invokation=self.invokation, 

423 status=self.status, **prec_sep_other) 

424 

425 

426class _SolveGDictBase(_Solve3Base): 

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

428 ''' 

429 

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

431 '''New C{Solve} instance. 

432 

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

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

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

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

437 is specified as C{scalar}. 

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

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

440 @kwarg name: Optional C{B{name}=NN} (C{str}). 

441 

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

443 ''' 

444 _earth_datum(self, a_ellipsoid, f=f, **name) 

445 if name: 

446 self.name = name 

447 if path: 

448 self._setXable(path) 

449 

450 def ArcDirect(self, lat1, lon1, azi1, a12, outmask=_UNUSED_): # PYCHOK unused 

451 '''Return the C{Direct} result at C{a12} degrees. 

452 ''' 

453 return self._GDictDirect(lat1, lon1, azi1, True, a12) 

454 

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

456 '''Return the C{Direct} result at distance C{s12}. 

457 ''' 

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

459 

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

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

462 ''' 

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

464 

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

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

467 ''' 

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

469 # and .HeightIDWkarney._distances 

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

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

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

473 return abs(float(r.a12)) 

474 

475 def toStr(self, **prec_sep_other): # PYCHOK signature 

476 '''Return this C{_Solve} as string. 

477 ''' 

478 return _Solve3Base.toStr(self, ellipsoid=self.ellipsoid, **prec_sep_other) 

479 

480 

481class _SolveGDictLineBase(_SolveGDictBase): 

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

483 ''' 

484# _caps = 0 

485# _lla1 = {} 

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

487 

488 def __init__(self, solve, lat1, lon1, caps, **azi_name): 

489 name, azi = _name2__(azi_name, _or_nameof=solve) 

490 if name: 

491 self.name = name 

492 

493 self._caps = caps | Caps._AZIMUTH_LATITUDE_LONG_UNROLL 

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

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

496 self._solve = solve 

497 

498 @Property_RO 

499 def _cmdDistance(self): 

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

501 ''' 

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

503 _, azi = _xkwds_item2(azi) 

504 return lat1, lon1, azi 

505 

506 return self._cmdBasic + ('-L',) + self._toStdin(_lla3(**self._lla1)) 

507 

508 @property_RO 

509 def datum(self): 

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

511 ''' 

512 return self._solve.datum 

513 

514 @property_RO 

515 def ellipsoid(self): 

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

517 ''' 

518 return self._solve.ellipsoid 

519 

520 @Property_RO 

521 def lat1(self): 

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

523 ''' 

524 return self._lla1.lat1 

525 

526 @Property_RO 

527 def lon1(self): 

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

529 ''' 

530 return self._lla1.lon1 

531 

532 def toStr(self, **prec_sep_other): # PYCHOK signature 

533 '''Return this C{_LineSolve} as string. 

534 ''' 

535 return _Solve3Base.toStr(self, **prec_sep_other) 

536 

537 

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

539 

540# **) MIT License 

541# 

542# Copyright (C) 2016-2026 -- mrJean1 at Gmail -- All Rights Reserved. 

543# 

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

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

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

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

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

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

550# 

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

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

553# 

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

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

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

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

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

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

560# OTHER DEALINGS IN THE SOFTWARE.