1

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

3

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

5'''

6

7 from pygeodesy.basics import clips, map2, ub2str, _zip

8 from pygeodesy.constants import DIG

9 from pygeodesy.datums import _earth_datum, _WGS84, _EWGS84

10# from pygeodesy.ellipsoids import _EWGS84 # from .datums

11 from pygeodesy.errors import _AssertionError, _xkwds_get, _xkwds_get1, \

12 _xkwds_item2

13 from pygeodesy.internals import _enquote, printf

14 from pygeodesy.interns import NN, _0_, _BACKSLASH_, _COMMASPACE_, \

15 _EQUAL_, _Error_, _SPACE_, _UNUSED_

16 from pygeodesy.karney import Caps, _CapsBase, GDict

17 from pygeodesy.lazily import _ALL_DOCS, _ALL_LAZY, _sys_version_info2

18 from pygeodesy.named import callername, _name2__, notOverloaded

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

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

21 from pygeodesy.units import Precision_

22 from pygeodesy.utily import unroll180, wrap360 # PYCHOK shared

23

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

25

26__all__ = _ALL_LAZY.solveBase

27__version__ = '24.07.08'

28

29_ERROR_ = 'ERROR'

30_Popen_kwds = dict(creationflags=0,

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

32 stdin=_PIPE, stdout=_PIPE, stderr=_STDOUT)

33 if _sys_version_info2 > (3, 6):

34 _Popen_kwds.update(text=True)

35 del _PIPE, _STDOUT, _sys_version_info2 # _ALL_LAZY

36

37

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

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

40 '''

41 if stdin is not None:

42 cmd += _BACKSLASH_, str(stdin)

43 cmd += Fmt.PAREN(callername(up=3)),

44 return _SPACE_.join(cmd)

45

46

47# def _float_int(r):

48# '''(INTERNAL) Convert result into C{float} or C{int}.

49# '''

50# f = float(r)

51# i = int(f)

52# return i if float(i) == f else f # PYCHOK inconsistent

53

54

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

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

57 and all output from C{stdout/-err}.

58 '''

59 p = _Popen(cmd, **_Popen_kwds) # PYCHOK kwArgs

60 r = p.communicate(stdin)[0] # stdout + NL + stderr

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

62

63

64 class _SolveCapsBase(_CapsBase):

65 '''(NTERNAL) Base class for C{_SolveBase} and C{_LineSolveBase}.

66 '''

67 _datum = _WGS84

68 _Error = None

69 _Exact = True

70 _invokation = 0

71 _linelimit = 0

72 _prec = Precision_(prec=DIG)

73 _prec2stdin = DIG

74 _Xable_name = NN # executable basename

75 _Xable_path = NN # executable path

76 _status = None

77 _verbose = False

78

79 @Property_RO

80 def a(self):

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

82 '''

83 return self.ellipsoid.a

84

85 @property_RO

86 def _cmdBasic(self): # PYCHOK no cover

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

88 notOverloaded(self, underOK=True)

89

90 @property_RO

91 def datum(self):

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

93 '''

94 return self._datum

95

96 def _Dict(self, Dict, n, v, floats=True, **unused):

97 if self.verbose: # PYCHOK no cover

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

99 if floats:

100 v = map(float, v) # _float_int, see Intersectool._XDistInvoke

101 return Dict(_zip(n, v)) # strict=True

102

103 def _DictInvoke2(self, cmd, Names, Dict, args, **floats_R):

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

105 '''

106 N = len(Names)

107 if N < 1:

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

109 i = fstr(args, prec=self._prec2stdin, fmt=Fmt.F, sep=_SPACE_) if args else None # NOT Fmt.G!

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

111 if _xkwds_get(floats_R, _R=None): # == '-R' in cmd

112 return self._Dicts(Dict, Names, t, **floats_R), True

113 elif len(t) > N: # PYCHOK no cover

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

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

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

117 n += Names

118 else:

119 n, v = Names, t

120 r = self._Dict(Dict, n, t, **floats_R)

121 return self._iter2tion(r, **r), None

122

123 def _Dicts(self, Dict, Names, t, **floats_R):

124 i, N = 0, len(Names)

125 for x in range(0, len(t), N):

126 if t[x] == 'nan':

127 break

128 X = self._Dict(Dict, Names, t[x:x + N], **floats_R)

129 yield X.set_(iteration=i)

130 i += 1

131

132 @Property_RO

133 def _E_option(self):

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

135

136 @property

137 def Exact(self):

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

139 '''

140 return self._Exact

141

142 @Exact.setter # PYCHOK setter!

143 def Exact(self, Exact):

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

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

146 '''

147 Exact = bool(Exact)

148 if self._Exact != Exact:

149 _update_all(self)

150 self._Exact = Exact

151

152 @Property_RO

153 def ellipsoid(self):

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

155 '''

156 return self.datum.ellipsoid

157

158 @Property_RO

159 def _e_option(self):

160 E = self.ellipsoid

161 if E is _EWGS84:

162 return () # default

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

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

165

166 @Property_RO

167 def flattening(self):

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

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

170 '''

171 return self.ellipsoid.f

172

173 f = flattening

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 s, r = _popen2(cmd, stdin)

220 if len(r) < 6 or r[:5] in (_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))

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): # 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 line = _SPACE_(line, Fmt.PAREN(self.status))

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

279

280 def _setXable(self, path, **Xable_path):

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

282 '''

283 hold = self._Xable_path

284 if hold != path:

285 _update_all(self)

286 self._Xable_path = path

287 try:

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

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

290 S_p = Xable_path or {self._Xable_name: _enquote(path)}

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

292 hold = path

293 finally: # restore in case of error

294 if self._Xable_path != hold:

295 _update_all(self)

296 self._Xable_path = hold

297

298 @property_RO

299 def status(self):

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

301 or C{None}.

302 '''

303 return self._status

304

305 @property

306 def verbose(self):

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

308 '''

309 return self._verbose

310

311 @verbose.setter # PYCHOK setter!

312 def verbose(self, verbose):

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

314 a message around each C{RhumbSolve} invokation.

315 '''

316 self._verbose = bool(verbose)

317

318 @Property_RO

319 def version(self):

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

321 '''

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

323

324

325 class _SolveBase(_SolveCapsBase):

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

327 '''

328 _Names_Direct = \

329 _Names_Inverse = ()

330 _reverse2 = False

331 _unroll = False

332

333 @Property

334 def reverse2(self):

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

336 '''

337 return self._reverse2

338

339 @reverse2.setter # PYCHOK setter!

340 def reverse2(self, reverse2):

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

342 '''

343 reverse2 = bool(reverse2)

344 if self._reverse2 != reverse2:

345 _update_all(self)

346 self._reverse2 = reverse2

347

348 @Property

349 def unroll(self):

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

351 '''

352 return self._unroll

353

354 @unroll.setter # PYCHOK setter!

355 def unroll(self, unroll):

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

357 '''

358 unroll = bool(unroll)

359 if self._unroll != unroll:

360 _update_all(self)

361 self._unroll = unroll

362

363

364 class _SolveGDictBase(_SolveBase):

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

366 '''

367

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

369 '''New C{Solve} instance.

370

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

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

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

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

375 is specified as C{scalar}.

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

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

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

379

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

381 '''

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

383 if name:

384 self.name = name

385 if path:

386 self._setXable(path)

387

388 @Property_RO

389 def _cmdDirect(self):

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

391 '''

392 return self._cmdBasic

393

394 @Property_RO

395 def _cmdInverse(self):

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

397 '''

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

399

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

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

402 '''

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

404

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

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

407 '''

408 if arcmode:

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

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

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

412

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

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

415 '''

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

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

418

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

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

421 '''

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

423

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

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

426 '''

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

428

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

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

431 '''

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

433 # and .HeightIDWkarney._distances

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

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

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

437 return abs(float(r.a12))

438

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

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

441 '''

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

443 status=self.status, **Solve)

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

445

446

447 class _SolveGDictLineBase(_SolveGDictBase):

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

449 '''

450# _caps = 0

451# _lla1 = {}

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

453

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

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

456 if name:

457 self.name = name

458

459 self._caps = caps | Caps._LINE

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

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

462 self._solve = solve

463

464 @Property_RO

465 def _cmdDistance(self):

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

467 '''

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

469 _, azi = _xkwds_item2(azi)

470 return lat1, lon1, azi

471

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

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

474

475 @property_RO

476 def datum(self):

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

478 '''

479 return self._solve.datum

480

481 @property_RO

482 def ellipsoid(self):

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

484 '''

485 return self._solve.ellipsoid

486

487 @Property_RO

488 def lat1(self):

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

490 '''

491 return self._lla1.lat1

492

493 @Property_RO

494 def lon1(self):

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

496 '''

497 return self._lla1.lon1

498

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

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

501 '''

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

503 lat1=self.lat1, lon1=self.lon1,

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

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

506

507

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

509

510# **) MIT License

511#

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

513#

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

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

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

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

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

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

520#

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

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

523#

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

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

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

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

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

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

530# OTHER DEALINGS IN THE SOFTWARE.