1

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

3

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

5'''

6

7 from pygeodesy.basics import map2, ub2str, _zip

8 from pygeodesy.constants import DIG

9 from pygeodesy.errors import _AssertionError, _xkwds_get

10 from pygeodesy.interns import NN, _0_, _BACKSLASH_, _COMMASPACE_, _enquote, \

11 _EQUAL_, _Error_, _not_, _SPACE_, _UNUSED_

12 from pygeodesy.karney import Caps, _CapsBase, _a_ellipsoid, _EWGS84, GDict, \

13 Precision_

14 from pygeodesy.lazily import _ALL_DOCS, printf, _sys_version_info2

15 from pygeodesy.named import callername, notOverloaded

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

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

18# from pygeodesy.units import Precision_ # from .karney

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

20

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

22

23__all__ = () # nothing public

24__version__ = '23.08.04'

25

26_ERROR_ = 'ERROR'

27_text_True = dict() if _sys_version_info2 < (3, 7) else dict(text=True)

28

29

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

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

32 '''

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

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

35 return _SPACE_.join(cmd + t)

36

37

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

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

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

41 '''

42 p = _Popen(cmd, creationflags=0,

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

44 stdin=_PIPE, stdout=_PIPE, stderr=_STDOUT,

45 **_text_True) # PYCHOK kwArgs

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

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

48

49

50 class _SolveLineSolveBase(_CapsBase):

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

52 '''

53 _E = _EWGS84

54 _Error = None

55 _Exact = True

56 _invokation = 0

57 _Names_Direct = \

58 _Names_Inverse = ()

59 _prec = Precision_(prec=DIG)

60 _reverse2 = False

61 _Solve_name = NN # executable basename

62 _Solve_path = NN # executable path

63 _status = None

64 _unroll = False

65 _verbose = False

66

67 @Property_RO

68 def a(self):

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

70 '''

71 return self.ellipsoid.a

72

73 @property_RO

74 def _cmdBasic(self): # PYCHOK no cover

75 '''(INTERNAL) I{Must be overloaded}, see function C{notOverloaded}.

76 '''

77 notOverloaded(self)

78

79 @Property_RO

80 def ellipsoid(self):

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

82 '''

83 return self._E

84

85 @Property_RO

86 def _e_option(self):

87 E = self.ellipsoid

88 if E is _EWGS84:

89 return () # default

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

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

92

93 @property

94 def Exact(self):

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

96 '''

97 return self._Exact

98

99 @Exact.setter # PYCHOK setter!

100 def Exact(self, Exact):

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

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

103 '''

104 Exact = bool(Exact)

105 if self._Exact != Exact:

106 _update_all(self)

107 self._Exact = Exact

108

109 @Property_RO

110 def f(self):

111 '''Get the ellipsoid's I{flattening} (C{float}), M{(a - b) / a}, C{0} for spherical, negative for prolate.

112 '''

113 return self.ellipsoid.f

114

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

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

117 '''

118 N = len(Names)

119 if N < 1:

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

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

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

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

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

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

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

127 n += Names

128 else:

129 n, v = Names, t

130 if self.verbose: # PYCHOK no cover

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

132 if floats:

133 v = map(float, v)

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

135 return self._iter2tion(r, r)

136

137 @property_RO

138 def invokation(self):

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

140 '''

141 return self._invokation

142

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

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

145

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

147 options (C{str}s).

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

149

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

151

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

153 code from C{GeodSolve}.

154

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

156 from C{RhumbSolve}.

157

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

159 '''

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

161 i = _xkwds_get(stdin, stdin=None)

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

163 s = self.status

164 if s:

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

166 txt=_not_(_0_))

167 if self.verbose: # PYCHOK no cover

168 self._print(r)

169 return r

170

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

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

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

174 '''

175 self._invokation += 1

176 self._status = t = None

177 if self.verbose: # PYCHOK no cover

178 t = _cmd_stdin_(cmd, stdin)

179 self._print(t)

180 try: # invoke and write to stdin

181 s, r = _popen2(cmd, stdin)

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

183 raise ValueError(r)

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

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

186 self._status = s

187 return r

188

189 @property_RO

190 def _p_option(self):

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

192

193 @Property

194 def prec(self):

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

196 '''

197 return self._prec

198

199 @prec.setter # PYCHOK setter!

200 def prec(self, prec):

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

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

203

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

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

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

207 '''

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

209 if self._prec != prec:

210 _update_all(self)

211 self._prec = prec

212

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

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

215 '''

216 if self.status is not None:

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

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

219

220 @Property

221 def reverse2(self):

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

223 '''

224 return self._reverse2

225

226 @reverse2.setter # PYCHOK setter!

227 def reverse2(self, reverse2):

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

229 '''

230 reverse2 = bool(reverse2)

231 if self._reverse2 != reverse2:

232 _update_all(self)

233 self._reverse2 = reverse2

234

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

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

237 '''

238 hold = self._Solve_path

239 if hold != path:

240 _update_all(self)

241 self._Solve_path = path

242 try:

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

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

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

246 raise self._Error(status=self.status, txt=_not_(_0_), **S_p)

247 hold = path

248 finally: # restore in case of error

249 if self._Solve_path != hold:

250 _update_all(self)

251 self._Solve_path = hold

252

253 @property_RO

254 def status(self):

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

256 or C{None}.

257 '''

258 return self._status

259

260 @Property

261 def unroll(self):

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

263 '''

264 return self._unroll

265

266 @unroll.setter # PYCHOK setter!

267 def unroll(self, unroll):

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

269 '''

270 unroll = bool(unroll)

271 if self._unroll != unroll:

272 _update_all(self)

273 self._unroll = unroll

274

275 @property

276 def verbose(self):

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

278 '''

279 return self._verbose

280

281 @verbose.setter # PYCHOK setter!

282 def verbose(self, verbose):

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

284 a message around each C{RhumbSolve} invokation.

285 '''

286 self._verbose = bool(verbose)

287

288 @Property_RO

289 def version(self):

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

291 '''

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

293

294

295 class _SolveBase(_SolveLineSolveBase):

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

297 '''

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

299 '''New C{Solve} instance.

300

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

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

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

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

305 is specified as C{scalar}.

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

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

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

309

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

311 '''

312 if a_ellipsoid not in (self._E, None): # NOT self.ellipsoid

313 self._E = _a_ellipsoid(a_ellipsoid, f, name=name)

314 if name:

315 self.name = name

316 if path:

317 self._setSolve(path)

318

319 @Property_RO

320 def _cmdDirect(self):

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

322 '''

323 return self._cmdBasic

324

325 @Property_RO

326 def _cmdInverse(self):

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

328 '''

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

330

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

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

333 '''

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

335

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

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

338 '''

339 if arcmode:

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

341 floats = _xkwds_get(floats, floats=True)

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

343 lat, lon, azi, s12_a12)

344

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

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

347 I{without} C{_SALPs_CALPs_}.

348 '''

349 floats = _xkwds_get(floats, floats=True)

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

351 lat1, lon1, lat2, lon2)

352

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

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

355 '''

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

357

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

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

360 '''

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

362 # and .HeightIDWkarney._distances

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

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

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

366 return abs(float(r.a12))

367

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

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

370 '''

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

372 status=self.status, **Solve)

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

374

375

376 class _SolveLineBase(_SolveLineSolveBase):

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

378 '''

379# _caps = 0

380# _lla1 = {}

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

382

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

384 self._caps = caps | Caps._LINE

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

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

387 self._solve = solve

388

389 n = name or solve.name

390 if n:

391 self.name = n

392

393 @Property_RO

394 def _cmdDistance(self):

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

396 '''

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

398 _, azi = azi.popitem()

399 return lat1, lon1, azi

400

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

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

403

404 @property_RO

405 def ellipsoid(self):

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

407 '''

408 return self._solve.ellipsoid

409

410 @Property_RO

411 def lat1(self):

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

413 '''

414 return self._lla1.lat1

415

416 @Property_RO

417 def lon1(self):

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

419 '''

420 return self._lla1.lon1

421

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

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

424 '''

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

426 lat1=self.lat1, lon1=self.lon1,

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

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

429

430

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

432

433# **) MIT License

434#

435# Copyright (C) 2016-2023 -- mrJean1 at Gmail -- All Rights Reserved.

436#

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

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

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

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

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

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

443#

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

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

446#

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

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

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

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

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

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

453# OTHER DEALINGS IN THE SOFTWARE.