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, _isin, map2, _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, _popen2, printf

14 from pygeodesy.interns import NN, _0_, _AT_,_BACKSLASH_, _COLONSPACE_, \

15 _COMMASPACE_, _EQUAL_, _Error_, _SPACE_, \

16 _UNUSED_

17 from pygeodesy.karney import Caps, _CapsBase, GDict

18 from pygeodesy.lazily import _ALL_DOCS, _ALL_LAZY

19 from pygeodesy.named import callername, _name2__, notOverloaded

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

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

22 from pygeodesy.units import Precision_

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

24

25__all__ = _ALL_LAZY.solveBase

26__version__ = '25.04.14'

27

28_ERROR_ = 'ERROR'

29

30

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

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

33 '''

34 if stdin is not None:

35 cmd += _BACKSLASH_, str(stdin)

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

37 return _SPACE_.join(cmd)

38

39

40# def _float_int(r):

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

42# '''

43# f = float(r)

44# i = int(f)

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

46

47

48 class _SolveCapsBase(_CapsBase):

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

50 '''

51 _datum = _WGS84

52 _Error = None

53 _Exact = True

54 _invokat = _AT_

55 _invokation = 0

56 _linelimit = 0

57 _prec = Precision_(prec=DIG)

58 _prec2stdin = DIG

59 _Xable_name = NN # executable basename

60 _Xable_path = NN # executable path

61 _status = None

62 _verbose = False

63

64 @Property_RO

65 def a(self):

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

67 '''

68 return self.ellipsoid.a

69

70 @property_RO

71 def _cmdBasic(self): # PYCHOK no covers '''(INTERNAL) I{Must be overloaded}.'''

72 notOverloaded(self, underOK=True)

73

74 @property_RO

75 def datum(self):

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

77 '''

78 return self._datum

79

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

81 if self.verbose: # PYCHOK no cover

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

83 if floats:

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

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

86

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

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

89 '''

90 N = len(Names)

91 if N < 1:

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

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

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

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

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

97 elif 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 r = self._Dict(Dict, n, t, **floats_R)

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

106

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

108 i, N = 0, len(Names)

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

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

111 break

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

113 yield X.set_(iteration=i)

114 i += 1

115

116 @Property_RO

117 def _E_option(self):

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

119

120 @property

121 def Exact(self):

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

123 '''

124 return self._Exact

125

126 @Exact.setter # PYCHOK setter!

127 def Exact(self, Exact):

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

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

130 '''

131 Exact = bool(Exact)

132 if self._Exact != Exact:

133 _update_all(self)

134 self._Exact = Exact

135

136 @Property_RO

137 def ellipsoid(self):

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

139 '''

140 return self.datum.ellipsoid

141

142 @Property_RO

143 def _e_option(self):

144 E = self.ellipsoid

145 if E is _EWGS84:

146 return () # default

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

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

149

150 @Property_RO

151 def flattening(self):

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

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

154 '''

155 return self.ellipsoid.f

156

157 f = flattening

158

159 def invokat(self, *prefix):

160 '''Get and set the invokation number C{"@"} prefix (C{str}).

161

162 @return: Previous prefix (C{str}).

163 '''

164 p = self._invokat

165 if prefix:

166 set._invokat = str(prefix[0])

167 return p

168

169 @property_RO

170 def invokation(self):

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

172 '''

173 return self._invokation

174

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

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

177

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

179 options (C{str}s).

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

181

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

183

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

185 code from C{GeodSolve}.

186

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

188 from C{RhumbSolve}.

189

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

191 '''

192 c = (self._Xable_path,) + map2(str, options) # map2(_enquote, options)

193 i = _xkwds_get1(stdin, stdin=None)

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

195 s = self.status

196 if s:

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

198 txt_not_=_0_)

199 if self.verbose: # PYCHOK no cover

200 self._print(r)

201 return r

202

203 def _invoke(self, cmd, stdin=None, **unused): # _R=None

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

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

206 '''

207 self._invokation += 1

208 self._status = t = None

209 if self.verbose: # PYCHOK no cover

210 t = _cmd_stdin_(cmd, stdin)

211 self._print(t)

212 try: # invoke and write to stdin

213 r, s = _popen2(cmd, stdin)

214 if len(r) < 6 or _isin(r[:5], _Error_, _ERROR_):

215 raise ValueError(r)

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

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

218 self._status = s

219 if self.verbose: # and _R is None: # PYCHOK no cover

220 self._print(repr(r), 'stdout/-err')

221 return r

222

223 def linelimit(self, *limit):

224 '''Set and get the print line length limit.

225

226 @arg limit: New line limit (C{int}) or C{0}

227 or C{None} for unlimited.

228

229 @return: Teh previous limit (C{int}).

230 '''

231 n = self._linelimit

232 if limit:

233 m = int(limit[0] or 0)

234 self._linelimit = max(80, m) if m > 0 else (n if m < 0 else 0)

235 return n

236

237 @Property_RO

238 def _mpd(self): # meter per degree

239 return self.ellipsoid._Lpd

240

241 @property_RO

242 def _p_option(self):

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

244

245 @Property

246 def prec(self):

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

248 '''

249 return self._prec

250

251 @prec.setter # PYCHOK setter!

252 def prec(self, prec):

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

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

255

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

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

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

259 '''

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

261 if self._prec != prec:

262 _update_all(self)

263 self._prec = prec

264

265 def _print(self, line, *suffix): # PYCHOK no cover

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

267 '''

268 if self._linelimit:

269 line = clips(line, limit=self._linelimit, length=True)

270 if self.status is not None:

271 s = _COMMASPACE_(self.status, *suffix)

272 line = _SPACE_(line, Fmt.PAREN(s))

273 p = NN(self.named2, self._invokat, self.invokation)

274 printf(_COLONSPACE_(p, line))

275

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

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

278 '''

279 hold = self._Xable_path

280 if hold != path:

281 _update_all(self)

282 self._Xable_path = path

283 try:

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

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

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

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

288 hold = path

289 finally: # restore in case of error

290 if self._Xable_path != hold:

291 _update_all(self)

292 self._Xable_path = hold

293

294 @property_RO

295 def status(self):

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

297 or C{None}.

298 '''

299 return self._status

300

301 @property

302 def verbose(self):

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

304 '''

305 return self._verbose

306

307 @verbose.setter # PYCHOK setter!

308 def verbose(self, verbose):

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

310 a message around each C{RhumbSolve} invokation.

311 '''

312 self._verbose = bool(verbose)

313

314 @Property_RO

315 def version(self):

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

317 '''

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

319

320

321 class _SolveBase(_SolveCapsBase):

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

323 '''

324 _Names_Direct = \

325 _Names_Inverse = ()

326 _reverse2 = False

327 _unroll = False

328

329 @Property

330 def reverse2(self):

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

332 '''

333 return self._reverse2

334

335 @reverse2.setter # PYCHOK setter!

336 def reverse2(self, reverse2):

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

338 '''

339 reverse2 = bool(reverse2)

340 if self._reverse2 != reverse2:

341 _update_all(self)

342 self._reverse2 = reverse2

343

344 @Property

345 def unroll(self):

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

347 '''

348 return self._unroll

349

350 @unroll.setter # PYCHOK setter!

351 def unroll(self, unroll):

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

353 '''

354 unroll = bool(unroll)

355 if self._unroll != unroll:

356 _update_all(self)

357 self._unroll = unroll

358

359

360 class _SolveGDictBase(_SolveBase):

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

362 '''

363

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

365 '''New C{Solve} instance.

366

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

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

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

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

371 is specified as C{scalar}.

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

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

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

375

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

377 '''

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

379 if name:

380 self.name = name

381 if path:

382 self._setXable(path)

383

384 @Property_RO

385 def _cmdDirect(self):

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

387 '''

388 return self._cmdBasic

389

390 @Property_RO

391 def _cmdInverse(self):

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

393 '''

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

395

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

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

398 '''

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

400

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

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

403 '''

404 if arcmode:

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

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

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

408

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

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

411 '''

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

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

414

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

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

417 '''

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

419

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

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

422 '''

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

424

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

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

427 '''

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

429 # and .HeightIDWkarney._distances

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

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

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

433 return abs(float(r.a12))

434

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

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

437 '''

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

439 status=self.status, **Solve)

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

441

442

443 class _SolveGDictLineBase(_SolveGDictBase):

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

445 '''

446# _caps = 0

447# _lla1 = {}

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

449

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

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

452 if name:

453 self.name = name

454

455 self._caps = caps | Caps._AZIMUTH_LATITUDE_LONG_UNROLL

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

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

458 self._solve = solve

459

460 @Property_RO

461 def _cmdDistance(self):

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

463 '''

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

465 _, azi = _xkwds_item2(azi)

466 return lat1, lon1, azi

467

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

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

470

471 @property_RO

472 def datum(self):

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

474 '''

475 return self._solve.datum

476

477 @property_RO

478 def ellipsoid(self):

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

480 '''

481 return self._solve.ellipsoid

482

483 @Property_RO

484 def lat1(self):

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

486 '''

487 return self._lla1.lat1

488

489 @Property_RO

490 def lon1(self):

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

492 '''

493 return self._lla1.lon1

494

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

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

497 '''

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

499 lat1=self.lat1, lon1=self.lon1,

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

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

502

503

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

505

506# **) MIT License

507#

508# Copyright (C) 2016-2025 -- mrJean1 at Gmail -- All Rights Reserved.

509#

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

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

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

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

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

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

516#

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

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

519#

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

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

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

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

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

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

526# OTHER DEALINGS IN THE SOFTWARE.