1

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

3

4 u'''Base classes C{RhumbBase} and C{RhumbLineBase}, pure Python version of I{Karney}'s C++

5 classes U{Rhumb<https://GeographicLib.SourceForge.io/C++/doc/classGeographicLib_1_1Rhumb.html>}

6 and U{RhumbLine<https://GeographicLib.SourceForge.io/C++/doc/classGeographicLib_1_1RhumbLine.html>}

7 from I{GeographicLib versions 2.0} and I{2.2} and I{Karney}'s C++ example U{Rhumb intersect

8<https://SourceForge.net/p/geographiclib/discussion/1026620/thread/2ddc295e/>}.

9

10 Class L{RhumbLine} has been enhanced with methods C{Intersection} and C{NearestOn} to iteratively

11 find the intersection of two rhumb lines, respectively the nearest point on a rumb line along a

12 geodesic or perpendicular rhumb line from an other point.

13

14 For more details, see the C++ U{GeographicLib<https://GeographicLib.SourceForge.io/C++/doc/index.html>}

15 documentation, especially the U{Class List<https://GeographicLib.SourceForge.io/C++/doc/annotated.html>},

16 the background information on U{Rhumb lines<https://GeographicLib.SourceForge.io/C++/doc/rhumb.html>},

17 the utily U{RhumbSolve<https://GeographicLib.SourceForge.io/C++/doc/RhumbSolve.1.html>} and U{Online

18 rhumb line calculations<https://GeographicLib.SourceForge.io/cgi-bin/RhumbSolve>}.

19

20 Copyright (C) U{Charles Karney<mailto:Karney@Alum.MIT.edu>} (2014-2023) and licensed under the MIT/X11

21 License. For more information, see the U{GeographicLib<https://GeographicLib.SourceForge.io>} documentation.

22'''

23# make sure int/int division yields float quotient

24 from __future__ import division as _; del _ # PYCHOK semicolon

25

26# from pygeodesy.basics import unsigned0, _xinstanceof # from .karney

27 from pygeodesy.constants import EPS, EPS1, INT0, _EPSqrt as _TOL, NAN, \

28 _0_0, _0_01, _1_0, _90_0, _over

29# from pygeodesy.datums import _spherical_datum # from .formy

30# from pygeodesy.ellipsoids import _EWGS84 # from .karney

31 from pygeodesy.errors import IntersectionError, itemsorted, RhumbError, \

32 _xdatum, _xkwds, _Xorder

33# from pygeodesy.etm import ExactTransverseMercator # _MODS

34 from pygeodesy.fmath import euclid, favg, sqrt_a, fabs, Fsum

35 from pygeodesy.formy import opposing, _spherical_datum

36# from pygeodesy.fsums import Fsum # from .fmath

37 from pygeodesy.interns import NN, _coincident_, _COMMASPACE_, _Dash, \

38 _no_, _parallel_, _too_, _under

39 from pygeodesy.karney import _atan2d, Caps, _CapsBase, _diff182, _fix90, \

40 GDict, _norm180, _EWGS84, unsigned0, _xinstanceof

41# from pygeodesy.ktm import KTransverseMercator, _AlpCoeffs # _MODS

42 from pygeodesy.lazily import _ALL_DOCS, _ALL_MODS as _MODS

43# from pygeodesy.named import notOverloaded # _MODS

44 from pygeodesy.namedTuples import Distance2Tuple, LatLon2Tuple

45 from pygeodesy.props import deprecated_method, Property, Property_RO, \

46 property_RO, _update_all

47 from pygeodesy.streprs import Fmt, pairs, unstr

48 from pygeodesy.units import Float_, Lat, Lon, Meter, Radius_, Int # PYCHOK shared

49 from pygeodesy.utily import atan2d, _azireversed, _loneg, sincos2d, \

50 sincos2d_, _unrollon, _Wrap, sincos2_ # PYCHOK shared

51 from pygeodesy.vector3d import _intersect3d3, Vector3d # in .Intersection below

52

53# from math import fabs # from .fmath

54

55__all__ = ()

56__version__ = '23.10.10'

57

58_anti_ = _Dash('anti')

59_rls = [] # instances of C{RbumbLine...} to be updated

60_TRIPS = 65 # .Intersection, .NearestOn, 19+

61

62

63 class _Lat(Lat):

64 '''(INTERNAL) Latitude B{C{lat}}.

65 '''

66 def __init__(self, *lat, **Error_name):

67 kwds = _xkwds(Error_name, clip=0, Error=RhumbError)

68 Lat.__new__(_Lat, *lat, **kwds)

69

70

71 class _Lon(Lon):

72 '''(INTERNAL) Longitude B{C{lon}}.

73 '''

74 def __init__(self, *lon, **Error_name):

75 kwds = _xkwds(Error_name, clip=0, Error=RhumbError)

76 Lon.__new__(_Lon, *lon, **kwds)

77

78

79 def _update_all_rls(r):

80 '''(INTERNAL) Zap cached/memoized C{Property[_RO]}s

81 of any C{RhumbLine} instances tied to the given

82 C{Rhumb} instance B{C{r}}.

83 '''

84 # _xinstanceof(_MODS.rhumbaux.RhumbAux, _MODS.rhumbx.Rhumb, r=r)

85 _update_all(r)

86 for rl in _rls: # PYCHOK use weakref?

87 if rl._rhumb is r:

88 _update_all(rl)

89

90

91 class RhumbBase(_CapsBase):

92 '''(INTERNAL) Base class for C{rhumbaux.RhumbAux} and C{rhumbx.Rhumb}.

93 '''

94 _E = _EWGS84

95 _exact = True

96 _f_max = _0_01

97 _mTM = 6 # see .TMorder

98

99 def __init__(self, a_earth, f, exact, name):

100 '''New C{rhumbaux.RhumbAux} or C{rhumbx.Rhum}.

101 '''

102 if f is not None:

103 self.ellipsoid = a_earth, f

104 elif a_earth not in (_EWGS84, None):

105 self.ellipsoid = a_earth

106 if not exact:

107 self.exact = False

108 if name:

109 self.name = name

110

111 @Property_RO

112 def a(self):

113 '''Get the C{ellipsoid}'s equatorial radius, semi-axis (C{meter}).

114 '''

115 return self.ellipsoid.a

116

117 equatoradius = a

118

119 def ArcDirect(self, lat1, lon1, azi12, a12, outmask=Caps.LATITUDE_LONGITUDE):

120 '''Solve the I{direct rhumb} problem, optionally with area.

121

122 @arg lat1: Latitude of the first point (C{degrees90}).

123 @arg lon1: Longitude of the first point (C{degrees180}).

124 @arg azi12: Azimuth of the rhumb line (compass C{degrees}).

125 @arg a12: Angle along the rhumb line from the given to the

126 destination point (C{degrees}), can be negative.

127

128 @return: L{GDict} with 2 up to 8 items C{lat2, lon2, a12, S12,

129 lat1, lon1, azi12, s12} with the destination point's

130 latitude C{lat2} and longitude C{lon2} in C{degrees},

131 the rhumb angle C{a12} in C{degrees} and area C{S12}

132 under the rhumb line in C{meter} I{squared}.

133

134 @raise ImportError: Package C{numpy} not found or not installed,

135 only required for area C{S12} when C{B{exact}

136 is True} and L{RhumbAux}.

137

138 @note: If B{C{a12}} is large enough that the rhumb line crosses

139 a pole, the longitude of the second point is indeterminate

140 and C{NAN} is returned for C{lon2} and area C{S12}.

141

142 @note: If the given point is a pole, the cosine of its latitude is

143 taken to be C{sqrt(L{EPS})}. This position is extremely

144 close to the actual pole and allows the calculation to be

145 carried out in finite terms.

146 '''

147 s12 = a12 * self._mpd

148 return self._DirectRhumb(lat1, lon1, azi12, a12, s12, outmask)

149

150 @Property_RO

151 def b(self):

152 '''Get the C{ellipsoid}'s polar radius, semi-axis (C{meter}).

153 '''

154 return self.ellipsoid.b

155

156 polaradius = b

157

158 def _Direct(self, ll1, azi12, s12, **outmask):

159 '''(INTERNAL) Short-cut version, see .latlonBase.

160 '''

161 return self.Direct(ll1.lat, ll1.lon, azi12, s12, **outmask)

162

163 def Direct(self, lat1, lon1, azi12, s12, outmask=Caps.LATITUDE_LONGITUDE):

164 '''Solve the I{direct rhumb} problem, optionally with area.

165

166 @arg lat1: Latitude of the first point (C{degrees90}).

167 @arg lon1: Longitude of the first point (C{degrees180}).

168 @arg azi12: Azimuth of the rhumb line (compass C{degrees}).

169 @arg s12: Distance along the rhumb line from the given to

170 the destination point (C{meter}), can be negative.

171

172 @return: L{GDict} with 2 up to 8 items C{lat2, lon2, a12, S12,

173 lat1, lon1, azi12, s12} with the destination point's

174 latitude C{lat2} and longitude C{lon2} in C{degrees},

175 the rhumb angle C{a12} in C{degrees} and area C{S12}

176 under the rhumb line in C{meter} I{squared}.

177

178 @raise ImportError: Package C{numpy} not found or not installed,

179 only required for area C{S12} when C{B{exact}

180 is True} and L{RhumbAux}.

181

182 @note: If B{C{s12}} is large enough that the rhumb line crosses

183 a pole, the longitude of the second point is indeterminate

184 and C{NAN} is returned for C{lon2} and area C{S12}.

185

186 @note: If the given point is a pole, the cosine of its latitude is

187 taken to be C{sqrt(L{EPS})}. This position is extremely

188 close to the actual pole and allows the calculation to be

189 carried out in finite terms.

190 '''

191 a12 = _over(s12, self._mpd)

192 return self._DirectRhumb(lat1, lon1, azi12, a12, s12, outmask)

193

194 def Direct8(self, lat1, lon1, azi12, s12, outmask=Caps.LATITUDE_LONGITUDE_AREA):

195 '''Like method L{Rhumb.Direct} but returning a L{Rhumb8Tuple} with area C{S12}.

196 '''

197 return self.Direct(lat1, lon1, azi12, s12, outmask=outmask).toRhumb8Tuple()

198

199 def _DirectLine(self, ll1, azi12, **caps_name):

200 '''(INTERNAL) Short-cut version, see .latlonBase.

201 '''

202 return self.DirectLine(ll1.lat, ll1.lon, azi12, **caps_name)

203

204 def DirectLine(self, lat1, lon1, azi12, **caps_name):

205 '''Define a C{RhumbLine} in terms of the I{direct} rhumb

206 problem to compute several points on a single rhumb line.

207

208 @arg lat1: Latitude of the first point (C{degrees90}).

209 @arg lon1: Longitude of the first point (C{degrees180}).

210 @arg azi12: Azimuth of the rhumb line (compass C{degrees}).

211 @kwarg caps_name: Optional keyword arguments C{B{name}=NN} and

212 C{B{caps}=Caps.STANDARD}, a bit-or'ed combination of

213 L{Caps} values specifying the required capabilities.

214 Include C{Caps.LINE_OFF} if updates to the B{C{rhumb}}

215 should I{not} be reflected in this rhumb line.

216

217 @return: A C{RhumbLine...} instance and invoke its method

218 C{.Position} to compute each point.

219

220 @note: Updates to this rhumb are reflected in the returned

221 rhumb line, unless C{B{caps} |= Caps.LINE_OFF}.

222 '''

223 return self._RhumbLine(self, lat1, lon1, azi12, **caps_name)

224

225 Line = DirectLine # synonyms

226

227 def _DirectRhumb(self, lat1, lon1, azi12, a12, s12, outmask):

228 '''(INTERNAL) See methods C{.ArcDirect} and C{.Direct}.

229 '''

230 rl = self._RhumbLine(self, lat1, lon1, azi12, caps=Caps.LINE_OFF,

231 name=self.name)

232 return rl._Position(a12, s12, outmask | self._debug) # lat2, lon2, S12

233

234 @Property

235 def ellipsoid(self):

236 '''Get this rhumb's ellipsoid (L{Ellipsoid}).

237 '''

238 return self._E

239

240 @ellipsoid.setter # PYCHOK setter!

241 def ellipsoid(self, a_earth_f):

242 '''Set this rhumb's ellipsoid (L{Ellipsoid}, L{Ellipsoid2}, L{Datum} or

243 L{a_f2Tuple}) or (equatorial) radius and flattening (2-tuple C{(a, f)}).

244

245 @raise RhumbError: If C{abs(B{f}} exceeds non-zero C{f_max} and C{exact=False}.

246 '''

247 E = _spherical_datum(a_earth_f, Error=RhumbError).ellipsoid

248 if self._E != E:

249 self._exactest(self.exact, E, self.f_max)

250 _update_all_rls(self)

251 self._E = E

252

253 @Property

254 def exact(self):

255 '''Get the I{exact} option (C{bool}).

256 '''

257 return self._exact

258

259 @exact.setter # PYCHOK setter!

260 def exact(self, exact):

261 '''Set the I{exact} option (C{bool}). If C{True}, use I{exact} rhumb

262 expressions, otherwise a series expansion (accurate for oblate or

263 prolate ellipsoids with C{abs(flattening)} below C{f_max}.

264

265 @raise RhumbError: If C{B{exact}=False} and C{abs(flattening})

266 exceeds non-zero C{f_max}.

267

268 @see: Option U{B{-s}<https://GeographicLib.SourceForge.io/C++/doc/RhumbSolve.1.html>}

269 and U{ACCURACY<https://GeographicLib.SourceForge.io/C++/doc/RhumbSolve.1.html#ACCURACY>}.

270 '''

271 x = bool(exact)

272 if self._exact != x:

273 self._exactest(x, self.ellipsoid, self.f_max)

274 _update_all_rls(self)

275 self._exact = x

276

277 def _exactest(self, exact, ellipsoid, f_max):

278 # Helper for property setters C{ellipsoid}, C{exact} and C{f_max}

279 if fabs(ellipsoid.f) > f_max > 0 and not exact:

280 raise RhumbError(exact=exact, f=ellipsoid.f, f_max=f_max)

281

282 @Property_RO

283 def f(self):

284 '''Get the C{ellipsoid}'s flattening (C{float}).

285 '''

286 return self.ellipsoid.f

287

288 flattening = f

289

290 @property

291 def f_max(self):

292 '''Get the I{max.} flattening (C{float}).

293 '''

294 return self._f_max

295

296 @f_max.setter # PYCHOK setter!

297 def f_max(self, f_max): # PYCHOK no cover

298 '''Set the I{max.} flattening, not to exceed (C{float}).

299

300 @raise RhumbError: If C{exact=False} and C{abs(flattening})

301 exceeds non-zero C{f_max}.

302 '''

303 f = Float_(f_max=f_max, low=_0_0, high=EPS1)

304 if self._f_max != f:

305 self._exactest(self.exact, self.ellipsoid, f)

306 self._f_max = f

307

308 def _Inverse(self, ll1, ll2, wrap, **outmask):

309 '''(INTERNAL) Short-cut version, see .latlonBase.

310 '''

311 if wrap:

312 ll2 = _unrollon(ll1, _Wrap.point(ll2))

313 return self.Inverse(ll1.lat, ll1.lon, ll2.lat, ll2.lon, **outmask)

314

315 def Inverse(self, lat1, lon1, lat2, lon2, outmask=Caps.AZIMUTH_DISTANCE):

316 '''Solve the I{inverse rhumb} problem.

317

318 @arg lat1: Latitude of the first point (C{degrees90}).

319 @arg lon1: Longitude of the first point (C{degrees180}).

320 @arg lat2: Latitude of the second point (C{degrees90}).

321 @arg lon2: Longitude of the second point (C{degrees180}).

322

323 @return: L{GDict} with 4 to 9 items C{lat1, lon1, lat2, lon2,

324 azi12, azi21, s12, a12, S12}, the rhumb line's azimuth

325 C{azi12} and I{reverse} azimuth C{azi21}, both in

326 compass C{degrees} between C{-180} and C{+180}, the

327 rhumb distance C{s12} and rhumb angle C{a12} between

328 both points in C{meter} respectively C{degrees} and

329 the area C{S12} under the rhumb line in C{meter}

330 I{squared}.

331

332 @raise ImportError: Package C{numpy} not found or not installed,

333 only required for L{RhumbAux} area C{S12}

334 when C{B{exact} is True}.

335

336 @note: The shortest rhumb line is found. If the end points are

337 on opposite meridians, there are two shortest rhumb lines

338 and the East-going one is chosen.

339

340 @note: If either point is a pole, the cosine of its latitude is

341 taken to be C{sqrt(L{EPS})}. This position is extremely

342 close to the actual pole and allows the calculation to be

343 carried out in finite terms.

344 '''

345 r = GDict(lat1=lat1, lon1=lon1, lat2=lat2, lon2=lon2, name=self.name)

346 Cs = Caps

347 if (outmask & Cs.AZIMUTH_DISTANCE_AREA):

348 lon12, _ = _diff182(lon1, lon2, K_2_0=True)

349 y, x, s1, s2 = self._Inverse4(lon12, r, outmask)

350 if (outmask & Cs.AZIMUTH):

351 z = _atan2d(y, x)

352 r.set_(azi12=z, azi21=_azireversed(z))

353 if (outmask & Cs.AREA):

354 S12 = self._S12d(s1, s2, lon12)

355 r.set_(S12=unsigned0(S12)) # like .gx

356 return r

357

358 def _Inverse4(self, lon12, r, outmask): # PYCHOK no cover

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

360 _MODS.named.notOverloaded(self, lon12, r, Caps.toStr(outmask))

361

362 def Inverse8(self, lat1, lon1, azi12, s12, outmask=Caps.AZIMUTH_DISTANCE_AREA):

363 '''Like method L{Rhumb.Inverse} but returning a L{Rhumb8Tuple} with area C{S12}.

364 '''

365 return self.Inverse(lat1, lon1, azi12, s12, outmask=outmask).toRhumb8Tuple()

366

367 def _InverseLine(self, ll1, ll2, wrap, **caps_name):

368 '''(INTERNAL) Short-cut version, see .latlonBase.

369 '''

370 if wrap:

371 ll2 = _unrollon(ll1, _Wrap.point(ll2))

372 return self.InverseLine(ll1.lat, ll1.lon, ll2.lat, ll2.lon, **caps_name)

373

374 def InverseLine(self, lat1, lon1, lat2, lon2, **caps_name):

375 '''Define a C{RhumbLine} in terms of the I{inverse} rhumb problem.

376

377 @arg lat1: Latitude of the first point (C{degrees90}).

378 @arg lon1: Longitude of the first point (C{degrees180}).

379 @arg lat2: Latitude of the second point (C{degrees90}).

380 @arg lon2: Longitude of the second point (C{degrees180}).

381 @kwarg caps_name: Optional keyword arguments C{B{name}=NN} and

382 C{B{caps}=Caps.STANDARD}, a bit-or'ed combination of

383 L{Caps} values specifying the required capabilities.

384 Include C{Caps.LINE_OFF} if updates to the B{C{rhumb}}

385 should I{not} be reflected in this rhumb line.

386

387 @return: A C{RhumbLine...} instance and invoke its method

388 C{ArcPosition} or C{Position} to compute points.

389

390 @note: Updates to this rhumb are reflected in the returned

391 rhumb line, unless C{B{caps} |= Caps.LINE_OFF}.

392 '''

393 r = self.Inverse(lat1, lon1, lat2, lon2, outmask=Caps.AZIMUTH)

394 return self._RhumbLine(self, lat1, lon1, r.azi12, **caps_name)

395

396 @Property_RO

397 def _mpd(self): # PYCHOK no cover

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

399 _MODS.named.notOverloaded(self)

400

401 @property_RO

402 def _RhumbLine(self): # PYCHOK no cover

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

404 _MODS.named.notOverloaded(self, underOK=True)

405

406 def _S12d(self, s1, s2, lon): # PYCHOK no cover

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

408 _MODS.named.notOverloaded(self, s1, s2, lon)

409

410 @Property

411 def TMorder(self):

412 '''Get the I{Transverse Mercator} order (C{int}, 4, 5, 6, 7 or 8).

413 '''

414 return self._mTM

415

416 @TMorder.setter # PYCHOK setter!

417 def TMorder(self, order):

418 '''Set the I{Transverse Mercator} order (C{int}, 4, 5, 6, 7 or 8).

419

420 @note: Setting C{TMorder} turns property C{exact} off, but only

421 for L{Rhumb} instances.

422 '''

423 m = _Xorder(_MODS.ktm._AlpCoeffs, RhumbError, TMorder=order)

424 if self._mTM != m:

425 _update_all_rls(self)

426 self._mTM = m

427 if self.exact and isinstance(self, _MODS.rhumbx.Rhumb):

428 self.exact = False

429

430 def toStr(self, prec=6, sep=_COMMASPACE_, **unused): # PYCHOK no cover

431 '''I{Must be overloaded}.'''

432 _MODS.named.notOverloaded(self, prec=prec, sep=sep)

433

434

435 class RhumbLineBase(_CapsBase):

436 '''(INTERNAL) Base class for C{rhumbaux.RhumbLineAux} and C{rhumbx.RhumbLine}.

437 '''

438 _azi12 = _0_0

439 _calp = _1_0

440# _caps = 0

441# _debug = 0

442# _lat1 = _0_0

443# _lon1 = _0_0

444# _lon12 = _0_0

445 _Rhumb = RhumbBase # compatible C{Rhumb} class

446 _rhumb = None # C{Rhumb} instance

447 _salp = _0_0

448 _talp = _0_0

449

450 def __init__(self, rhumb, lat1, lon1, azi12, caps=Caps.STANDARD, name=NN):

451 '''New C{RhumbLine}.

452 '''

453 _xinstanceof(self._Rhumb, rhumb=rhumb)

454

455 self._lat1 = _Lat(lat1=_fix90(lat1))

456 self._lon1 = _Lon(lon1= lon1)

457 self._lon12 = _norm180(self._lon1)

458 if azi12: # non-zero, non-None

459 self.azi12 = _norm180(azi12)

460

461 n = name or rhumb.name

462 if n:

463 self.name=n

464

465 self._caps = caps

466 self._debug |= (caps | rhumb._debug) & Caps._DEBUG_DIRECT_LINE

467 if (caps & Caps.LINE_OFF): # copy to avoid updates

468 self._rhumb = rhumb.copy(deep=False, name=_under(rhumb.name))

469 else:

470 self._rhumb = rhumb

471 _rls.append(self)

472

473 def __del__(self): # XXX use weakref?

474 if _rls: # may be empty or None

475 try: # PYCHOK no cover

476 _rls.remove(self)

477 except (TypeError, ValueError):

478 pass

479 self._rhumb = None

480 # _update_all(self) # throws TypeError during Python 2 cleanup

481

482 def ArcPosition(self, a12, outmask=Caps.LATITUDE_LONGITUDE):

483 '''Compute a point at a given angular distance on this rhumb line.

484

485 @arg a12: The angle along this rhumb line from its origin to the

486 point (C{degrees}), can be negative.

487 @kwarg outmask: Bit-or'ed combination of L{Caps} values specifying

488 the quantities to be returned.

489

490 @return: L{GDict} with 4 to 8 items C{azi12, a12, s12, S12, lat2,

491 lon2, lat1, lon1} with latitude C{lat2} and longitude

492 C{lon2} of the point in C{degrees}, the rhumb distance

493 C{s12} in C{meter} from the start point of and the area

494 C{S12} under this rhumb line in C{meter} I{squared}.

495

496 @raise ImportError: Package C{numpy} not found or not installed,

497 only required for L{RhumbLineAux} area C{S12}

498 when C{B{exact} is True}.

499

500 @note: If B{C{a12}} is large enough that the rhumb line crosses a

501 pole, the longitude of the second point is indeterminate and

502 C{NAN} is returned for C{lon2} and area C{S12}.

503

504 If the first point is a pole, the cosine of its latitude is

505 taken to be C{sqrt(L{EPS})}. This position is extremely

506 close to the actual pole and allows the calculation to be

507 carried out in finite terms.

508 '''

509 return self._Position(a12, self.degrees2m(a12), outmask)

510

511 @Property

512 def azi12(self):

513 '''Get this rhumb line's I{azimuth} (compass C{degrees}).

514 '''

515 return self._azi12

516

517 @azi12.setter # PYCHOK setter!

518 def azi12(self, azi12):

519 '''Set this rhumb line's I{azimuth} (compass C{degrees}).

520 '''

521 z = _norm180(azi12)

522 if self._azi12 != z:

523 if self._rhumb:

524 _update_all(self)

525 self._azi12 = z

526 self._salp, self._calp = t = sincos2d(z) # no NEG0

527 self._talp = _over(*t)

528

529 @property_RO

530 def azi12_sincos2(self): # PYCHOK no cover

531 '''Get the sine and cosine of this rhumb line's I{azimuth} (2-tuple C{(sin, cos)}).

532 '''

533 return self._scalp, self._calp

534

535 def degrees2m(self, angle):

536 '''Convert an angular distance along this rhumb line to C{meter}.

537

538 @arg angle: Angular distance (C{degrees}).

539

540 @return: Distance (C{meter}).

541 '''

542 return float(angle) * self.rhumb._mpd

543

544 @deprecated_method

545 def distance2(self, lat, lon): # PYCHOK no cover

546 '''DEPRECATED on 23.09.23, use method L{RhumbLineAux.Inverse} or L{RhumbLine.Inverse}.

547

548 @return: A L{Distance2Tuple}C{(distance, initial)} with the C{distance}

549 in C{meter} and C{initial} bearing (azimuth) in C{degrees}.

550 '''

551 r = self.Inverse(lat, lon)

552 return Distance2Tuple(r.s12, r.azi12)

553

554 @property_RO

555 def ellipsoid(self):

556 '''Get this rhumb line's ellipsoid (L{Ellipsoid}).

557 '''

558 return self.rhumb.ellipsoid

559

560 @property_RO

561 def exact(self):

562 '''Get this rhumb line's I{exact} option (C{bool}).

563 '''

564 return self.rhumb.exact

565

566 def Intersecant2(self, lat0, lon0, radius, **tol_eps):

567 '''Compute the intersection(s) of this rhumb line and a circle.

568

569 @arg lat0: Latitude of the circle center (C{degrees}).

570 @arg lon0: Longitude of the circle center (C{degrees}).

571 @arg radius: Radius of the circle (C{meter}, conventionally).

572 @kwarg tol_eps: Optional keyword arguments, see method

573 method L{Intersection} for further details.

574

575 @return: 2-Tuple C{(P, Q)} with the 2 intersections

576 (representing a chord), each a L{GDict} from method

577 L{Intersection} extended to 14 items with C{azi03,

578 a03, s03, lat3, lon3} for the chord center C{lat3,

579 lon3} and the angle C{a03}, distance C{s03}, azimuth

580 C{azi03} and start point C{lat0, lon0} of the rhumb

581 line perpendicular to this rhumb line. If this

582 rhumb line is tangential to the circle, both points

583 are the same L{GDict} instance with rhumb distances

584 C{s02} and C{s03} near-equal to the B{C{radius}}.

585

586 @raise IntersectionError: The circle and this rhumb line

587 do not intersect.

588

589 @raise UnitError: Invalid B{C{radius}}.

590 '''

591 r = Radius_(radius)

592 p = q = self.NearestOn(lat0, lon0, exact=None, **tol_eps)

593 d = q.s02

594 t = dict(azi03=q.azi02, a03=q.a02, s03=d, lat3=q.lat2, lon3=q.lon2)

595 if d < r:

596 a = sqrt_a(r, d)

597 s = q.s12

598 i = q.iteration

599 n = self.name or self.Intersecant2.__name__

600 t.update(lat0=q.lat1, lon0=q.lon1) # or lat0, lon0

601 p = self.Position(s + a).set_(name=n, **t)

602 q = self.Position(s - a).set_(name=n, **t)

603 p._iteration = q._iteration = i

604 elif d > r:

605 t = unstr(self.Intersecant2, lat0, lon0, radius=radius, **tol_eps)

606 raise IntersectionError(_no_(t), txt=_too_(Fmt.distant(d)))

607 else: # tangential

608 q.set_(**t) # == p.set(_**t)

609 return p, q

610

611 def intersection2(self, other, **tol_eps): # PYCHOK no cover

612 '''DEPRECATED on 23.10.10, use method L{Intersection}.'''

613 p = self.Intersection(other, **tol_eps)

614 r = LatLon2Tuple(p.lat2, p.lon2, name=self.intersection2.__name__)

615 r._iteration = p.iteration

616 return r

617

618 def Intersection(self, other, tol=_TOL, **eps):

619 '''I{Iteratively} find the intersection of this and an other rhumb line.

620

621 @arg other: The other rhumb line (C{RhumbLine}) or C{None} for a

622 rhumb line perpendicular to this one.

623 @kwarg tol: Tolerance for longitudinal convergence and parallel

624 error (C{degrees}).

625 @kwarg eps: Tolerance for L{pygeodesy.intersection3d3} (C{EPS}).

626

627 @return: The intersection point, a L{Position}-like L{GDict} with

628 12 items C{azi12, a12, s12, lat2, lat1, lat0, lon1, lon2,

629 lon0, azi02, a02, s02} with C{a02} and C{s02} the rhumb

630 angle and rhumb distance between the intersection C{lat2,

631 lon2} point and the start point C{lat0, lon0} on the

632 B{C{other}} rhumb line and the latter's azimuth C{azi02}.

633 See method L{Position} for further details.

634

635 @raise IntersectionError: No convergence for this B{C{tol}} or

636 no intersection for an other reason.

637

638 @see: Methods C{distance2} and C{NearestOn} and function

639 L{pygeodesy.intersection3d3}.

640

641 @note: Each iteration involves a round trip to this rhumb line's

642 L{ExactTransverseMercator} or L{KTransverseMercator}

643 projection and function L{pygeodesy.intersection3d3} in

644 that domain.

645 '''

646 _xinstanceof(RhumbLineBase, other=other)

647 _xdatum(self.rhumb, other.rhumb, Error=RhumbError)

648 try:

649 if self.others(other) is self:

650 raise ValueError(_coincident_)

651 # make invariants and globals locals

652 _s_3d, s_az = self._xTM3d, self.azi12

653 _o_3d, o_az = other._xTM3d, other.azi12

654 p = opposing(s_az, o_az, margin=tol)

655 if p is not None: # == t in (True, False)

656 raise ValueError(_anti_(_parallel_) if p else _parallel_)

657 _diff = euclid # approximate length

658 _i3d3 = _intersect3d3 # NOT .vector3d.intersection3d3

659 _LL2T = LatLon2Tuple

660 _xTMr = self.xTM.reverse # ellipsoidal or spherical

661 # use halfway point as initial estimate

662 p = _LL2T(favg(self.lat1, other.lat1),

663 favg(self.lon1, other.lon1))

664 for i in range(1, _TRIPS):

665 v = _i3d3(_s_3d(p), s_az, # point + bearing

666 _o_3d(p), o_az, useZ=False, **eps)[0]

667 t = _xTMr(v.x, v.y, lon0=p.lon) # PYCHOK Reverse4Tuple

668 d = _diff(t.lon - p.lon, t.lat) # PYCHOK t.lat + p.lat - p.lat

669 p = _LL2T(t.lat + p.lat, t.lon) # PYCHOK t.lon + p.lon = lon0

670 if d < tol: # 19 trips

671 lat, lon = p.lat, p.lon # PYCHOK p...

672 t = other.Inverse(lat, lon, outmask=Caps.DISTANCE)

673 r = self.Inverse( lat, lon, outmask=Caps.DISTANCE)

674 p = GDict(azi12=self.azi12, a12=r.a12, s12=r.s12,

675 lat2=lat, lat1=self.lat1, lat0=other.lat1,

676 lon2=lon, lon1=self.lon1, lon0=other.lon1,

677 azi02=other.azi12, a02=t.a12, s02=t.s12,

678 name=self.name or self.Intersection.__name__)

679 p._iteration = i

680 return p

681 else:

682 raise ValueError(Fmt.no_convergence(d))

683

684 except Exception as x:

685 t = unstr(self.Intersection, other, tol=tol, eps=eps)

686 raise IntersectionError(_no_(t), cause=x)

687

688 def Inverse(self, lat2, lon2, wrap=False, **outmask):

689 '''Return the rhumb angle, distance, azimuth, I{reverse} azimuth, etc. of

690 a rhumb line between the given point and this rhumb line's start point.

691

692 @arg lat2: Latitude of the point (C{degrees}).

693 @arg lon2: Longitude of the points (C{degrees}).

694 @kwarg wrap: If C{True}, wrap or I{normalize} and unroll B{C{lat2}}

695 and B{C{lon2}} (C{bool}).

696

697 @return: L{GDict} with 8 items C{a12, s12, azi12, azi21, lat1, lon1,

698 lat2, lon2}, the rhumb angle C{a12} and rhumb distance C{s12}

699 between both points in C{degrees} respectively C{meter}, the

700 rhumb line's azimuth C{azi12} and I{reverse} azimuth C{azi21}

701 both in compass C{degrees} between C{-180} and C{+180}.

702 '''

703 if wrap:

704 _, lat2, lon2 = _Wrap.latlon3(self.lon1, _fix90(lat2), lon2, wrap)

705 r = self.rhumb.Inverse(self.lat1, self.lon1, lat2, lon2, **outmask)

706 return r

707

708 @Property_RO

709 def isLoxodrome(self):

710 '''Is this rhumb line a meridional (C{None}), a parallel

711 (C{False}) or a C{True} loxodrome?

712

713 @see: I{Osborne's} U{2.5 Rumb lines and loxodromes

714 <https://Zenodo.org/record/35392>}, page 37.

715 '''

716 return bool(self._salp) if self._calp else None

717

718 @Property_RO

719 def lat1(self):

720 '''Get this rhumb line's latitude (C{degrees90}).

721 '''

722 return self._lat1

723

724 @Property_RO

725 def lon1(self):

726 '''Get this rhumb line's longitude (C{degrees180}).

727 '''

728 return self._lon1

729

730 @Property_RO

731 def latlon1(self):

732 '''Get this rhumb line's lat- and longitude (L{LatLon2Tuple}C{(lat, lon)}).

733 '''

734 return LatLon2Tuple(self.lat1, self.lon1)

735

736 def m2degrees(self, distance):

737 '''Convert a distance along this rhumb line to an angular distance.

738

739 @arg distance: Distance (C{meter}).

740

741 @return: Angular distance (C{degrees}).

742 '''

743 return _over(float(distance), self.rhumb._mpd)

744

745 @property_RO

746 def _mu1(self): # PYCHOK no cover

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

748 _MODS.named.notOverloaded(self, underOK=True)

749

750 def _mu2lat(self, mu2): # PYCHOK no cover

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

752 _MODS.named.notOverloaded(self, mu2, underOK=True)

753

754 @deprecated_method

755 def nearestOn4(self, lat, lon, **exact_eps_est_tol):

756 '''DEPRECATED on 23.10.10, use method L{NearestOn}.'''

757 p = self.NearestOn(lat, lon, **exact_eps_est_tol)

758 r = _MODS.deprecated.NearestOn4Tuple(p.lat2, p.lon2, p.s12, p.azi02,

759 name=self.nearestOn4.__name__)

760 r._iteration = p.iteration

761 return r

762

763 def NearestOn(self, lat0, lon0, exact=None, eps=EPS, est=None, tol=_TOL):

764 '''I{Iteratively} locate the point on this rhumb line nearest to the

765 given point, in part transcoded from I{Karney}'s C++ U{rhumb-intercept

766 <https://SourceForge.net/p/geographiclib/discussion/1026620/thread/2ddc295e/>}.

767

768 @arg lat0: Latitude of the point (C{degrees}).

769 @arg lon0: Longitude of the point (C{degrees}).

770 @kwarg exact: If C{None}, use a rhumb line perpendicular to this rhumb

771 line, otherwise use an I{exact} C{Geodesic...} from the

772 given point perpendicular to this rhumb line (C{bool} or

773 C{Geodesic...}), see method L{Ellipsoid.geodesic_}.

774 @kwarg eps: Optional tolerance for L{pygeodesy.intersection3d3}

775 (C{EPS}), used only if C{B{exact} is None}.

776 @kwarg est: Optional, initial estimate for the distance C{s13} of

777 the intersection I{along} this rhumb line (C{meter}),

778 used only if C{B{exact} is not None}.

779 @kwarg tol: Longitudinal convergence tolerance (C{degrees}) or the

780 distance tolerance (C(meter)) when C{B{exact} is None},

781 respectively C{not None}.

782

783 @return: The nearest point on this rhumb line, a L{GDict} from method

784 L{Intersection} if B{C{exact}=None}. If B{C{exact}} is not

785 C{None}, a L{GDict} of 13 items C{azi12, a12, s12, lat2, lat1,

786 lon2, lon1, a02, s02, azi0, azi2, lat0, lon0} with C{a02} and

787 C{s02} the angle respectively distance from the given C{lat0,

788 lon0} to the nearest C{lat2, lon2} point along the specified

789 C{geodetic} and C{azi0} and C{azi2} the (forward) azimuth at

790 the given respectively the nearest point. The latter is always

791 perpendicular to this rhumb line's azimuth. See method

792 L{Position} for further details.

793

794 @raise ImportError: I{Karney}'s U{geographiclib

795 <https://PyPI.org/project/geographiclib>}

796 package not found or not installed.

797

798 @raise IntersectionError: No convergence for this B{C{eps}} or

799 no intersection for an other reason.

800

801 @note: The (forward) azimuth C{azi2} at the nearest point is

802 always perpendicular to this rhumb line's azimuth.

803

804 @see: Methods C{distance2}, C{Intersecant2} and C{Intersection}

805 and function L{pygeodesy.intersection3d3}.

806 '''

807 Cs = Caps

808 if exact is None:

809 z = _norm180(self.azi12 + _90_0) # perpendicular azimuth

810 rl = RhumbLineBase(self.rhumb, lat0, lon0, z, caps=Cs.LINE_OFF)

811 p = self.Intersection(rl, tol=tol, eps=eps)

812

813 else: # C{rhumb-intercept}

814 azi = self.azi12

815 szi = self._salp

816 E = self.ellipsoid

817 _gI = E.geodesic_(exact=exact).Inverse

818 gm = Cs.STANDARD | Cs._REDUCEDLENGTH_GEODESICSCALE # ^ Cs.DISTANCE_IN

819 if est is None: # get an estimate from the "perpendicular" geodesic

820 r = _gI(self.lat1, self.lon1, lat0, lon0, outmask=Cs.AZIMUTH_DISTANCE)

821 d, _ = _diff182(r.azi2, azi, K_2_0=True)

822 _, s12 = sincos2d(d)

823 s12 *= r.s12 # signed

824 else:

825 s12 = Meter(est=est)

826 try:

827 tol = Float_(tol=tol, low=EPS, high=None)

828 # def _over(p, q): # see @note at RhumbLine[Aux].Position

829 # return (p / (q or _copysign(tol, q))) if isfinite(q) else NAN

830

831 _ErT = E.rocPrimeVertical # aka rocTransverse

832 _S12 = Fsum(s12).fsum2_

833 for i in range(1, _TRIPS): # suffix 1 == C++ 2, 2 == C++ 3

834 p = self.Position(s12) # outmask = Cs.LATITUDE_LONGITUDE

835 r = _gI(lat0, lon0, p.lat2, p.lon2, outmask=gm)

836 d, _ = _diff182(azi, r.azi2, K_2_0=True)

837 s, c, s2, c2 = sincos2d_(d, r.lat2)

838 c2 *= _ErT(r.lat2)

839 s *= _over(s2 * szi, c2) - _over(s * r.M21, r.m12)

840 s12, t = _S12(c / s) # XXX _over?

841 if fabs(t) < tol: # or fabs(c) < EPS

842 break

843 p.set_(azi0=r.azi1, azi2=r.azi2, a02=r.a12, s02=r.s12,

844 lat0=lat0, lon0=lon0, name=self.name or

845 self.NearestOn.__name__)

846 p._iteration = i

847 except Exception as x: # Fsum(NAN) Value-, ZeroDivisionError

848 t = unstr(self.NearestOn, lat0, lon0, tol=tol, exact=exact,

849 iteration=i, eps=eps, est=est)

850 raise IntersectionError(_no_(t), cause=x)

851

852 return p

853

854 def Position(self, s12, outmask=Caps.LATITUDE_LONGITUDE):

855 '''Compute a point at a given distance on this rhumb line.

856

857 @arg s12: The distance along this rhumb line from its origin to

858 the point (C{meters}), can be negative.

859 @kwarg outmask: Bit-or'ed combination of L{Caps} values specifying

860 the quantities to be returned.

861

862 @return: L{GDict} with 4 to 8 items C{azi12, a12, s12, S12, lat2,

863 lat1. lon2, lon1} with latitude C{lat2} and longitude

864 C{lon2} of the point in C{degrees}, the rhumb angle C{a12}

865 in C{degrees} from the start point of and the area C{S12}

866 under this rhumb line in C{meter} I{squared}.

867

868 @raise ImportError: Package C{numpy} not found or not installed,

869 only required for L{RhumbLineAux} area C{S12}

870 when C{B{exact} is True}.

871

872 @note: If B{C{s12}} is large enough that the rhumb line crosses a

873 pole, the longitude of the second point is indeterminate and

874 C{NAN} is returned for C{lon2} and area C{S12}.

875

876 If the first point is a pole, the cosine of its latitude is

877 taken to be C{sqrt(L{EPS})}. This position is extremely

878 close to the actual pole and allows the calculation to be

879 carried out in finite terms.

880 '''

881 return self._Position(self.m2degrees(s12), s12, outmask)

882

883 def _Position(self, a12, s12, outmask):

884 '''(INTERNAL) C{Arc-/Position} helper.

885 '''

886 r = GDict(azi12=self.azi12, a12=a12, s12=s12, name=self.name)

887 Cs = Caps

888 if (outmask & Cs.LATITUDE_LONGITUDE_AREA):

889 if a12 or s12:

890 mu12 = self._calp * a12

891 mu2 = self._mu1 + mu12

892 if fabs(mu2) > 90: # past pole

893 mu2 = _norm180(mu2) # reduce to [-180, 180)

894 if fabs(mu2) > 90: # point on anti-meridian

895 mu2 = _norm180(_loneg(mu2))

896 lat2 = self._mu2lat(mu2)

897 lon2 = S12 = NAN

898 else:

899 lat2, lon2, S1, S2 = self._Position4(a12, mu2, s12, mu12)

900 if (outmask & Cs.AREA):

901 S12 = self.rhumb._S12d(S1, S2, lon2)

902 S12 = unsigned0(S12) # like .gx

903# else:

904# S12 = None # unused

905 if (outmask & Cs.LONGITUDE):

906 if (outmask & Cs.LONG_UNROLL):

907 lon2 += self.lon1

908 else:

909 lon2 = _norm180(self._lon12 + lon2)

910 else: # coincident

911 lat2, lon2 = self.latlon1

912 S12 = _0_0

913

914 if (outmask & Cs.AREA):

915 r.set_(S12=S12)

916 if (outmask & Cs.LATITUDE):

917 r.set_(lat2=lat2, lat1=self.lat1)

918 if (outmask & Cs.LONGITUDE):

919 r.set_(lon2=lon2, lon1=self.lon1)

920 return r

921

922 def _Position4(self, a12, mu2, s12, mu12): # PYCHOK no cover

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

924 _MODS.named.notOverloaded(self, a12, s12, mu2, mu12)

925

926 @Property_RO

927 def rhumb(self):

928 '''Get this rhumb line's rhumb (L{RhumbAux} or L{Rhumb}).

929 '''

930 return self._rhumb

931

932 def toStr(self, prec=6, sep=_COMMASPACE_, **unused): # PYCHOK signature

933 '''Return this C{RhumbLine} as string.

934

935 @kwarg prec: The C{float} precision, number of decimal digits (0..9).

936 Trailing zero decimals are stripped for B{C{prec}} values

937 of 1 and above, but kept for negative B{C{prec}} values.

938 @kwarg sep: Separator to join (C{str}).

939

940 @return: C{RhumbLine} (C{str}).

941 '''

942 d = dict(rhumb=self.rhumb, lat1=self.lat1, lon1=self.lon1,

943 azi12=self.azi12, exact=self.exact,

944 TMorder=self.TMorder, xTM=self.xTM)

945 return sep.join(pairs(itemsorted(d, asorted=False), prec=prec))

946

947 @property_RO

948 def TMorder(self):

949 '''Get this rhumb line's I{Transverse Mercator} order (C{int}, 4, 5, 6, 7 or 8).

950 '''

951 return self.rhumb.TMorder

952

953 @Property_RO

954 def xTM(self):

955 '''Get this rhumb line's I{Transverse Mercator} projection (L{ExactTransverseMercator}

956 if I{exact} and I{ellipsoidal}, otherwise L{KTransverseMercator} for C{TMorder}).

957 '''

958 E = self.ellipsoid

959 # ExactTransverseMercator doesn't handle spherical earth models

960 return _MODS.etm.ExactTransverseMercator(E) if self.exact and E.isEllipsoidal else \

961 _MODS.ktm.KTransverseMercator(E, TMorder=self.TMorder)

962

963 def _xTM3d(self, latlon0, z=INT0, V3d=Vector3d):

964 '''(INTERNAL) C{xTM.forward} this C{latlon1} to C{V3d} with B{C{latlon0}}

965 as current intersection estimate and central meridian.

966 '''

967 t = self.xTM.forward(self.lat1 - latlon0.lat, self.lon1, lon0=latlon0.lon)

968 return V3d(t.easting, t.northing, z)

969

970

971__all__ += _ALL_DOCS(RhumbBase, RhumbLineBase)

972

973 if __name__ == '__main__':

974

975 from pygeodesy import printf, Rhumb as R, RhumbAux as A

976

977 A = A(_EWGS84).Line(30, 0, 45)

978 R = R(_EWGS84).Line(30, 0, 45)

979

980 for i in range(1, 10):

981 s = .5e6 + 1e6 / i

982 a = A.Position(s).lon2

983 r = R.Position(s).lon2

984 e = (fabs(a - r) / a) if a else 0

985 printf('# Position.lon2 %.14f vs %.14f, diff %g', r, a, e)

986

987 for exact in (None, False, True):

988 for est in (None, 1e6):

989 a = A.NearestOn(60, 0, exact=exact, est=est)

990 r = R.NearestOn(60, 0, exact=exact, est=est)

991 printf('# %s, iteration=%s, exact=%s, est=%s\n# %s, iteration=%s',

992 a.toRepr(), a.iteration, exact, est,

993 r.toRepr(), r.iteration, nl=1)

994

995# % python3 -m pygeodesy.rhumbBase

996

997# Position.lon2 11.61455846901637 vs 11.61455846901637, diff 6.11769e-16

998# Position.lon2 7.58982302826842 vs 7.58982302826842, diff 1.17022e-16

999# Position.lon2 6.28526067416369 vs 6.28526067416369, diff 2.82623e-16

1000# Position.lon2 5.63938995325146 vs 5.63938995325146, diff 4.72486e-16

1001# Position.lon2 5.25385527435707 vs 5.25385527435707, diff 1.69053e-16

1002# Position.lon2 4.99764604290380 vs 4.99764604290380, diff 3.55439e-16

1003# Position.lon2 4.81503363740473 vs 4.81503363740473, diff 1.84459e-16

1004# Position.lon2 4.67828821748836 vs 4.67828821748835, diff 5.69553e-16

1005# Position.lon2 4.57205667906283 vs 4.57205667906283, diff 1.94262e-16

1006

1007# Intersection(a02=17.798332, a12=19.521356, azi02=135.0, azi12=45.0, lat1=30.0, lat2=45.0, lon1=0.0, lon2=15.830286, name='Intersection', s02=1977981.142985, s12=2169465.957531), iteration=9, exact=None, est=None

1008# Intersection(a02=17.798332, a12=19.521356, azi02=135.0, azi12=45.0, lat1=30.0, lat2=45.0, lon1=0.0, lon2=15.830286, name='Intersection', s02=1977981.142985, s12=2169465.957531), iteration=9

1009

1010# Intersection(a02=17.798332, a12=19.521356, azi02=135.0, azi12=45.0, lat1=30.0, lat2=45.0, lon1=0.0, lon2=15.830286, name='Intersection', s02=1977981.142985, s12=2169465.957531), iteration=9, exact=None, est=1000000.0

1011# Intersection(a02=17.798332, a12=19.521356, azi02=135.0, azi12=45.0, lat1=30.0, lat2=45.0, lon1=0.0, lon2=15.830286, name='Intersection', s02=1977981.142985, s12=2169465.957531), iteration=9

1012

1013# NearestOn(a02=17.978107, a12=27.74256, azi0=113.73626, azi12=45.0, azi2=135.0, lat1=30.0, lat2=49.634582, lon1=0.0, lon2=25.767876, name='NearestOn', s02=1997960.116871, s12=3083112.636236), iteration=5, exact=False, est=None

1014# NearestOn(a02=17.978107, a12=27.74256, azi0=113.73626, azi12=45.0, azi2=135.0, lat1=30.0, lat2=49.634582, lon1=0.0, lon2=25.767876, name='NearestOn', s02=1997960.116871, s12=3083112.636236), iteration=5

1015

1016# NearestOn(a02=17.978107, a12=27.74256, azi0=113.73626, azi12=45.0, azi2=135.0, lat1=30.0, lat2=49.634582, lon1=0.0, lon2=25.767876, name='NearestOn', s02=1997960.116871, s12=3083112.636236), iteration=7, exact=False, est=1000000.0

1017# NearestOn(a02=17.978107, a12=27.74256, azi0=113.73626, azi12=45.0, azi2=135.0, lat1=30.0, lat2=49.634582, lon1=0.0, lon2=25.767876, name='NearestOn', s02=1997960.116871, s12=3083112.636236), iteration=7

1018

1019# NearestOn(a02=17.978107, a12=27.74256, azi0=113.73626, azi12=45.0, azi2=135.0, lat1=30.0, lat2=49.634582, lon1=0.0, lon2=25.767876, name='NearestOn', s02=1997960.116871, s12=3083112.636236), iteration=5, exact=True, est=None

1020# NearestOn(a02=17.978107, a12=27.74256, azi0=113.73626, azi12=45.0, azi2=135.0, lat1=30.0, lat2=49.634582, lon1=0.0, lon2=25.767876, name='NearestOn', s02=1997960.116871, s12=3083112.636236), iteration=5

1021

1022# NearestOn(a02=17.978107, a12=27.74256, azi0=113.73626, azi12=45.0, azi2=135.0, lat1=30.0, lat2=49.634582, lon1=0.0, lon2=25.767876, name='NearestOn', s02=1997960.116871, s12=3083112.636236), iteration=7, exact=True, est=1000000.0

1023# NearestOn(a02=17.978107, a12=27.74256, azi0=113.73626, azi12=45.0, azi2=135.0, lat1=30.0, lat2=49.634582, lon1=0.0, lon2=25.767876, name='NearestOn', s02=1997960.116871, s12=3083112.636236), iteration=7

1024

1025# **) MIT License

1026#

1027# Copyright (C) 2022-2023 -- mrJean1 at Gmail -- All Rights Reserved.

1028#

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

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

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

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

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

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

1035#

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

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

1038#

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

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

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

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

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

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

1045# OTHER DEALINGS IN THE SOFTWARE.