Coverage for pygeodesy/geodesici.py: 91%

1

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

3

4 u'''Classes L{Intersectool} and L{Intersector} to find the intersections of two geodesic lines or line segments.

5

6 Class L{Intersector} is a pure Python version of I{Karney}'s C++ class U{Intersect

7<https://GeographicLib.SourceForge.io/C++/doc/classGeographicLib_1_1Intersect.html>}.

8

9 Class L{Intersectool} is a wrapper to invoke I{Karney}'s U{IntersectTool

10<https://GeographicLib.SourceForge.io/C++/doc/IntersectTool.1.html>} utility, mainly intended I{for testing purposes}.

11

12 Set env variable C{PYGEODESY_INTERSECTTOOL} to the (fully qualified) path of the C{IntersectTool} executable or use

13 property L{Intersectool.IntersectTool}. For usage and some examples run C{"env PYGEODESY_INTERSECTTOOL=<IntersectTool-path>

14 python3 -m pygeodesy.geodesici --help"}.

15

16 Both L{Intersectool} and L{Intersector} provide methods C{All}, C{Closest}, C{Next} and C{Segment} and produce

17 L{XDict} instances with 4 or more items. Adjacent methods C{All5}, C{Closest5}, C{Next5} and C{Segment} return

18 or yield L{Intersectool5Tuple} or L{Intersector5Tuple}s with the lat-, longitude and azimuth of each intersection

19 as an extended, geodesic C{Position}-like L{GDict} instance.

20

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

22 documentation, I{Charles F.F. Karney}'s paper U{Geodesics intersections<https://arxiv.org/abs/2308.00495>}

23 and I{S. Baselga Moreno & J.C. Martinez-Llario}'s U{Intersection and point-to-line solutions for geodesics

24 on the ellipsoid<https://riunet.UPV.ES/bitstream/handle/10251/122902/Revised_Manuscript.pdf>}.

25'''

26# make sure int/int division yields float quotient

27 from __future__ import division as _; del _ # noqa: E702 ;

28

29 from pygeodesy.basics import _copy, _enumereverse, map1, \

30 _xinstanceof, _xor, typename

31 from pygeodesy.constants import EPS, INF, INT0, PI, PI2, PI_4, \

32 _0_0, _0_5, _1_0, _1_5, _2_0, _3_0, \

33 _45_0, _64_0, _90_0, isfinite

34 from pygeodesy.constants import _EPSjam # PYCHOK used!

35 from pygeodesy.ellipsoids import _EWGS84, Fmt, unstr

36 from pygeodesy.errors import GeodesicError, IntersectionError, _an, \

37 _xgeodesics, _xkwds, _xkwds_get, \

38 _xkwds_kwds, _xkwds_pop2

39# from pygeodesy.errors import exception_chaining # _MODS

40 from pygeodesy.fmath import euclid, fdot

41 from pygeodesy.fsums import Fsum, fsum1_, _ceil

42# from pygeodesy.internals import typename # from .basics

43 from pygeodesy.interns import NN, _A_, _B_, _c_, _COMMASPACE_, _DMAIN_, \

44 _HASH_, _M_, _not_, _SPACE_, _too_

45 from pygeodesy.karney import Caps, _diff182, GDict, _sincos2de, _Xables

46 from pygeodesy.lazily import _ALL_DOCS, _ALL_LAZY, _ALL_MODS as _MODS

47 from pygeodesy.named import ADict, _NamedBase, _NamedTuple, _Pass

48# from pygeodesy.namedTuples import _LL4Tuple # _MODS

49 from pygeodesy.props import deprecated_method, Property, \

50 Property_RO, property_RO, property_ROver

51 from pygeodesy.solveBase import _SolveCapsBase, pairs

52# from pygeodesy.streprs import pairs # from .solveBase

53# from pygeodesy.streprs import Fmt, unstr # from .ellipsoids

54 from pygeodesy.units import Azimuth as Azi, Degrees, Int, _isDegrees, \

55 Lat, Lon, Meter, Meter_

56 from pygeodesy.utily import atan2, sincos2, fabs, radians

57

58# from math import ceil as _ceil, fabs, radians # .fsums, .utily

59

60 __all__ = _ALL_LAZY.geodesici

61 __version__ = '26.02.04'

62

63 _0t = 0, # int

64 _1_1t = -1, +1

65 _1_0_1t = -1, 0, +1

66 _aAB_ = 'aAB'

67 _c__ = '-c' # PYCHOK used!

68 _cWGS84 = _EWGS84.a * PI2 # outer circumference

69 _EPS3 = EPS * _3_0

70 _EPSr5 = pow(EPS, 0.2) # PYCHOK used! 7.4e-4 or ~3"

71 _i__ = '-i' # PYCHOK used!

72 _latA_ = 'latA'

73 _lonA_ = 'lonA'

74 _n__ = '-n' # PYCHOK used!

75 _o__ = '-o' # PYCHOK used!

76 _R__ = '-R'

77 _sAB_ = 'sAB'

78 _sX0_ = 'sX0'

79 _TRIPS = 128

80

81

82 class XDict(ADict):

83 '''4+Item result from L{Intersectool} and L{Intersector} methods C{All},

84 C{Closest}, C{Next} and C{Segment} with the intersection offsets

85 C{sA}, C{sB} and C{sX0} in C{meter} and the coincidence indicator

86 C{c}, an C{int}, +1 for parallel, -1 for anti-parallel or 0 otherwise.

87

88 Offsets C{sA} and C{sB} are distances measured I{along} geodesic line

89 C{glA} respectively C{glB}, but C{sX0} is the I{L1-distance} between

90 the intersection and the I{origin} C{X0}.

91

92 If present, distance C{sAB} and angular distance C{aAB} represent the

93 separation between the intersection point on geodesic lines C{glA}

94 and C{glB} in C{meter} respectively C{degrees}, typically below

95 C{5e-9 meter} or C{5 nm} respectively C{5e-14 degrees} or C{1 n"}.

96

97 For segments, indicators C{kA} and C{kB} are C{0} if the segments

98 intersect or C{-1} or C{+1} if the intersection is I{before} the

99 start, respectively I{after} the end of the segment, similar to

100 L{Intersection3Tuple<Intersection3Tuple>}. Segment indicator C{k}

101 is I{Karney}'s C{segmode}, equal C{kA * 3 + kB}.

102 '''

103 _Delta = EPS # default margin, see C{Intersector._Delto}

104

105 def __add__(self, other):

106 X = _copy(self)

107 X += other

108 return X

109

110 def __eq__(self, other):

111 return not self.__ne__(other)

112

113 def __iadd__(self, other):

114 if isinstance(other, tuple): # and len(other) == 2:

115 a, b = other

116 else:

117 # _xinstanceof(XDict, other=other)

118 a = other.sA

119 b = other.sB

120 if other.c:

121 self.c = other.c

122 self.sA += a # PYCHOK sA

123 self.sB += b # PYCHOK sB

124 return self

125

126 def __le__(self, other):

127 # _xinstanceof(XDict, other=other)

128 return self == other or self < other

129

130 def __lt__(self, other):

131 # _xinstanceof(XDict, other=other)

132 return self.sA < other.sA or (self.sA == other.sA and # PYCHOK sA

133 self.sB < other.sB and # PYCHOK sB

134 self != other)

135

136 def __ne__(self, other):

137 # _xinstanceof(XDict, other=other)

138 return self is not other and self.L1(other) > self._Delta

139

140 def _corners(self, sA, sB, T2):

141 # yield all corners further than C{T2}

142 a, b = self.sA, self.sB # PYCHOK sA, sB

143 for x in (0, sA):

144 for y in (0, sB):

145 if _L1(x - a, y - b) >= T2:

146 yield XDict_(x, y)

147

148 def _fixCoincident(self, X, c0=0):

149 # return the mid-point if C{X} is anti-/parallel

150 c = c0 or X.c

151 if c:

152 s = (self.sA - X.sA + # PYCHOK sA

153 (self.sB - X.sB) * c) * _0_5 # PYCHOK sB

154 X = X + (s, s * c) # NOT +=

155 return X

156

157 def _fixSegment(self, sA, sB): # PYCHOK no cover

158 # modify this anti-/parallel C{XDict}

159 a, b, c = self.sA, self.sB, self.c # PYCHOK sA, sB, c

160

161 def _g(): # intersection in smallest gap

162 if c > 0: # distance to [A, B] is |(a - b) - (A - B)|

163 t = a - b # consider corners [0, sB] and [sA, 0]

164 t = fabs(t + sB) < fabs(t - sA)

165 s = a + b

166 else: # distance to [A, B] is |(a + b) - (A + B)|

167 t = a + b # consider corner [0, 0] and [sA, sB]

168 t = fabs(t) < fabs(t - (sA + sB))

169 s = sB + (a - b)

170 return (sB if t else sA) - s

171

172 ta = -a

173 tb = sA - a

174 tc = -c * b

175 td = -c * (b - sB)

176

177 ga = 0 <= (b + c * ta) <= sB

178 gb = 0 <= (b + c * tb) <= sB

179 gc = 0 <= (a + tc) <= sA

180 gd = 0 <= (a + td) <= sA

181

182 # test opposite rectangle sides first

183 s = ((ta + tb) if ga and gb else (

184 (tc + td) if gc and gd else (

185 (ta + tc) if ga and gc else (

186 (ta + td) if ga and gd else (

187 (tb + tc) if gb and gc else (

188 (tb + td) if gb and gd else _g())))))) * _0_5

189 self += s, s * c

190

191 @property_RO

192 def _is00(self):

193 return not (self.sA or self.sB) # PYCHOK sA, sB

194

195 def L1(self, other=None):

196 '''Return the C{L1} distance.

197 '''

198 a, b = self.sA, self.sB # PYCHOK sA, sB

199 if other is not None:

200 # _xinstanceof(XDict, other=other)

201 a -= other.sA

202 b -= other.sB

203 return _L1(a, b)

204

205 def _nD1(self, D1):

206 # yield the C{Closest} starts

207 D_ = 0, D1, -D1

208 for a, b in zip((0, 1, -1, 0, 0),

209 (0, 0, 0, 1, -1)):

210 yield self + (D_[a], D_[b])

211

212 def _nD2(self, D2):

213 # yield the C{Next} starts

214 D22 = D2 * _2_0

215 D_ = 0, D2, D22, -D22, -D2

216 for a, b in zip((-1, -1, 1, 1, -2, 0, 2, 0),

217 (-1, 1, -1, 1, 0, 2, 0, -2)):

218 yield self + (D_[a], D_[b])

219

220 def _nmD3(self, n, m, D3): # d3 / 2

221 # yield the C{All} starts

222 yield self

223 for i in range(n, m, 2):

224 for j in range(n, m, 2):

225 if i or j: # skip self

226 yield self + ((i + j) * D3,

227 (i - j) * D3)

228

229 def _outSide(self, sA, sB):

230 # is this C{Xdist} outside one or both segments?

231 a, b = self.sA, self.sB # PYCHOK sA, sB

232 kA = -1 if a < 0 else (+1 if a > sA else INT0)

233 kB = -1 if b < 0 else (+1 if b > sB else INT0)

234 self.set_(kA=kA, kB=kB, k=(kA * 3 + kB) or INT0)

235 return bool(kA or kB)

236

237 def _skip(self, S_, T1_Delta):

238 # remove starts from list C{S_} near this C{XDict}

239 for j, S in _enumereverse(S_):

240 if S.L1(self) < T1_Delta:

241 S_.pop(j)

242

243

244 def XDict_(sA=_0_0, sB=_0_0, c=INT0, sX0=_0_0):

245 '''(INTERNAL) New L{XDict} from positionals.

246 '''

247 return XDict(sA=sA, sB=sB, c=c, sX0=sX0)

248

249 _X000 = XDict_() # PYCHOK origin

250 _XINF = XDict_(INF)

251

252

253 class _IntersectBase(_NamedBase):

254 '''(INTERNAL) Base class for L{Intersectool} and L{Intersector}.

255 '''

256 # _g = None

257

258 def __init__(self, geodesic, **name):

259 _xinstanceof(*_EWGS84._Geodesics, geodesic=geodesic)

260 self._g = geodesic

261 if name:

262 self.name = name

263

264 @Property_RO

265 def a(self):

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

267 '''

268 return self.ellipsoid.a

269

270 equatoradius = a # = Requatorial

271

272 def All(self, glA, glB, **kwds): # PYCHOK no cover

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

274 self._notOverloaded(glA, glB, **kwds)

275

276 @Property_RO

277 def _cHalf(self): # normalizer, semi-circumference

278 return self.R * PI # ~20K Km WGS84

279

280 @Property_RO

281 def _cMax(self): # outer circumference

282 return max(self.a, self.ellipsoid.b, self.R) * PI2

283

284 @property_RO

285 def datum(self):

286 '''Get the geodesic's datum (C{Datum}).

287 '''

288 return self.geodesic.datum

289

290 @Property_RO

291 def ellipsoid(self):

292 '''Get the C{geodesic}'s ellipsoid (C{Ellipsoid}).

293 '''

294 return self.geodesic.datum.ellipsoid

295

296 @Property_RO

297 def f(self):

298 '''Get the I{flattening} (C{scalar}), C{0} for spherical, negative for prolate.

299 '''

300 return self.ellipsoid.f

301

302 flattening = f

303

304 @property_RO

305 def geodesic(self):

306 '''Get the C{Geodesic} (C{GeodesicExact, GeodesicSolve, or ...}).

307 '''

308 return self._g

309

310 def _illz2G(self, G, il):

311 '''(INTERNAL) Set C{InverseLine} 1-/2-attrs into C{G}, a C{GDict}.

312 '''

313 try:

314 G.set_(lat1=il.lat1, lon1=il.lon1, azi1=il.azi1, a12=il.a13, # .Arc()

315 lat2=il.lat2, lon2=il.lon2, azi2=il.azi2, s12=il.s13) # .Distance()

316 except AttributeError:

317 r = il.Position(il.s13, outmask=Caps.STANDARD_LINE) # isfinite(il.s13)

318 G.set_(**r)

319# for n, v in r.items():

320# if not hasattr(il, n):

321# setattr(il, n, v)

322 return G

323

324 def intersect7(self, start1, end1, start2, end2, X0=_X000, aMaX0=0, sMaX0=_cWGS84,

325 **LatLon_and_kwds):

326 '''Yield the intersections of two geodesic lines, each defined by a start

327 and end point or by a start point and an azimuth (bearing from North).

328

329 @arg start1: Start point of the first line (C{LatLon}).

330 @arg end1: End point of the first line (C{LatLon}) or the azimuth at the

331 B{C{start1}} point (compass C{degrees360}).

332 @arg start2: Start point of the second line (C{LatLon}).

333 @arg end2: End point of the second line (C{LatLon}) or the azimuth at the

334 B{C{start2}} point (compass C{degrees360}).

335 @kwarg X0: Optional I{origin} for I{L1-distances} (L{XDict}) or C{None} for

336 the L{Middle<Intersector.Middle>}, otherwise C{XDiff_(0, 0)}.

337 @kwarg aMaX0: Upper limit for the I{angular L1-distance}

338 (C{degrees}) or C{None} or C{0} for unlimited.

339 @kwarg sMaX0_C: Optional, upper limit C{B{sMaX0}=2*PI*R} for the

340 I{L1-distance} to B{C{X0}} (C{meter}).

341 @kwarg LatLon_and_kwds: Optional class C{B{LatLon}=None} to return intersection

342 points and optional, additional B{C{LatLon}} keyword arguments.

343

344 @note: The C{lat} and C{lon} attr of B{C{start1}}, B{C{end1}}, B{C{start2}} and

345 B{C{end2}} are used I{verbatim}, ignoring C{datum} or C{ellipsoid}.

346

347 @return: Yield an L{Intersect7Tuple}C{(A, B, sAB, aAB, c, kA, kB)} for every

348 intersection found, with C{A} and C{B} each a B{C{LatLon}} or if

349 C{B{LatLon} is None} or not specified, a L{LatLon4Tuple}C{(lat, lon,

350 height, datum)} with C{height 0} and this C{datum}.

351

352 @raise GeodesicError: Invalid B{C{start1}}, B{C{end1}}, B{C{start2}} or

353 B{C{end2}} or B{C{end1}} and B{C{end2}} differ in type.

354

355 @raise IntersectionError: No convergence.

356 '''

357

358 def _args(s, e):

359 t = (e,) if _isDegrees(e) else (e.lat, e.lon)

360 return (s.lat, s.lon) + t

361

362 try:

363 glA = self.Line(*_args(start1, end1))

364 glB = self.Line(*_args(start2, end2))

365 except Exception as x:

366 raise GeodesicError(start1=start1, end1=end1, start2=start2, end2=end2, cause=x)

367

368 LL, kwds = _xkwds_pop2(LatLon_and_kwds, LatLon=None)

369 d, kwds = _xkwds_pop2(kwds, datum=self.datum)

370 h, kwds = _xkwds_pop2(kwds, height=0)

371

372 _LL4T = _MODS.namedTuples._LL4Tuple

373 for X in self.All(glA, glB, X0=X0, aMaX0=aMaX0, sMaX0=sMaX0, _C=True):

374 A = B = _LL4T(X.latA, X.lonA, h, d, LL, kwds, iteration=X.iteration)

375 if X.sAB or X.latA != X.latB or X.lonA != X.lonB:

376 B = _LL4T(X.latB, X.lonB, h, d, LL, kwds, iteration=X.iteration)

377 yield Intersect7Tuple(A, B, X.sAB, X.aAB, X.c, _xkwds_get(X, kA=0),

378 _xkwds_get(X, kB=0))

379

380 def _Inversa12(self, A, B=None):

381 lls = (0, 0, A, 0) if B is None else (A.lat2, A.lon2,

382 B.lat2, B.lon2)

383 r = self._g.Inverse(*lls, outmask=Caps.DISTANCE)

384 return r.s12, r.a12 # .a12 always in r

385

386 def k2kAkB(self, k):

387 '''Unravel C{k} into C{kA} and C{kB}.

388

389 @arg k: Segment indicator C{kA * 3 + kB} (C{int}).

390

391 @return: An C{ADict(k=k, kA=kA, kB=kB)}.

392

393 @raise GeodesicError: Invalid B{C{k}}.

394 '''

395 for kA in range(-1, 2):

396 for kB in range(-1, 2):

397 if (kA * 3 + kB) == k:

398 return ADict(k=k, kA=kA, kB=kB)

399 raise GeodesicError(k=k)

400

401# def k2kAkB(self, k):

402# # unravel C{k} into C{kA} and C{kB}.

403# kA, kB = divmod(k, 3)

404# if kB > 1:

405# kA += 1

406# kB -= 3

407# return kA, kB

408

409 def Line(self, lat1, lon1, azi1_lat2, *lon2, **name): # PYCHOK no cover

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

411 self._notOverloaded(lat1, lon1, azi1_lat2, *lon2, **name)

412

413 def _ll3z4ll(self, lat1, lon1, azi1_lat2, *lon2):

414 t = Lat(lat1=lat1), Lon(lon1=lon1)

415 if lon2: # get azis for All, keep lat-/lons

416 t += Lat(lat2=azi1_lat2), Lon(lon2=lon2[0])

417 else:

418 t += Azi(azi1=azi1_lat2),

419 return t

420

421 @deprecated_method

422 def Next5s(self, glA, glB, X0=_X000, aMax=1801, sMax=0, **unused): # PYCHOK no cover

423 '''DEPRECATED on 2024.07.02, use method C{All5}.'''

424 return self.All5(glA, glB, X0=X0, aMaX0=aMax, sMaX0=sMax) # PYCHOK attr

425

426 @Property_RO

427 def R(self):

428 '''Get the I{authalic} earth radius (C{meter}).

429 '''

430 return self.ellipsoid.R2

431

432 def _sMaX0_C2(self, aMaX0=0, **sMaX0_C):

433 _g = _xkwds_get

434 s = _g(sMaX0_C, sMaX0=self._cMax)

435 s = _g(sMaX0_C, sMax=s) # for backward ...

436 a = _g(sMaX0_C, aMax=aMaX0) # ... compatibility

437 if a: # degrees to meter, approx.

438 s = min(s, self.R * radians(a)) # ellipsoid.degrees2m(a)

439 s = _g(sMaX0_C, _R=s)

440 if s < _EPS3:

441 s = _EPS3 # raise GeodesicError(sMaX0=s)

442 return s, _g(sMaX0_C, _C=False)

443

444 def _xNext(self, glA, glB, eps1, **eps_C): # PYCHOK no cover

445 eps1 = _xkwds_get(eps_C, eps=eps1) # eps for backward compatibility

446 if eps1 is not None:

447 a = glA.lat1 - glB.lat1

448 b = glA.lon1 - glB.lon1

449 if euclid(a, b) > eps1:

450 raise GeodesicError(lat_=a, lon_=b, eps1=eps1)

451 return _xkwds_kwds(eps_C, _C=False)

452

453

454 class Intersectool(_IntersectBase, _SolveCapsBase):

455 '''Wrapper to invoke I{Karney}'s utility U{IntersectTool

456 <https://GeographicLib.SourceForge.io/C++/doc/IntersectTool.1.html>}

457 similar to class L{Intersector<geodesici.Intersector>}.

458

459 @note: Use property C{IntersectTool} or env variable C{PYGEODESY_INTERSECTTOOL}

460 to specify the (fully qualified) path to the C{IntersectTool} executable.

461

462 @note: This C{Intersectool} is intended I{for testing purposes only}, it invokes

463 the C{IntersectTool} executable for I{every} method call.

464 '''

465 _c_alt = _c__, # Closest latA lonA aziA latB lonB aziB

466 _C_option = '-C',

467 _Error = GeodesicError

468 _i_alt = _i__, # Segment latA1 lonA1 latA2 lonA2 latB1 lonB1 latB2 lonB2

469 _linelimit = 1200 # line printer width X 10

470 _n_alt = _n__, # Next latA lonA aziA aziB

471 _Names_ABs = _latA_, _lonA_, 'latB', 'lonB', _sAB_ # -C to stderr

472 _Names_XDict = 'sA', 'sB', _c_ # plus 'k' from -i or 'sX0' from -R

473 _o_alt = _o__, # Offset latA lonA aziA latB lonB aziB x0 y0

474 _Xable_name = _Xables.IntersectTool.__name__ # typename

475 _Xable_path = _Xables.IntersectTool()

476

477 def __init__(self, a_geodesic=None, f=None, **name):

478 '''New L{IntersectTool}.

479

480 @arg a_geodesic: Earth' equatorial axis (C{meter}) or a C{Geodesic}

481 (L{GeodesicExact<pygeodesy.geodesicx.GeodesicExact>},

482 wrapped L{Geodesic<pygeodesy.geodesicw.Geodesic>} or

483 L{GeodesicSolve<pygeodesy.geodsolve.GeodesicSolve>}).

484 @kwarg f: Earth' flattening (C{scalar}), required if B{C{a_geodesic}}

485 is in C{meter}, ignored otherwise.

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

487

488 @raise GeodesicError: The eccentricity of the B{C{geodesic}}'s ellipsoid is too

489 large or no initial convergence.

490

491 @see: The B{Note} at I{Karney}'s C++ U{Intersect<https://GeographicLib.SourceForge.io/

492 C++/doc/classGeographicLib_1_1Intersect.html#ae41f54c9a44836f6c8f140f6994930cf>}.

493 '''

494 g = self._GeodesicExact() if a_geodesic is None else (a_geodesic if f is None else

495 self._GeodesicExact(a_geodesic, f))

496 _IntersectBase.__init__(self, g, **name)

497

498 def All(self, glA, glB, X0=_X000, eps1=_0_0, aMaX0=0, **sMaX0_C): # PYCHOK signature

499 '''Yield all intersections of two geodesic lines up to a limit.

500

501 @kwarg eps1: Optional margin for the L{euclid<pygeodesy.euclid>}ean distance

502 (C{degrees}) between the C{(lat1, lon1)} points of both lines for

503 using the L{IntersectTool<Intersectool.IntersectTool>}'s C{"-n"}

504 option, unless C{B{eps1}=None}.

505

506 @return: An L{XDict} for each intersection.

507 '''

508 for X, _ in self._All2(glA, glB, X0, eps1, aMaX0=aMaX0, **sMaX0_C):

509 yield X

510

511 def _All2(self, glA, glB, X0, eps1, **aMaX0_sMaX0_C): # MCCABE 13

512 '''(INTERNAL) Helper for methods C{.All} and C{.All5}.

513 '''

514 def _xz2(**gl):

515 try:

516 n, gl = gl.popitem() # _xkwds_item2(gl)

517 try:

518 return self._c_alt, (gl.azi1,)

519 except (AttributeError, KeyError):

520 return self._i_alt, (gl.lat2, gl.lon2)

521 except Exception as x:

522 raise GeodesicError(n, gl, cause=x)

523

524 _t, a = _xz2(glA=glA)

525 _x, b = _xz2(glB=glB)

526 if _x is not _t:

527 raise GeodesicError(glA=glA, glB=glB)

528

529 A = glA.lat1, glA.lon1

530 B = glB.lat1, glB.lon1

531 if _x is self._c_alt:

532 if X0 is _X000 or X0._is00:

533 if eps1 is not None and \

534 euclid(glA.lat1 - glB.lat1,

535 glA.lon1 - glB.lon1) <= eps1:

536 _x, B = self._n_alt, ()

537 else: # non-zero offset

538 _x = self._o_alt

539 b += X0.sA, X0.sB

540

541 sMaX0, _C = self._sMaX0_C2(**aMaX0_sMaX0_C)

542 for X in self._XDictInvoke(_x, _sX0_, (A + a + B + b),

543 _C=_C, _R=sMaX0):

544 if _C:

545 T = self._In5T(glA, glB, X, X)

546 if _aAB_ not in X:

547 X.set_(sAB=T.sAB, aAB=T.aAB)

548 else:

549 T = None

550 yield X.set_(c=int(X.c)), T

551

552 def All5(self, glA, glB, X0=_X000, **aMaX0_sMaX0):

553 '''Yield all intersections of two geodesic lines up to a limit.

554

555 @return: An L{Intersectool5Tuple} for each intersection.

556 '''

557 for _, T in self._All2(glA, glB, X0, _0_0, _C=True, **aMaX0_sMaX0):

558 yield T

559

560 @Property_RO

561 def _cmdBasic(self):

562 '''(INTERNAL) Get the basic C{IntersectTool} cmd (C{tuple}).

563 '''

564 return (self.IntersectTool,) + (self._e_option +

565 self._E_option +

566 self._p_option)

567

568 def Closest(self, glA, glB, X0=_X000, _C=False):

569 '''Find the closest intersection of two geodesic lines.

570

571 @kwarg _C: Use C{B{_C}=True} to include the C{"-C"} results (C{bool}).

572

573 @return: An L{XDict}.

574 '''

575 args = glA.lat1, glA.lon1, glA.azi1, \

576 glB.lat1, glB.lon1, glB.azi1

577 if X0 is _X000 or X0._is000:

578 _x = self._c_alt

579 else:

580 _x = self._o_alt

581 args += X0.sA, X0.sB

582 return self._XDictInvoke(_x, NN, args, _C=_C) # _R=None)

583

584 def Closest5(self, glA, glB, **unused):

585 '''Find the closest intersection of two geodesic lines.

586

587 @return: An L{Intersectool5Tuple}.

588 '''

589 X = self.Closest(glA, glB, _C=True)

590 return self._In5T(glA, glB, X, X)

591

592 @property_ROver

593 def _GeodesicExact(self):

594 '''Get the I{class} L{GeodesicExact}, I{once}.

595 '''

596 return _MODS.geodesicx.GeodesicExact # overwrite property_ROver

597

598 def _In5T(self, glA, glB, S, X, k2=False, **_2X):

599 A = GDict(glA).set_(lat2=X.latA, lon2=X.lonA, s12=S.sA)

600 B = GDict(glB).set_(lat2=X.latB, lon2=X.lonB, s12=S.sB)

601 if k2:

602 A.set_(k2=X.kA)

603 B.set_(k2=X.kB)

604 s, a = self._Inversa12(A, B)

605 sAB = _xkwds_get(X, sAB=s)

606 if a and s and s != sAB:

607 a *= sAB / s # adjust a

608 return Intersectool5Tuple(A._2X(glA, **_2X),

609 B._2X(glB, **_2X), sAB, a, X.c)

610

611 @Property

612 def IntersectTool(self):

613 '''Get the U{IntersectTool<https://GeographicLib.SourceForge.io/C++/doc/IntersectTool.1.html>}

614 executable (C{filename}).

615 '''

616 return self._Xable_path

617

618 @IntersectTool.setter # PYCHOK setter!

619 def IntersectTool(self, path):

620 '''Set the U{IntersectTool<https://GeographicLib.SourceForge.io/C++/doc/IntersectTool.1.html>}

621 executable (C{filename}), the (fully qualified) path to the C{IntersectTool} executable.

622

623 @raise GeodesicError: Invalid B{C{path}}, B{C{path}} doesn't exist or isn't the

624 C{IntersectTool} executable.

625 '''

626 self._setXable(path)

627

628 def Line(self, lat1, lon1, azi1_lat2, *lon2, **name):

629 '''Return a geodesic line from this C{Intersector}'s geodesic, specified

630 by two points or a point and an azimuth (bearing from North).

631

632 @return: A 3- or 6-item, named L{GDict}.

633 '''

634 args = self._ll3z4ll(lat1, lon1, azi1_lat2, *lon2)

635 gl = GDict((u.name, u) for u in args)

636# if lon2: # get azis for All, use lat-/lons as given

637# r = self._g.Inverse(outmask=Caps.AZIMUTH, *args)

638# gl.set_(azi1=Azi(azi1=r.azi1), azi2=Azi(azi2=r.azi2))

639 if name:

640 gl.name= name

641 return gl

642

643 def Middle(self, glA, glB, **_C):

644 '''Get the mid-points on two geodesic line segments.

645

646 @kwarg _C: Use C{B{_C}=True} to include the C{"-C"} results (C{bool}).

647

648 @return: An L{XDict}.

649 '''

650 return self._middle5(glA, glB, **_C)[0]

651

652 def _middle5(self, glA, glB, _C=False, **unused):

653 # return intersections C{A} and C{B} and the

654 # center C{X0} of rectangle [sA, sB]

655

656 def _smi4(**gl):

657 try:

658 n, gl = gl.popitem()

659 il = self._g.InverseLine(gl.lat1, gl.lon1, gl.lat2, gl.lon2)

660 except Exception as x:

661 raise GeodesicError(n, gl, cause=x)

662 s = il.s13

663 m = s * _0_5

664 return s, m, il, (il.Position(m, outmask=Caps.STANDARD_LINE) if _C else None)

665

666 sA, mA, iA, A = _smi4(glA=glA)

667 sB, mB, iB, B = _smi4(glB=glB)

668 X = XDict_(mA, mB) # centers

669 _ = X._outSide(sA, sB)

670 if _C: # _Names_ABs

671 s, a = self._Inversa12(A, B)

672 X.set_(latA=A.lat2, lonA=A.lon2, aMM=a, # assert sA == A.s12

673 latB=B.lat2, lonB=B.lon2, sMM=s) # assert sB == B.s12

674 return X, A, iA, B, iB

675

676 def Middle5(self, glA, glB, **unused):

677 '''Get the mid-points on two geodesic line segments and their distance.

678

679 @return: A L{Middle5Tuple}.

680 '''

681 X, A, iA, B, iB = self._middle5(glA, glB, _C=True)

682 A, B, s, a, c = self._In5T(A, B, X, X, _2X=_M_)

683 return Middle5Tuple(self._illz2G(A, iA),

684 self._illz2G(B, iB), s, a, c)

685

686 def Next(self, glA, glB, eps1=None, **_C): # PYCHOK no cover

687 '''Find the next intersection of two I{intersecting} geodesic lines.

688

689 @kwarg _C: Use C{B{_C}=True} to include the option C{"-C"} results (C{bool}).

690

691 @return: An L{XDict}.

692 '''

693 if eps1 or _C:

694 _C = self._xNext(glA, glB, eps1, **_C)

695 return self._XDictInvoke(self._n_alt, NN,

696 (glA.lat1, glA.lon1, glA.azi1, glB.azi1),

697 **_C) # _R=None

698

699 def Next5(self, glA, glB, **eps1): # PYCHOK no cover

700 '''Find the next intersection of two I{intersecting} geodesic lines.

701

702 @return: An L{Intersectool5Tuple}.

703 '''

704 X = self.Next(glA, glB, _C=True, **eps1)

705 return self._In5T(glA, glB, X, X)

706

707 def _R_option(self, _R=None):

708 '''(INTERNAL) Get the C{-R maxdist} option.

709 '''

710 return () if _R is None else (_R__, str(_R)) # -R maxdist

711

712 def Segment(self, glA, glB, **_C_unused):

713 '''Find the intersection between two geodesic line segments.

714

715 @kwarg _C: Use C{B{_C}=True} to include the option C{"-C"} results (C{bool}).

716

717 @return: An L{XDict}.

718 '''

719 X = self._XDictInvoke(self._i_alt, 'k', (glA.lat1, glA.lon1, glA.lat2, glA.lon2,

720 glB.lat1, glB.lon1, glB.lat2, glB.lon2),

721 _C=_xkwds_get(_C_unused, _C=False)) # _R=None

722 try:

723 ks = self.k2kAkB(int(X.k))

724 except Exception as x:

725 raise GeodesicError(glA=glA, glB=glB, X=str(X), cause=x)

726 return X.set_(**ks)

727

728 def Segment5(self, glA, glB, **unused):

729 '''Find the next intersection of two I{intersecting} geodesic lines.

730

731 @return: An L{Intersectool5Tuple}.

732 '''

733 X = self.Segment(glA, glB, _C=True)

734 return self._In5T(glA, glB, X, X, k2=True)

735

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

737 '''Return this C{Intersectool} as string.

738

739 @kwarg prec_sep: Keyword argumens C{B{prec}=6} and C{B{sep}=", "}

740 for the C{float} C{prec}ision, number of decimal digits

741 (0..9) and the C{sep}arator string to join. Trailing

742 zero decimals are stripped for B{C{prec}} values of 1

743 and above, but kept for negative B{C{prec}} values.

744

745 @return: Intersectool items (C{str}).

746 '''

747 d = dict(geodesic=self.geodesic, invokation=self.invokation,

748 status=self.status,

749 IntersectTool=self.IntersectTool)

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

751

752 def _XDictInvoke(self, alt, _k_sX0, args, _C=False, **_R):

753 '''(INTERNAL) Invoke C{IntersectTool}, return results as C{XDict} or

754 a C{generator} if keyword argument C{B{_R}=sMaX0} is specified.

755 '''

756 # assert len(args) == {self._c_alt: 6,

757 # self._i_alt: 8,

758 # self._n_alt: 4,

759 # self._o_alt: 8}.get(alt, len(args))

760 cmd = self._cmdBasic

761 Names = self._Names_XDict # has _c_ always

762 if _k_sX0:

763 Names += _k_sX0,

764 if _C:

765 cmd += self._C_option

766 Names += self._Names_ABs

767 if _R:

768 cmd += self._R_option(**_R)

769 X, _R = self._DictInvoke2(cmd + alt, args, Names, XDict, **_R)

770 return X if _R else X.set_(c=int(X.c)) # generator or XDict

771

772

773 class Intersector(_IntersectBase):

774 '''Finder of intersections between two goedesic lines, each an instance of

775 L{GeodesicLineExact<pygeodesy.geodesicx.GeodesicLineExact>}, wrapped

776 L{GeodesicLine<pygeodesy.geodesicw.GeodesicLine>} or

777 L{GeodesicLineSolve<pygeodesy.geodsolve.GeodesicLineSolve>}.

778

779 @see: I{Karney}'s C++ class U{Intersect<https://GeographicLib.SourceForge.io/

780 C++/doc/classGeographicLib_1_1Intersect.html#details>} for more details.

781 '''

782

783 def __init__(self, geodesic, **name):

784 '''New L{Intersector}.

785

786 @arg geodesic: The geodesic (L{GeodesicExact<pygeodesy.geodesicx.GeodesicExact>},

787 wrapped L{Geodesic<pygeodesy.geodesicw.Geodesic>} or

788 L{GeodesicLineSolve<pygeodesy.geodsolve.GeodesicLineSolve>}).

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

790

791 @raise GeodesicError: The B{C{geodesic}}'s ellipsoid is too eccentric or no initial convergence.

792

793 @see: The B{Note} at I{Karney}'s C++ U{Intersect<https://GeographicLib.SourceForge.io/

794 C++/doc/classGeographicLib_1_1Intersect.html#ae41f54c9a44836f6c8f140f6994930cf>}.

795 '''

796 _IntersectBase.__init__(self, geodesic, **name)

797 E = self.ellipsoid

798 t1 = E.b * PI # min distance between intersects

799 t2 = self._polarDist2(_90_0)[0] * _2_0 # furthest distance to closest intersect

800 if self.f > 0:

801 t3 = self._obliqDist4()[0] # half furthest min distance to next intersect

802 t4 = t1 # capture radius for spherical

803 else: # PYCHOK no cover

804 t1, t2 = t2, t1

805 t3 = self._Inversa12( _90_0)[0] * _2_0 # longest, shortest geodesic

806 t4 = self._polarB3()[0]

807

808 self._D1 = d1 = t2 * _0_5 # ~E.L tile spacing for Closest

809 self._D2 = d2 = t3 / _1_5 # tile spacing for Next

810 self._D3 = d3 = t4 - self.Delta # tile spacing for All

811 self._T1 = t1 # min distance between intersects

812 self._T2 = t2 = t1 * _2_0

813 if not (d1 < d3 and d2 < d3 and d2 < t2):

814 t = Fmt.PARENSPACED(_too_('eccentric'), E.e)

815 raise GeodesicError(ellipsoid=E.toStr(terse=2), txt=t)

816

817 def All(self, glA, glB, X0=None, aMaX0=0, **sMaX0_C): # MCCABE 15

818 '''Yield all intersections of two geodesic lines up to a limit.

819

820 @arg glA: A geodesic line (L{Line<Intersector.Line>}).

821 @arg glB: An other geodesic line (L{Line<Intersector.Line>}).

822 @kwarg X0: Optional I{origin} for I{L1-distances} (L{XDict}) or

823 C{None} for the L{Middle<Intersector.Middle>} of both

824 lines if both are a 4-C{args} L{Line<Intersector.Line>}

825 or C{InverseLine}, otherwise C{XDiff_(0, 0)}.

826 @kwarg aMaX0: Upper limit for the I{angular L1-distance}

827 (C{degrees}) or C{None} or C{0} for unlimited.

828 @kwarg sMaX0_C: Optional, upper limit C{B{sMaX0}=2*PI*R} for the

829 I{L1-distance} to B{C{X0}} (C{meter}) and option

830 C{B{_C}=False} to include the intersection lat-/

831 longitudes C{latA}, C{lonA}, C{latB}, C{lonB} and

832 distances C{sAB} and C{aSB}.

833

834 @return: Yield an L{XDict} for each intersection found.

835

836 @raise GeodesicError: Geodesic line B{C{glA}} or B{C{glB}}

837 invalid, incompatible or ill-configured.

838

839 @raise IntersectionError: No convergence.

840 '''

841 self._xLines(glA, glB)

842 if X0 is None:

843 try: # determine X0

844 X0, _, _ = self._middle3(glA, glB, True)

845 except GeodesicError: # no .Distance

846 X0 = _X000

847 sMaX0, _C = self._sMaX0_C2(aMaX0, **sMaX0_C)

848

849 D, _D = self.Delta, self._cHalf # C++ _d

850 xMaX0 = sMaX0 + D

851 m = int(_ceil(xMaX0 / self._D3)) # m x m tiles

852 d3 = xMaX0 / m

853 T2d3D = self._T2d3Delta(d3)

854

855 C_ = _List(D) # closest coincident

856 X_ = _List(D) # intersections found

857 c0 = 0

858 S_ = list(X0._nmD3(1 - m, m, d3 * _0_5))

859 # assert len(S_) == m * m + (m - 1) % 2

860 while S_:

861 Q, i = self._Basic2(glA, glB, S_.pop(0))

862 if Q in X_:

863 continue

864 if Q.c: # coincident intersection # PYCHOK no cover

865 _X0fx = X0._fixCoincident

866 Q = _X0fx(Q) # Q = Q'

867 if c0 and Q in C_:

868 continue

869 C_.addend(Q)

870 # elimate all existing intersections

871 # on this line (which didn't set c0)

872 c0 = Q.c

873 for j, X in _enumereverse(X_):

874 if _X0fx(X, c0).L1(Q) <= D: # X' == Q

875 X_.pop(j)

876

877 a, s0 = len(X_), Q.sA

878 args = self._m12_M12_M21(glA, s0)

879 _cjD = self._conjDist

880 for s in (-_D, _D):

881 s += s0

882 sa = 0

883 while True:

884 i += 1

885 sa = _cjD(glA, s + sa, *args) - s0

886 X = Q + (sa, sa * c0)

887 if X_.addend(X, X0.L1(X), i) > xMaX0:

888 break

889

890 elif c0 and Q in C_: # Q.c == 0

891 continue

892 else:

893 a = len(X_)

894

895 X_.addend(Q, X0.L1(Q), i + 1)

896 for X in X_[a:]: # addended Xs

897 X._skip(S_, T2d3D)

898

899 return X_.sorter(sMaX0, self._C, glA, glB, _C=_C) # generator

900

901 def All5(self, glA, glB, X0=_X000, **aMaX0_sMaX0_C):

902 '''Yield all intersection of two geodesic lines up to a limit.

903

904 @return: Yield an L{Intersector5Tuple}C{(A, B, sAB, aAB, c)}

905 for each intersection found.

906

907 @see: Methods L{All} for further details.

908 '''

909 for X in self.All(glA, glB, X0=X0, **aMaX0_sMaX0_C):

910 yield self._In5T(glA, glB, X, X)

911

912 def _Basic2(self, glA, glB, S, i=0):

913 '''(INTERNAL) Get a basic solution.

914 '''

915 X = _copy(S)

916 for _ in range(_TRIPS):

917 S = self._Spherical(glA, glB, X)

918 X += S

919 i += 1

920 if X.c or not (S.L1() > self._Tol): # or isnan

921 return self._Delto(X), i

922

923 raise IntersectionError(Fmt.no_convergence(S.L1(), self._Tol))

924

925 def _C(self, X, glA, glB, _C=False, _MM=False):

926 # add the C{_C} items to C{X}, if requested.

927 if _C:

928 A = self._Position(glA, X.sA)

929 B = self._Position(glB, X.sB)

930 s, a = self._Inversa12(A, B)

931 X.set_(latA=A.lat2, lonA=A.lon2,

932 latB=B.lat2, lonB=B.lon2)

933 if _MM: # in .Middle5

934 X.set_(sMM=s, aMM=a)

935 else:

936 X.set_(sAB=s, aAB=a)

937 return X

938

939 def Closest(self, glA, glB, X0=_X000, **_C):

940 '''Find the closest intersection of two geodesic lines.

941

942 @arg glA: A geodesic line (L{Line<Intersector.Line>}).

943 @arg glB: An other geodesic line (L{Line<Intersector.Line>}).

944 @kwarg X0: Optional I{origin} for I{L1-closeness} (L{XDict}).

945 @kwarg _C: If C{True}, include the lat-/longitudes C{latA},

946 C{lonA}, C{latB}, C{lonB} and distances C{sAB}

947 and C{aSB} between the intersections.

948

949 @return: The intersection (L{XDict}) or C{None} if none found.

950

951 @raise GeodesicError: Geodesic line B{C{glA}} or B{C{glB}}

952 invalid, incompatible or ill-configured.

953

954 @raise IntersectionError: No convergence.

955 '''

956 self._xLines(glA, glB)

957 Q, d, S_, i = X0, INF, list(X0._nD1(self._D1)), 0

958 while S_:

959 X, i = self._Basic2(glA, glB, S_.pop(0), i)

960 X = X0._fixCoincident(X)

961 if X.L1(Q) > self.Delta: # X != Q

962 d0 = X.L1(X0)

963 if d0 < self._T1:

964 Q, d, q = X, d0, i

965 break

966 if d0 < d or Q is X0:

967 Q, d, q = X, d0, i

968 X._skip(S_, self._T2D1Delta)

969

970 return None if Q is X0 else self._C(Q, glA, glB, **_C).set_(sX0=d, iteration=q)

971

972 def Closest5(self, glA, glB, X0=_X000):

973 '''Find the closest intersection of two geodesic lines.

974

975 @return: An L{Intersector5Tuple}C{(A, B, sAB, aAB, c)}

976 or C{None} if none found.

977

978 @see: Method L{Closest} for further details.

979 '''

980 X = self.Closest(glA, glB, X0=X0)

981 return X if X is None else self._In5T(glA, glB, X, X)

982

983 def _conjDist(self, gl, s, m12=0, M12=1, M21=1, semi=False):

984 # Find semi-/conjugate point relative to s0 which is close to s1.

985 # if semi:

986 # solve for M23 = 0 using dM23 / ds3 = - (1 - M23 * M32) / m23

987 # else:

988 # solve for m23 = 0 using dm23 / ds3 = M32

989 _S2, _abs, _1 = Fsum(s).fsum2_, fabs, _1_0

990 for _ in range(_TRIPS): # 2..3

991 m13, M13, M31 = self._m12_M12_M21(gl, s)

992 # see "Algorithms for geodesics", eqs. 31, 32, 33.

993 m23 = m13 * M12

994 M32 = M31 * M12

995 if m12: # PYCHOK no cover

996 m23 -= m12 * M13

997 if m12 and m13:

998 M32 += (_1 - M13 * M31) * m12 / m13

999 if semi:

1000 M23 = M13 * M21

1001 # when m12 -> eps, (1 - M12 * M21) -> eps^2, I suppose.

1002 if m12 and m13:

1003 M23 += (_1 - M12 * M21) * m13 / m12

1004 d = m23 * M23 / (_1 - M23 * M32)

1005 else:

1006 d = -m23 / M32

1007 s, d = _S2(d)

1008 if _abs(d) <= self._Tol:

1009 break

1010 return s

1011

1012 _gl3 = None

1013

1014 @Property

1015 def _conjDist3s(self):

1016 gl, self._gl3, _D = self._gl3, None, self._cHalf

1017 return tuple(self._conjDist(gl, s) for s in (-_D, 0, _D))

1018

1019 @_conjDist3s.setter # PYCHOK setter!

1020 def _conjDist3(self, gl):

1021 # _XLines(gl, gl)

1022 self._gl3 = gl

1023

1024 def _conjDist3Tt_(self, c, X0=_X000):

1025 for s in self._conjDist3s:

1026 T = XDict_(s, s * c, c)

1027 yield self._Delto(T), T.L1(X0)

1028

1029 def _conjDist5(self, azi):

1030 gl = self._Line(azi1=azi)

1031 s = self._conjDist(gl, self._cHalf)

1032 X, _ = self._Basic2(gl, gl, XDict_(s * _0_5, -s * _1_5))

1033 return s, (X.L1() - s * _2_0), azi, X.sA, X.sB

1034

1035 @Property_RO

1036 def Delta(self):

1037 '''Get the equality and tiling margin (C{meter}).

1038 '''

1039 return self._cHalf * _EPSr5 # ~15 Km WGS84

1040

1041 def _Delto(self, X):

1042 # copy Delta into X, overriding X's default

1043 X._Delta = self.Delta # NOT X.set_(self.Delta)

1044 return X

1045

1046 @Property_RO

1047 def _EPS3R(self):

1048 return _EPS3 * self.R

1049

1050 @Property_RO

1051 def _faPI_4(self):

1052 return (self.f + _2_0) * self.a * PI_4

1053

1054 @Property_RO

1055 def _GeodesicLine(self):

1056 '''(INTERNAL) Get the C{GeodesicLine} class (C{GeodesicLineExact, GeodesicLineSolve or ...}).

1057 '''

1058 return type(self._Line()), # as *args tuple

1059

1060 def _In5T(self, glA, glB, S, X, k2=False, **_2X):

1061 # Return an intersection as C{Intersector5Tuple}.

1062 A = self._Position(glA, S.sA)

1063 B = self._Position(glB, S.sB)

1064 if k2:

1065 A.set_(k2=X.kA)

1066 B.set_(k2=X.kB)

1067 s, a = self._Inversa12(A, B)

1068 return Intersector5Tuple(A._2X(glA, **_2X),

1069 B._2X(glB, **_2X), s, a, X.c, iteration=X.iteration)

1070

1071 def _Inverse(self, A, B): # caps=Caps.STANDARD

1072 return self._g.Inverse(A.lat2, A.lon2, B.lat2, B.lon2)

1073

1074 def Line(self, lat1, lon1, azi1_lat2, *lon2, **name):

1075 '''Return a geodesic line from this C{Intersector}'s geodesic, specified by

1076 two points or a point and an azimuth (bearing from North).

1077

1078 @arg lat1: Latitude of the first point (C{degrees}).

1079 @arg lon1: Longitude of the first point (C{degrees}).

1080 @arg azi1_lat2: Azimuth at the first point (compass C{degrees}) if no

1081 B{C{lon2}} argument is given, otherwise the latitude of

1082 the second point (C{degrees}).

1083 @arg lon2: If given, the longitude of the second point (C{degrees}).

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

1085

1086 @return: A line (from L{geodesic<Intersector.geodesic>}C{.Line} or

1087 C{.InverseLine} method) with C{LINE_CAPS}.

1088 '''

1089 args = self._ll3z4ll(lat1, lon1, azi1_lat2, *lon2)

1090 kwds = _xkwds(name, caps=Caps.LINE_CAPS)

1091 return self._g.InverseLine(*args, **kwds) if lon2 else \

1092 self._g.Line( *args, **kwds)

1093

1094 def _Line(self, lat1=0, lon1=0, azi1=0):

1095 return self._g.Line(lat1, lon1, azi1, caps=Caps.LINE_CAPS)

1096

1097 def Middle(self, glA, glB, raiser=True, **_C):

1098 '''Get the mid-points on two geodesic line segments.

1099

1100 @arg glA: A geodesic line (L{Line<Intersector.Line>}, 4-C{args}).

1101 @arg glB: An other geodesic line (L{Line<Intersector.Line>}, 4-C{args}).

1102 @kwarg raiser: If C{True}, check that B{C{glA}} and B{C{glB}} are a

1103 4-C{args} L{Line<Intersector.Line>} or C{InverseLine}

1104 (C{bool}).

1105 @kwarg _C: If C{True}, include the lat-/longitudes C{latA}, C{lonA},

1106 C{latB}, C{lonB} of the mid-points and half-lengths C{sA}

1107 and C{sB} in C{meter} of the respective line segments.

1108

1109 @return: The mid-point and half-length of each segment (L{XDict}),

1110 B{C{_C}} above.

1111

1112 @raise GeodesicError: Geodesic line B{C{glA}} or B{C{glB}} invalid,

1113 incompatible, ill-configured or not a 4-C{args

1114 Line} or other C{InverseLine}.

1115 '''

1116 M, _, _ = self._middle3(glA, glB, raiser)

1117 return self._C(M, glA, glB, **_C) if _C else M

1118

1119 def _middle3(self, glA, glB, raiser): # in .All, .Segment

1120 # return segment length C{sA} and C{sB} and the

1121 # center C{X0} of rectangle [sA, sB]

1122 self._xLines(glA, glB, s13=raiser) # need .Arc, .Distance

1123 sA = glA.Distance()

1124 sB = glB.Distance()

1125 X = XDict_(sA * _0_5, sB * _0_5)

1126 # _ = X._outSide(sA, sB)

1127 return self._Delto(X), sA, sB

1128

1129 def Middle5(self, glA, glB, raiser=True):

1130 '''Get the mid-points of two geodesic line segments and distances.

1131

1132 @return: A L{Middle5Tuple}C{(A, B, sMM, aMM, c)}.

1133

1134 @see: Method L{Middle} for further details.

1135 '''

1136 M, _, _ = self._middle3(glA, glB, raiser)

1137 M = self._C(M, glA, glB, _C=True, _MM=True)

1138 A, B, s, a, c = self._In5T(glA, glB, M, M, _2X=_M_)

1139 return Middle5Tuple(self._illz2G(A, glA),

1140 self._illz2G(B, glB), s, a, c)

1141

1142 def _m12_M12_M21(self, gl, s):

1143 P = gl.Position(s, outmask=Caps._REDUCEDLENGTH_GEODESICSCALE)

1144 return P.m12, P.M12, P.M21

1145

1146 def Next(self, glA, glB, eps1=None, **_C): # PYCHOK no cover

1147 '''Yield the next intersection of two I{intersecting} geodesic lines.

1148

1149 @arg glA: A geodesic line (L{Line<Intersector.Line>}).

1150 @arg glB: An other geodesic line (L{Line<Intersector.Line>}).

1151 @kwarg eps1: Optional margin for the L{euclid<pygeodesy.euclid>}ean

1152 distance (C{degrees}) between the C{(lat1, lon1)} points

1153 of both lines or C{None} for unchecked.

1154 @kwarg _C: If C{True}, include the lat-/longitudes C{latA}, C{lonA},

1155 C{latB}, C{lonB} of and distances C{sAB} and C{aSB}

1156 between the intersections.

1157

1158 @return: The intersection (L{XDict}) or C{None} if none found.

1159

1160 @raise GeodesicError: Geodesic line B{C{glA}} or B{C{glB}} invalid,

1161 incompatible, ill-configured or C{(lat1, lon1)}

1162 not B{C{eps1}}-equal.

1163

1164 @raise IntersectionError: No convergence.

1165

1166 @note: Offset C{X0} is implicit, zeros.

1167 '''

1168 self._xLines(glA, glB)

1169 if eps1 or _C: # eps

1170 _C = self._xNext(glA, glB, eps1, **_C)

1171

1172 X0, self._conjDist3s = _X000, glA # reset Property

1173 Q, d, S_, i = _XINF, INF, list(X0._nD2(self._D2)), 0

1174 while S_:

1175 X, i = self._Basic2(glA, glB, S_.pop(0), i)

1176 X = X0._fixCoincident(X)

1177 t = X.L1(X0) # == X.L1()

1178 c, z = X.c, (t <= self.Delta) # X == X0

1179 if z:

1180 if not c:

1181 continue

1182 Tt_ = self._conjDist3Tt_(c, X0)

1183 else:

1184 Tt_ = (X, t),

1185

1186 for T, t in Tt_:

1187 if t < d or Q is _XINF:

1188 Q, d, q = T, t, i

1189 i += 1

1190

1191 for s in ((_1_1t if z else _1_0_1t)

1192 if c else _0t):

1193 T = X

1194 if s and c:

1195 s *= self._D2

1196 T = X + (s, s * c) # NOT +=

1197 T._skip(S_, self._T2D2Delta)

1198

1199 return None if Q is _XINF else self._C(Q, glA, glB, **_C).set_(sX0=d, iteration=q)

1200

1201 def Next5(self, glA, glB, **eps1): # PYCHOK no cover

1202 '''Yield the next intersection of two I{intersecting} geodesic lines.

1203

1204 @return: An L{Intersector5Tuple}C{(A, B, sAB, aAB, c)} or C{None}

1205 if none found.

1206

1207 @see: Method L{Next} for further details.

1208 '''

1209 X = self.Next(glA, glB, **eps1)

1210 return X if X is None else self._In5T(glA, glB, X, X)

1211

1212 def _obliqDist4(self):

1213 zx = _45_0

1214 if self.f:

1215 _abs, _cjD5 = fabs, self._conjDist5

1216

1217 _, ds0, z0, _, _ = _cjD5(zx + _1_0)

1218 s1, ds1, z1, sAx, sBx = _cjD5(zx - _1_0)

1219 sx, dsx, zx = s1, _abs(ds1), z1

1220 # find ds(azi) = 0 by secant method

1221 for _ in range(16):

1222 if ds1 == ds0:

1223 break

1224 z = (z0 * ds1 - z1 * ds0) / (ds1 - ds0)

1225 _, ds0, z0 = s1, ds1, z1

1226 s1, ds1, z1, a, b = _cjD5(z)

1227 if _abs(ds1) < dsx:

1228 sx, dsx, zx, sAx, sBx = s1, _abs(ds1), z, a, b

1229 if not dsx:

1230 break

1231 else:

1232 sx, sAx, sBx = self._cHalf, _0_5, -_1_5

1233 return sx, zx, sAx, sBx

1234

1235 def _polarB3(self, lats=False): # PYCHOK no cover

1236 latx = _64_0

1237 lat = _90_0 - latx

1238 if self.f:

1239 _d, _pD2 = fdot, self._polarDist2

1240

1241 s0, lat0 = _pD2(latx - _1_0)

1242 s1, lat1 = _pD2(latx + _1_0)

1243 s2, lat2 = \

1244 sx, latx = _pD2(latx)

1245 # solve for ds(lat) / dlat = 0 with a quadratic fit

1246 for _ in range(8): # 1..2

1247 t = (lat1 - lat0), (lat0 - lat2), (lat2 - lat1)

1248 d = _d(t, s2, s1, s0) * _2_0

1249 if not (d > 0 or d < 0): # d == 0 or isnan(d)

1250 break

1251 lat = _d(t, (lat1 + lat0) * s2,

1252 (lat0 + lat2) * s1,

1253 (lat2 + lat1) * s0) / d

1254 s0, lat0 = s1, lat1

1255 s1, lat1 = s2, lat2

1256 s2, lat2 = _pD2(lat)

1257 if (s2 < sx) if self.f < 0 else (s2 > sx):

1258 sx, latx = s2, lat2

1259 if lats:

1260 _, lat = _pD2(latx, lat2=True)

1261 sx += sx

1262 else:

1263 sx = self._cHalf

1264 return sx, latx, lat

1265

1266 def _polarDist2(self, lat1, lat2=False):

1267 gl = self._Line(lat1=lat1)

1268 s = self._conjDist(gl, self._faPI_4, semi=True)

1269 if lat2:

1270 lat1 = gl.Position(s, outmask=Caps.LATITUDE).lat2

1271 return s, lat1

1272

1273 def _Position(self, gl, s):

1274 return gl.Position(s, outmask=Caps.STANDARD_LINE)

1275

1276 def Segment(self, glA, glB, proven=None, raiser=True, **_C):

1277 '''Find the intersection between two geodesic line segments.

1278

1279 @kwarg proven: Conjecture is that whenever two geodesic line

1280 segments intersect, the intersection is the

1281 one closest to the mid-points of segments.

1282 If so, use C{B{proven}=True}, otherwise find

1283 intersections on the segments and specify

1284 C{B{proven}=None} to return the first or

1285 C{B{proven}=False} the closest (C{bool}).

1286 @kwarg raiser: If C{True}, check that B{C{glA}} and B{C{glB}}

1287 are a 4-C{args} L{Line<Intersector.Line>} or

1288 C{InverseLine} (C{bool}).

1289 @kwarg _C: If C{True}, include the lat-/longitudes C{latA},

1290 C{lonA}, C{latB}, C{lonB} of and distances C{sAB}

1291 and C{aSB} between the intersections.

1292

1293 @return: The intersection of the segments (L{XDict}) with

1294 indicators C{kA}, C{kB} and C{k} set or if no

1295 intersection is found, C{None}.

1296

1297 @raise GeodesicError: Geodesic line B{C{glA}} or B{C{glB}}

1298 invalid, incompatible, ill-configured or

1299 not an C{InverseLine} or 4-C{args Line}.

1300

1301 @raise IntersectionError: No convergence.

1302

1303 @see: Method L{Middle<Intersector.Middle>} for further details.

1304 '''

1305 X0, sA, sB = self._middle3(glA, glB, raiser)

1306 Q = self.Closest(glA, glB, X0) # to X0

1307 if Q is not None:

1308 if Q.c: # anti-/parallel

1309 Q._fixSegment(sA, sB)

1310 # are rectangle [sA, sB] corners further from X0 than Q?

1311 d0 = X0.L1(Q)

1312 if Q._outSide(sA, sB) and d0 <= X0.L1() and not proven:

1313 i = Q.iteration

1314 for T in Q._corners(sA, sB, self._T2):

1315 X, i = self._Basic2(glA, glB, T, i)

1316 X = T._fixCoincident(X)

1317 if not X._outSide(sA, sB):

1318 d = X0.L1(X)

1319 if d < d0 or proven is None:

1320 Q, d0 = X, d

1321 if proven is None:

1322 break

1323 Q.set_(iteration=i)

1324

1325 Q = self._C(Q, glA, glB, **_C).set_(sX0=d0)

1326 return Q

1327

1328 def Segment5(self, glA, glB, **proven_raiser):

1329 '''Find the intersection between two geodesic line segments.

1330

1331 @return: An L{Intersector5Tuple}C{(A, B, sAB, aAB, c)}

1332 or C{None} if none found.

1333

1334 @see: Method L{Segment} for further details.

1335 '''

1336 X = self.Segment(glA, glB, **proven_raiser)

1337 return X if X is None else self._In5T(glA, glB, X, X, k2=True)

1338

1339 def _Spherical(self, glA, glB, S):

1340 '''(INTERNAL) Get solution based from a spherical triangle.

1341 '''

1342 # threshold for coincident geodesics/intersections ~4.3 nm WGS84.

1343 A = self._Position(glA, S.sA)

1344 B = self._Position(glB, S.sB)

1345 D = self._Inverse(A, B)

1346

1347 a, da = _diff182(A.azi2, D.azi1) # interior angle at A

1348 b, db = _diff182(B.azi2, D.azi2) # exterior angle at B

1349 c, dc = _diff182(a, b)

1350 if fsum1_(dc, db, -da, c) < 0: # inverted triangle

1351 a, da = -a, -da

1352 b, db = -b, -db

1353 sa, ca = _sincos2de(a, da)

1354 sb, cb = _sincos2de(b, db)

1355

1356 e, z, _abs = _EPS3, D.s12, fabs

1357 if _abs(z) <= self._EPS3R: # XXX z <= ...

1358 sA = sB = 0 # at intersection

1359 c = 1 if _abs(sa - sb) <= e and _abs(ca - cb) <= e else (

1360 -1 if _abs(sa + sb) <= e and _abs(ca + cb) <= e else 0)

1361 elif _abs(sa) <= e and _abs(sb) <= e: # coincident

1362 sA = ca * z * _0_5 # choose mid-point

1363 sB = -cb * z * _0_5

1364 c = 1 if (ca * cb) > 0 else -1

1365 # alt1: sA = ca * z; sB = 0

1366 # alt2: sB = -cb * z; sA = 0

1367 else: # general case

1368 sz, cz = sincos2(z / self.R)

1369 # [SKIP: Divide args by |sz| to avoid possible underflow

1370 # in {sa, sb} * sz; this is probably not necessary].

1371 # Definitely need to treat sz < 0 (z > PI*R) correctly in

1372 # order to avoid some convergence failures in _Basic2.

1373 sA = atan2(sb * sz, sb * ca * cz - sa * cb) * self.R

1374 sB = atan2(sa * sz, -sa * cb * cz + sb * ca) * self.R

1375 c = 0

1376 return XDict_(sA, sB, c) # no ._Delto

1377

1378 @Property_RO

1379 def _T2D1Delta(self):

1380 return self._T2d3Delta(self._D1)

1381

1382 @Property_RO

1383 def _T2D2Delta(self):

1384 return self._T2d3Delta(self._D2)

1385

1386 def _T2d3Delta(self, d3):

1387 return self._T2 - d3 - self.Delta

1388

1389 @Property_RO

1390 def _Tol(self): # convergence tolerance

1391 return self._cHalf * _EPSjam

1392

1393 def toStr(self, **prec_sep_name): # PYCHOK signature

1394 '''Return this C{Intersector} as string.

1395

1396 @see: L{Ellipsoid.toStr<pygeodesy.ellipsoids.Ellipsoid.toStr>}

1397 for further details.

1398

1399 @return: C{Intersector} (C{str}).

1400 '''

1401 return self._instr(props=(Intersector.geodesic,), **prec_sep_name)

1402

1403 def _xLines(self, glA, glB, s13=False):

1404 # check two geodesic lines vs this geodesic

1405 gls, C = dict(glA=glA, glB=glB), Caps.LINE_CAPS

1406 _xinstanceof(*self._GeodesicLine, **gls)

1407 for n, gl in gls.items():

1408 try:

1409 _xgeodesics(gl.geodesic, self.geodesic)

1410 if s13 and not isfinite(gl.s13): # or not gl.caps & Caps.DISTANCE_IN

1411 t = _an(typename(gl.geodesic.InverseLine))

1412 raise TypeError(_not_(t))

1413 c = gl.caps & C

1414 if c != C: # not gl.caps_(C)

1415 c, C, x = map1(bin, c, C, _xor(c, C))

1416 t = _SPACE_(typename(_xor), repr(x))[1:]

1417 raise GeodesicError(caps=c, LINE_CAPS=C, txt=t)

1418 except Exception as x:

1419 raise GeodesicError(n, gl, cause=x)

1420

1421

1422 class Intersect7Tuple(_NamedTuple):

1423 '''7-Tuple C{(A, B, sAB, aAB, c, kA, kB)} with C{A} and C{B} each

1424 a C{LatLon} or L{LatLon4Tuple}C{(lat, lon, height, datum)} of

1425 the intersection on each geodesic line, the distance C{sAB} in

1426 in C{meter} and angular distance C{aAB} in C{degrees} between

1427 C{A} and C{B}, coincidence indicator C{c} and segment indicators

1428 C{kA} and C{kB} all C{int}, see L{XDict} and method U{intersect7

1429 <_IntersectBase.intersect7>}.

1430 '''

1431 _Names_ = (_A_, _B_, _sAB_, _aAB_, _c_, 'kA', 'kB')

1432 _Units_ = (_Pass, _Pass, Meter, Degrees, Int, Int, Int)

1433

1434

1435 class Intersectool5Tuple(_NamedTuple):

1436 '''5-Tuple C{(A, B, sAB, aAB, c)} with C{A} and C{B} the C{Position}

1437 of the intersection on each geodesic line, the distance C{sAB}

1438 between C{A} and C{B} in C{meter}, the angular distance C{aAB} in

1439 C{degrees} and coincidence indicator C{c} (C{int}), see L{XDict}.

1440

1441 @note: C{A} and C{B} are each a C{GDict} with C{lat1}, C{lon1} and

1442 C{azi1} or C{lat2}, C{lon2} from the geodesic line C{glA}

1443 respectively C{glB} and the intersection location in C{latX},

1444 C{lonX}, distance C{s1X} in C{meter} and angular distance

1445 C{a1X} in C{degrees} and the segment indicator C{kX}. See

1446 L{XDict} for more details.

1447 '''

1448 _Names_ = Intersect7Tuple._Names_[:5]

1449 _Units_ = Intersect7Tuple._Units_[:5]

1450

1451

1452 class Intersector5Tuple(Intersectool5Tuple):

1453 '''5-Tuple C{(A, B, sAB, aAB, c)} with C{A} and C{B} the C{Position}

1454 of the intersection on each geodesic line, the distance C{sAB}

1455 between C{A} and C{B} in C{meter}, angular distance C{aAB} in

1456 C{degrees} and coincidence indicator C{c} (C{int}), see L{XDict}.

1457

1458 @note: C{A} and C{B} are each a C{GeodesicLine...Position} for

1459 C{outmask=Caps.STANDARD} with the intersection location in

1460 C{latX}, C{lonX}, azimuth in C{aziX}, distance C{s1X} in

1461 C{meter} and angular distance C{a1X} in C{degrees} and the

1462 segment indicator C{kX}. See L{XDict} for more details.

1463 '''

1464 _Names_ = Intersectool5Tuple._Names_

1465# _Units_ = Intersectool5Tuple._Units_

1466

1467

1468 class Middle5Tuple(Intersectool5Tuple):

1469 '''5-Tuple C{(A, B, sMM, aMM, c)} with C{A} and C{B} the I{line segments}

1470 including the mid-point location in C{latM}, C{lonM}, distance C{s1M}

1471 in C{meter} and angular distance C{a1M} in C{degrees}, the distance

1472 between both mid-points C{sMM} in C{meter} and angular distance C{aMM}

1473 in C{degrees} and coincidence indicator C{c} (C{int}). See L{XDict}

1474 for more details.

1475 '''

1476 _Names_ = (_A_, _B_, 'sMM', 'aMM', _c_)

1477# _Units_ = Intersectool5Tuple._Units_

1478

1479

1480 class _List(list):

1481

1482 _Delta = 0 # equality margin

1483

1484 def __init__(self, Delta):

1485 self._Delta = Delta

1486# list.__init__(self)

1487

1488 def __contains__(self, other):

1489 # handle C{if X in this: ...}

1490 a, b = other.sA, other.sB

1491 D, _D1 = self._Delta, _L1

1492 for X in self:

1493 if _D1(X.sA - a, X.sB - b) <= D:

1494 return True

1495 return False

1496

1497 def addend(self, X, *d0_i):

1498 # append an item, updated

1499 if d0_i:

1500 d0, i = d0_i

1501 X.set_(sX0=d0, iteration=i)

1502 self.append(X)

1503 return X.sX0

1504

1505 def sorter(self, sMaX0, dot_C, glA, glB, **_C):

1506 # trim and sort the X items

1507

1508 def _key(X):

1509 return X.sX0 # rank of X

1510

1511 t = (X for X in self if X.sX0 <= sMaX0)

1512 for X in sorted(t, key=_key):

1513 yield dot_C(X, glA, glB, **_C) if _C else X

1514

1515

1516 def _L1(a, b):

1517 '''(INTERNAL) Return the I{L1} distance.

1518 '''

1519 return fabs(a) + fabs(b)

1520

1521

1522 __all__ += _ALL_DOCS(_IntersectBase)

1523

1524 if __name__ == _DMAIN_: # MCCABE 36

1525

1526 from pygeodesy import printf

1527 __help_ = '--help'

1528

1529 def _main(args):

1530

1531 from pygeodesy import Float, GeodesicExact

1532 from pygeodesy.internals import _plural, _usage

1533 from pygeodesy.interns import _COLONSPACE_, _DOT_, _EQUAL_, \

1534 _i_, _m_, _n_, _version_, _X_

1535 import re

1536

1537 class XY0(Float):

1538 pass

1539

1540 def _opts(_h): # for _usage()

1541 ll4 = ' latA1 lonA1'

1542 ll4 += ll4.replace('1', '2')

1543 ll4 += ll4.replace(_A_, _B_)

1544 llz = _SPACE_(NN, _latA_, _lonA_, 'aziA')

1545 llz2 = llz + llz.replace(_A_, _B_)

1546 return dict(opts='-Verbose|V--version|v--help|h--Tool|T--Check|C-R <meter>-',

1547 alts=((_c_ + llz2),

1548 (_i_ + ll4),

1549 (_m_ + ll4),

1550 (_n_ + llz + ' aziB'),

1551 ('o' + llz2 + ' x0 y0')),

1552 help=_h if isinstance(_h, str) else NN)

1553

1554 def _starts(Opt, arg):

1555 return arg == Opt[1:3] or (len(arg) > 2 and Opt.startswith(arg))

1556

1557 _isopt = re.compile('^[-]+[a-z]*$', flags=re.IGNORECASE).match

1558

1559 I = Intersector(GeodesicExact()) # noqa: E741 I is eye

1560 M = m = _R = None

1561 _T = _V = _h = _C = False

1562

1563 while args and _isopt(args[0]):

1564 arg = args.pop(0)

1565 if arg == _c__:

1566 M, m = I.Closest, 6 # latA lonA aziA latB lonB aziB

1567 elif _starts('--Check', arg):

1568 _C = True

1569 elif _starts(__help_, arg):

1570 _h = args[0] if args and _isopt(args[0]) else True

1571 elif arg == _i__:

1572 M, m = I.Segment, 8 # latA1 lonA1 latA2 lonA2 latB1 lonB1 latB2 lonB2

1573 elif arg == '-m':

1574 M, m = I.Middle, 8 # latA1 lonA1 latA2 lonA2 latB1 lonB1 latB2 lonB2

1575 _R = None # zap -R

1576 elif arg == _n__:

1577 M, m = I.Next, 4 # latA lonA aziA aziB

1578 elif arg == _o__:

1579 M, m = I.Closest, 8 # latA lonA aziA latB lonB aziB x0 y0

1580 elif arg == _R__ and args:

1581 _R = args.pop(0)

1582 elif _starts('--Tool', arg):

1583 I = Intersectool() # noqa: E741 I is eye

1584 if _V:

1585 I.verbose = True

1586 if not _Xables.X_OK(I.IntersectTool):

1587 I.IntersectTool = _Xables.IntersectTool(_Xables.bin_)

1588 elif _V:

1589 _ = I.version

1590 M, _T = None, True

1591 elif _starts('--Verbose', arg):

1592 _V = True

1593 if _T:

1594 I.verbose = True

1595 elif _starts('--version', arg):

1596 printf(_COLONSPACE_(*((_version_, I.version) if _T else

1597 (__version__, repr(I)))))

1598 else:

1599 raise ValueError('invalid option %r' % (arg,))

1600

1601 if _h or M is None:

1602 printf(_usage(__file__, **_opts(_h)), nl=1)

1603 else:

1604 n = len(args)

1605 if n < m:

1606 n = _plural('only %s arg' % (n,), n) if n else 'no args'

1607 raise ValueError('%s, need %s' % (n, m))

1608 args[:] = args[:m]

1609

1610 kwds = dict(_C=True) if _C else {}

1611 if M == I.Next: # -n

1612 # get latA lonA aziA latA lonA aziB

1613 args[3:] = args[:2] + args[3:4]

1614 elif M == I.Closest and m > 6: # -o

1615 y0 = Meter(y0=args.pop())

1616 x0 = Meter(x0=args.pop())

1617 kwds.update(X0=XDict_(x0, y0))

1618 if _R:

1619 m = Meter_(_R, name=_R__, low=0)

1620 kwds.update(sMaX0=m)

1621 M = I.All

1622

1623 n = len(args) // 2

1624 glA = I.Line(*args[:n])

1625 glB = I.Line(*args[n:])

1626

1627 m = _DOT_(*map1(typename, type(I), M))

1628 if _V:

1629 X = _SPACE_(_X_, _EQUAL_, m)

1630 printf(unstr(X, glA, glB, **kwds))

1631

1632 X = M(glA, glB, **kwds)

1633 if X is None or isinstance(X, (XDict, tuple)):

1634 printf(_COLONSPACE_(m, repr(X)))

1635 else:

1636 for i, X in enumerate(X):

1637 printf(_COLONSPACE_(Fmt.INDEX(m, i), repr(X)))

1638

1639 def _examples():

1640

1641 from pygeodesy.internals import _usage_argv

1642

1643 s = _SPACE_(*_usage_argv(__file__))

1644 for t in ('-h', '-h -n',

1645 '-c 0 0 45 40 10 135',

1646 '-C -c 0 0 45 40 10 135',

1647 '-T -R 2.6e7 -c 0 0 45 40 10 135',

1648 '-c 50 -4 -147.7 0 0 90',

1649 '-C -c 50 -4 -147.7 0 0 90',

1650 '# % echo 0 0 10 10 50 -4 50S 4W | IntersectTool -i -p 0 -C',

1651 '# -631414 5988887 0 -3',

1652 '# -4.05187 -4.00000 -4.05187 -4.00000 0',

1653 '-m 0 0 10 10 50 -4 50S 4W',

1654 '-C -m 0 0 10 10 50 -4 50S 4W',

1655 '-i 0 0 10 10 50 -4 50S 4W',

1656 '-T -i 0 0 10 10 50 -4 50S 4W',

1657 '-C -i 0 0 10 10 50 -4 50S 4W',

1658 '-T -C -i 0 0 10 10 50 -4 50S 4W',

1659 '-V -T -i 0 0 10 10 50 -4 -50 -4',

1660 '-C -R 4e7 -c 50 -4 -147.7 0 0 90',

1661 '-T -C -R 4e7 -c 50 -4 -147.7 0 0 90',

1662 '-R 4e7 -i 0 0 10 10 50 -4 -50 -4',

1663 '-T -R 4e7 -i 0 0 10 10 50 -4 -50 -4'):

1664 if t.startswith(_HASH_):

1665 printf(t, nl=int(t[2] == '%'))

1666 else:

1667 printf(_SPACE_(_HASH_, s, t), nl=1)

1668 argv[1:] = t = t.split()

1669 _main(t)

1670

1671 from sys import argv, stderr

1672 try:

1673 if len(argv) == 2 and argv[1] == __help_:

1674 _examples()

1675 else:

1676 _main(argv[1:])

1677

1678 except Exception as x:

1679 x = _SPACE_(x, NN, _HASH_, *argv)

1680 printf(x, file=stderr, nl=1)

1681 if '-V' in x or _MODS.errors.exception_chaining():

1682 raise

1683 exit(1)

1684

1685

1686# % env PYGEODESY_INTERSECTTOOL=<path-to-IntersectTool> python3 -m pygeodesy.geodesici --help

1687

1688# % python3 -m pygeodesy.geodesici -h

1689#

1690# usage: python3 -m ....pygeodesy.geodesici [--Verbose | -V] [--version | -v] [--help | -h] [--Tool | -T] [--Check | -C] [-R meter]

1691# [-c latA lonA aziA latB lonB aziB |

1692# -i latA1 lonA1 latA2 lonA2 latB1 lonB1 latB2 lonB2 |

1693# -m latA1 lonA1 latA2 lonA2 latB1 lonB1 latB2 lonB2 |

1694# -n latA lonA aziA aziB |

1695# -o latA lonA aziA latB lonB aziB x0 y0]

1696

1697# % python3 -m ....pygeodesy.geodesici -h -n

1698#

1699# usage: python3 -m ....pygeodesy.geodesici -n latA lonA aziA aziB

1700

1701# % python3 -m ....pygeodesy.geodesici -c 0 0 45 40 10 135

1702# Intersector.Closest: XDict(c=0, sA=3862290.547855, sB=2339969.547699, sX0=6202260.095554)

1703

1704# % python3 -m ....pygeodesy.geodesici -C -c 0 0 45 40 10 135

1705# Intersector.Closest: XDict(aAB=0.0, c=0, latA=23.875306, latB=23.875306, lonA=26.094096, lonB=26.094096, sA=3862290.547855, sAB=0.0, sB=2339969.547699, sX0=6202260.095554)

1706

1707# % env PYGEODESY_INTERSECTTOOL=...python3 -m ....pygeodesy.geodesici -T -R 2.6e7 -c 0 0 45 40 10 135

1708# Intersectool.All[0]: XDict(c=0, sA=3862290.547855, sB=2339969.547699, sX0=6202260.095554)

1709

1710# % python3 -m ....pygeodesy.geodesici -c 50 -4 -147.7 0 0 90

1711# Intersector.Closest: XDict(c=0, sA=6058048.653081, sB=-3311252.995823, sX0=9369301.648903)

1712

1713# % python3 -m ....pygeodesy.geodesici -C -c 50 -4 -147.7 0 0 90

1714# Intersector.Closest: XDict(aAB=0.0, c=0, latA=0.0, latB=-0.0, lonA=-29.745492, lonB=-29.745492, sA=6058048.653081, sAB=0.0, sB=-3311252.995823, sX0=9369301.648903)

1715

1716# % echo 0 0 10 10 50 -4 50S 4W | IntersectTool -i -p 0 -C

1717# -631414 5988887 0 -3

1718# -4.05187 -4.00000 -4.05187 -4.00000 0

1719

1720# % python3 -m ....pygeodesy.geodesici -m 0 0 10 10 50 -4 50S 4W

1721# Intersector.Middle: XDict(c=0, sA=782554.549609, sB=5536835.161499, sX0=0.0)

1722

1723# % python3 -m ....pygeodesy.geodesici -C -m 0 0 10 10 50 -4 50S 4W

1724# Intersector.Middle: XDict(aAB=10.262308, c=0, latA=5.019509, latB=0.036282, lonA=4.961883, lonB=-4.0, sA=782554.549609, sAB=1138574.546746, sB=5536835.161499, sX0=0.0)

1725

1726# % python3 -m ....pygeodesy.geodesici -i 0 0 10 10 50 -4 50S 4W

1727# Intersector.Segment: XDict(c=0, k=-3, kA=-1, kB=0, sA=-631414.26877, sB=5988887.278435, sX0=1866020.935315)

1728

1729# % env PYGEODESY_INTERSECTTOOL=... python3 -m ....pygeodesy.geodesici -T -i 0 0 10 10 50 -4 50S 4W

1730# Intersectool.Segment: XDict(c=0, k=-3, kA=-1, kB=0, sA=-631414.26877, sB=5988887.278435)

1731

1732# % python3 -m ....pygeodesy.geodesici -C -i 0 0 10 10 50 -4 50S 4W

1733# Intersector.Segment: XDict(aAB=0.0, c=0, k=-3, kA=-1, kB=0, latA=-4.051871, latB=-4.051871, lonA=-4.0, lonB=-4.0, sA=-631414.26877, sAB=0.0, sB=5988887.278435, sX0=1866020.935315)

1734

1735# % env PYGEODESY_INTERSECTTOOL=... python3 -m ....pygeodesy.geodesici -T -C -i 0 0 10 10 50 -4 50S 4W

1736# Intersectool.Segment: XDict(c=0, k=-3, kA=-1, kB=0, latA=-4.051871, latB=-4.051871, lonA=-4.0, lonB=-4.0, sA=-631414.26877, sAB=0.0, sB=5988887.278435)

1737

1738# % env PYGEODESY_INTERSECTTOOL=... python3 -m ....pygeodesy.geodesici -V -T -i 0 0 10 10 50 -4 -50 -4

1739# Intersectool@1: /opt/local/bin/IntersectTool --version (invoke)

1740# Intersectool@1: '/opt/local/bin/IntersectTool: GeographicLib version 2.3' (0)

1741# Intersectool@1: /opt/local/bin/IntersectTool: GeographicLib version 2.3 (0)

1742# X = Intersectool.Segment(GDict(lat1=0.0, lat2=10.0, lon1=0.0, lon2=10.0), GDict(lat1=50.0, lat2=-50.0, lon1=-4.0, lon2=-4.0))

1743# Intersectool@2: /opt/local/bin/IntersectTool -E -p 10 -i \ 0.0 0.0 10.0 10.0 50.0 -4.0 -50.0 -4.0 (Segment)

1744# Intersectool@2: '-631414.2687702414 5988887.2784352796 0 -3' (0)

1745# Intersectool@2: sA=-631414.2687702414, sB=5988887.2784352796, c=0, k=-3 (0)

1746# Intersectool.Segment: XDict(c=0, k=-3, kA=-1, kB=0, sA=-631414.26877, sB=5988887.278435)

1747

1748# % python3 -m ....pygeodesy.geodesici -C -R 4e7 -c 50 -4 -147.7 0 0 90

1749# Intersector.All[0]: XDict(aAB=0.0, c=0, latA=0.0, latB=-0.0, lonA=-29.745492, lonB=-29.745492, sA=6058048.653081, sAB=0.0, sB=-3311252.995823, sX0=9369301.648903)

1750# Intersector.All[1]: XDict(aAB=0.0, c=0, latA=0.0, latB=0.0, lonA=150.046964, lonB=150.046964, sA=-13941907.021445, sAB=0.0, sB=16703151.659744, sX0=30645058.681189)

1751# Intersector.All[2]: XDict(aAB=0.0, c=0, latA=-0.0, latB=-0.0, lonA=-30.16058, lonB=-30.16058, sA=-33941862.69597, sAB=0.0, sB=-3357460.370268, sX0=37299323.066238)

1752# Intersector.All[3]: XDict(aAB=0.0, c=0, latA=-0.0, latB=0.0, lonA=150.046964, lonB=150.046964, sA=-13941907.021445, sAB=0.0, sB=-23371865.025835, sX0=37313772.047279)

1753

1754# % env PYGEODESY_INTERSECTTOOL=... python3 -m ....pygeodesy.geodesici -T -C -R 4e7 -c 50 -4 -147.7 0 0 90

1755# Intersectool.All[0]: XDict(c=0, latA=-0.0, latB=-0.0, lonA=-29.745492, lonB=-29.745492, sA=6058048.653081, sAB=0.0, sB=-3311252.995823, sX0=9369301.648903)

1756# Intersectool.All[1]: XDict(c=0, latA=0.0, latB=0.0, lonA=150.046964, lonB=150.046964, sA=-13941907.021445, sAB=0.0, sB=16703151.659744, sX0=30645058.681189)

1757# Intersectool.All[2]: XDict(c=0, latA=-0.0, latB=-0.0, lonA=-30.16058, lonB=-30.16058, sA=-33941862.69597, sAB=0.0, sB=-3357460.370268, sX0=37299323.066238)

1758# Intersectool.All[3]: XDict(c=0, latA=-0.0, latB=0.0, lonA=150.046964, lonB=150.046964, sA=-13941907.021445, sAB=0.0, sB=-23371865.025835, sX0=37313772.047279)

1759

1760# % python3 -m ....pygeodesy.geodesici -R 4e7 -i 0 0 10 10 50 -4 -50 -4

1761# Intersector.All[0]: XDict(c=0, sA=-631414.26877, sB=5988887.278435, sX0=1866020.935315)

1762# Intersector.All[1]: XDict(c=0, sA=19422725.117572, sB=-14062417.105648, sX0=38239422.83511)

1763# Intersector.All[2]: XDict(c=0, sA=19422725.117572, sB=25945445.811603, sX0=39048781.218067)

1764# Intersector.All[3]: XDict(c=0, sA=39476927.464575, sB=5894074.699478, sX0=39051612.452944)

1765

1766# % env PYGEODESY_INTERSECTTOOL=... python3 -m ....pygeodesy.geodesici -T -R 4e7 -i 0 0 10 10 50 -4 -50 -4

1767# Intersectool.All[0]: XDict(c=0, sA=-631414.26877, sB=5988887.278435, sX0=1862009.05513)

1768# Intersectool.All[1]: XDict(c=0, sA=19422725.117572, sB=-14062417.105648, sX0=38243434.715295)

1769# Intersectool.All[2]: XDict(c=0, sA=19422725.117572, sB=25945445.811603, sX0=39044769.337882)

1770# Intersectool.All[3]: XDict(c=0, sA=39476927.464575, sB=5894074.699478, sX0=39047600.57276)

1771

1772

1773# **) MIT License

1774#

1775# Copyright (C) 2024-2026 -- mrJean1 at Gmail -- All Rights Reserved.

1776#

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

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

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

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

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

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

1783#

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

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

1786#

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

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

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

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

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

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

1793# OTHER DEALINGS IN THE SOFTWARE.