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 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_get1, _xkwds_item2

12 from pygeodesy.internals import _enquote, printf

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

14 _EQUAL_, _Error_, _SPACE_, _UNUSED_

15 from pygeodesy.karney import Caps, _CapsBase, GDict

16 from pygeodesy.lazily import _ALL_DOCS, _ALL_LAZY, _unlazy

17 from pygeodesy.named import callername, _name2__, notOverloaded

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

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

20 from pygeodesy.units import Precision_

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

22

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

24

25__all__ = _ALL_LAZY.solveBase

26__version__ = '24.05.31'

27

28_ERROR_ = 'ERROR'

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

30

31

32 def _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

40 def _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

52 class _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_get1(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

273 class _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):

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 C{B{name}=NN} (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)

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

390 class _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, **azi_name):

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

399 if name:

400 self.name = name

401

402 self._caps = caps | Caps._LINE

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

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

405 self._solve = solve

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.