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_get

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

13 _EQUAL_, _Error_, _not_, _SPACE_, _UNUSED_

14 from pygeodesy.karney import Caps, _CapsBase, GDict, Precision_

15 from pygeodesy.lazily import _ALL_DOCS, _ALL_LAZY, printf, _unlazy

16 from pygeodesy.named import callername, notOverloaded

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

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

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

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

21

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

23

24__all__ = _ALL_LAZY.solveBase

25__version__ = '23.12.09'

26

27_ERROR_ = 'ERROR'

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

29

30

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

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

33 '''

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

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

36 return _SPACE_.join(cmd + t)

37

38

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

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

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

42 '''

43 p = _Popen(cmd, creationflags=0,

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

45 stdin=_PIPE, stdout=_PIPE, stderr=_STDOUT,

46 **_text_True) # PYCHOK kwArgs

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

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

49

50

51 class _SolveLineSolveBase(_CapsBase):

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

53 '''

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 _cmdBasic(self): # PYCHOK no cover

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

70 notOverloaded(self, underOK=True)

71

72 @property

73 def Exact(self):

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

75 '''

76 return self._Exact

77

78 @Exact.setter # PYCHOK setter!

79 def Exact(self, Exact):

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

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

82 '''

83 Exact = bool(Exact)

84 if self._Exact != Exact:

85 _update_all(self)

86 self._Exact = Exact

87

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

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

90 '''

91 N = len(Names)

92 if N < 1:

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

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

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

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

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

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

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

100 n += Names

101 else:

102 n, v = Names, t

103 if self.verbose: # PYCHOK no cover

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

105 if floats:

106 v = map(float, v)

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

108 return self._iter2tion(r, r)

109

110 @property_RO

111 def invokation(self):

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

113 '''

114 return self._invokation

115

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

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

118

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

120 options (C{str}s).

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

122

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

124

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

126 code from C{GeodSolve}.

127

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

129 from C{RhumbSolve}.

130

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

132 '''

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

134 i = _xkwds_get(stdin, stdin=None)

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

136 s = self.status

137 if s:

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

139 txt=_not_(_0_))

140 if self.verbose: # PYCHOK no cover

141 self._print(r)

142 return r

143

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

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

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

147 '''

148 self._invokation += 1

149 self._status = t = None

150 if self.verbose: # PYCHOK no cover

151 t = _cmd_stdin_(cmd, stdin)

152 self._print(t)

153 try: # invoke and write to stdin

154 s, r = _popen2(cmd, stdin)

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

156 raise ValueError(r)

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

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

159 self._status = s

160 return r

161

162 @Property_RO

163 def _mpd(self): # meter per degree

164 return self.ellipsoid._Lpd

165

166 @property_RO

167 def _p_option(self):

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

169

170 @Property

171 def prec(self):

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

173 '''

174 return self._prec

175

176 @prec.setter # PYCHOK setter!

177 def prec(self, prec):

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

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

180

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

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

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

184 '''

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

186 if self._prec != prec:

187 _update_all(self)

188 self._prec = prec

189

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

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

192 '''

193 if self.status is not None:

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

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

196

197 @Property

198 def reverse2(self):

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

200 '''

201 return self._reverse2

202

203 @reverse2.setter # PYCHOK setter!

204 def reverse2(self, reverse2):

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

206 '''

207 reverse2 = bool(reverse2)

208 if self._reverse2 != reverse2:

209 _update_all(self)

210 self._reverse2 = reverse2

211

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

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

214 '''

215 hold = self._Solve_path

216 if hold != path:

217 _update_all(self)

218 self._Solve_path = path

219 try:

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

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

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

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

224 hold = path

225 finally: # restore in case of error

226 if self._Solve_path != hold:

227 _update_all(self)

228 self._Solve_path = hold

229

230 @property_RO

231 def status(self):

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

233 or C{None}.

234 '''

235 return self._status

236

237 @Property

238 def unroll(self):

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

240 '''

241 return self._unroll

242

243 @unroll.setter # PYCHOK setter!

244 def unroll(self, unroll):

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

246 '''

247 unroll = bool(unroll)

248 if self._unroll != unroll:

249 _update_all(self)

250 self._unroll = unroll

251

252 @property

253 def verbose(self):

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

255 '''

256 return self._verbose

257

258 @verbose.setter # PYCHOK setter!

259 def verbose(self, verbose):

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

261 a message around each C{RhumbSolve} invokation.

262 '''

263 self._verbose = bool(verbose)

264

265 @Property_RO

266 def version(self):

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

268 '''

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

270

271

272 class _SolveBase(_SolveLineSolveBase):

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

274 '''

275 _datum = _WGS84

276

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

278 '''New C{Solve} instance.

279

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

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

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

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

284 is specified as C{scalar}.

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

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

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

288

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

290 '''

291 _earth_datum(self, a_ellipsoid, f=f, name=name)

292 if name:

293 self.name = name

294 if path:

295 self._setSolve(path)

296

297 @Property_RO

298 def a(self):

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

300 '''

301 return self.ellipsoid.a

302

303 @Property_RO

304 def _cmdDirect(self):

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

306 '''

307 return self._cmdBasic

308

309 @Property_RO

310 def _cmdInverse(self):

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

312 '''

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

314

315 @property_RO

316 def datum(self):

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

318 '''

319 return self._datum

320

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

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

323 '''

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

325

326 @Property_RO

327 def ellipsoid(self):

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

329 '''

330 return self.datum.ellipsoid

331

332 @Property_RO

333 def _e_option(self):

334 E = self.ellipsoid

335 if E is _EWGS84:

336 return () # default

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

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

339

340 @Property_RO

341 def flattening(self):

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

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

344 '''

345 return self.ellipsoid.f

346

347 f = flattening

348

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

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

351 '''

352 if arcmode:

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

354 floats = _xkwds_get(floats, floats=True)

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

356 lat, lon, azi, s12_a12)

357

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

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

360 I{without} C{_SALPs_CALPs_}.

361 '''

362 floats = _xkwds_get(floats, floats=True)

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

364 lat1, lon1, lat2, lon2)

365

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

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

368 '''

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

370

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

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

373 '''

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

375 # and .HeightIDWkarney._distances

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

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

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

379 return abs(float(r.a12))

380

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

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

383 '''

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

385 status=self.status, **Solve)

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

387

388

389 class _SolveLineBase(_SolveLineSolveBase):

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

391 '''

392# _caps = 0

393# _lla1 = {}

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

395

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

397 self._caps = caps | Caps._LINE

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

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

400 self._solve = solve

401

402 n = name or solve.name

403 if n:

404 self.name = n

405

406 @Property_RO

407 def _cmdDistance(self):

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

409 '''

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

411 _, azi = azi.popitem()

412 return lat1, lon1, azi

413

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

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

416

417 @property_RO

418 def datum(self):

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

420 '''

421 return self._solve.datum

422

423 @property_RO

424 def ellipsoid(self):

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

426 '''

427 return self._solve.ellipsoid

428

429 @Property_RO

430 def lat1(self):

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

432 '''

433 return self._lla1.lat1

434

435 @Property_RO

436 def lon1(self):

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

438 '''

439 return self._lla1.lon1

440

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

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

443 '''

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

445 lat1=self.lat1, lon1=self.lon1,

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

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

448

449

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

451

452# **) MIT License

453#

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

455#

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

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

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

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

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

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

462#

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

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

465#

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

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

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

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

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

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

472# OTHER DEALINGS IN THE SOFTWARE.