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, _sys_version_info2

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.06.05'

27

28_ERROR_ = 'ERROR'

29_Popen_kwds = dict(creationflags=0,

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

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

32 if _sys_version_info2 > (3, 6):

33 _Popen_kwds.update(text=True)

34 del _PIPE, _STDOUT, _sys_version_info2 # _ALL_LAZY

35

36

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

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

39 '''

40 if stdin is not None:

41 cmd += _BACKSLASH_, str(stdin)

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

43 return _SPACE_.join(cmd)

44

45

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

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

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

49 '''

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

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

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

53

54

55 class _SolveCapsBase(_CapsBase):

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

57 '''

58 _Error = None

59 _Exact = True

60 _invokation = 0

61 _Names_Direct = \

62 _Names_Inverse = ()

63 _prec = Precision_(prec=DIG)

64 _reverse2 = False

65 _Solve_name = NN # executable basename

66 _Solve_path = NN # executable path

67 _status = None

68 _unroll = False

69 _verbose = False

70

71 @property_RO

72 def _cmdBasic(self): # PYCHOK no cover

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

74 notOverloaded(self, underOK=True)

75

76 @property

77 def Exact(self):

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

79 '''

80 return self._Exact

81

82 @Exact.setter # PYCHOK setter!

83 def Exact(self, Exact):

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

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

86 '''

87 Exact = bool(Exact)

88 if self._Exact != Exact:

89 _update_all(self)

90 self._Exact = Exact

91

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

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

94 '''

95 N = len(Names)

96 if N < 1:

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

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

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

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

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

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

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

104 n += Names

105 else:

106 n, v = Names, t

107 if self.verbose: # PYCHOK no cover

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

109 if floats:

110 v = map(float, v)

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

112 return self._iter2tion(r, **r)

113

114 @property_RO

115 def invokation(self):

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

117 '''

118 return self._invokation

119

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

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

122

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

124 options (C{str}s).

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

126

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

128

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

130 code from C{GeodSolve}.

131

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

133 from C{RhumbSolve}.

134

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

136 '''

137 c = (self._Solve_path,) + map2(str, options) # map2(_enquote, options)

138 i = _xkwds_get1(stdin, stdin=None)

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

140 s = self.status

141 if s:

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

143 txt_not_=_0_)

144 if self.verbose: # PYCHOK no cover

145 self._print(r)

146 return r

147

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

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

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

151 '''

152 self._invokation += 1

153 self._status = t = None

154 if self.verbose: # PYCHOK no cover

155 t = _cmd_stdin_(cmd, stdin)

156 self._print(t)

157 try: # invoke and write to stdin

158 s, r = _popen2(cmd, stdin)

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

160 raise ValueError(r)

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

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

163 self._status = s

164 return r

165

166 @Property_RO

167 def _mpd(self): # meter per degree

168 return self.ellipsoid._Lpd

169

170 @property_RO

171 def _p_option(self):

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

173

174 @Property

175 def prec(self):

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

177 '''

178 return self._prec

179

180 @prec.setter # PYCHOK setter!

181 def prec(self, prec):

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

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

184

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

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

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

188 '''

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

190 if self._prec != prec:

191 _update_all(self)

192 self._prec = prec

193

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

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

196 '''

197 if self.status is not None:

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

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

200

201 @Property

202 def reverse2(self):

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

204 '''

205 return self._reverse2

206

207 @reverse2.setter # PYCHOK setter!

208 def reverse2(self, reverse2):

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

210 '''

211 reverse2 = bool(reverse2)

212 if self._reverse2 != reverse2:

213 _update_all(self)

214 self._reverse2 = reverse2

215

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

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

218 '''

219 hold = self._Solve_path

220 if hold != path:

221 _update_all(self)

222 self._Solve_path = path

223 try:

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

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

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

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

228 hold = path

229 finally: # restore in case of error

230 if self._Solve_path != hold:

231 _update_all(self)

232 self._Solve_path = hold

233

234 @property_RO

235 def status(self):

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

237 or C{None}.

238 '''

239 return self._status

240

241 @Property

242 def unroll(self):

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

244 '''

245 return self._unroll

246

247 @unroll.setter # PYCHOK setter!

248 def unroll(self, unroll):

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

250 '''

251 unroll = bool(unroll)

252 if self._unroll != unroll:

253 _update_all(self)

254 self._unroll = unroll

255

256 @property

257 def verbose(self):

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

259 '''

260 return self._verbose

261

262 @verbose.setter # PYCHOK setter!

263 def verbose(self, verbose):

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

265 a message around each C{RhumbSolve} invokation.

266 '''

267 self._verbose = bool(verbose)

268

269 @Property_RO

270 def version(self):

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

272 '''

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

274

275

276 class _SolveBase(_SolveCapsBase):

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

278 '''

279 _datum = _WGS84

280

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

282 '''New C{Solve} instance.

283

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

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

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

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

288 is specified as C{scalar}.

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

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

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

292

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

294 '''

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

296 if name:

297 self.name = name

298 if path:

299 self._setSolve(path)

300

301 @Property_RO

302 def a(self):

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

304 '''

305 return self.ellipsoid.a

306

307 @Property_RO

308 def _cmdDirect(self):

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

310 '''

311 return self._cmdBasic

312

313 @Property_RO

314 def _cmdInverse(self):

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

316 '''

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

318

319 @property_RO

320 def datum(self):

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

322 '''

323 return self._datum

324

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

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

327 '''

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

329

330 @Property_RO

331 def ellipsoid(self):

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

333 '''

334 return self.datum.ellipsoid

335

336 @Property_RO

337 def _e_option(self):

338 E = self.ellipsoid

339 if E is _EWGS84:

340 return () # default

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

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

343

344 @Property_RO

345 def flattening(self):

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

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

348 '''

349 return self.ellipsoid.f

350

351 f = flattening

352

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

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

355 '''

356 if arcmode:

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

358 floats = _xkwds_get1(floats, floats=True)

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

360 lat, lon, azi, s12_a12)

361

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

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

364 I{without} C{_SALPs_CALPs_}.

365 '''

366 floats = _xkwds_get1(floats, floats=True)

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

368 lat1, lon1, lat2, lon2)

369

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

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

372 '''

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

374

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

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

377 '''

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

379 # and .HeightIDWkarney._distances

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

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

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

383 return abs(float(r.a12))

384

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

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

387 '''

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

389 status=self.status, **Solve)

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

391

392

393 class _SolveLineBase(_SolveCapsBase):

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

395 '''

396# _caps = 0

397# _lla1 = {}

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

399

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

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

402 if name:

403 self.name = name

404

405 self._caps = caps | Caps._LINE

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

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

408 self._solve = solve

409

410 @Property_RO

411 def _cmdDistance(self):

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

413 '''

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

415 _, azi = _xkwds_item2(azi)

416 return lat1, lon1, azi

417

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

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

420

421 @property_RO

422 def datum(self):

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

424 '''

425 return self._solve.datum

426

427 @property_RO

428 def ellipsoid(self):

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

430 '''

431 return self._solve.ellipsoid

432

433 @Property_RO

434 def lat1(self):

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

436 '''

437 return self._lla1.lat1

438

439 @Property_RO

440 def lon1(self):

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

442 '''

443 return self._lla1.lon1

444

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

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

447 '''

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

449 lat1=self.lat1, lon1=self.lon1,

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

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

452

453

454__all__ += _ALL_DOCS(_SolveBase, _SolveLineBase, _SolveCapsBase)

455

456# **) MIT License

457#

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

459#

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

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

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

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

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

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

466#

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

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

469#

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

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

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

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

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

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

476# OTHER DEALINGS IN THE SOFTWARE.