Coverage for pygeodesy/geodesici.py: 91%
620 statements
« prev ^ index » next coverage.py v7.6.1, created at 2025-05-29 12:40 -0400
« prev ^ index » next coverage.py v7.6.1, created at 2025-05-29 12:40 -0400
2# -*- coding: utf-8 -*-
4u'''Classes L{Intersectool} and L{Intersector} to find the intersections of two geodesic lines or line segments.
6Class 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>}.
9Class L{Intersectool} is a wrapper to invoke I{Karney}'s U{IntersectTool
10<https://GeographicLib.SourceForge.io/C++/doc/IntersectTool.1.html>} utility, but intended I{for testing purposes only}.
12Set env variable C{PYGEODESY_INTERSECTTOOL} to the (fully qualified) path of the C{IntersectTool} executable. For usage
13and some examples run C{"env PYGEODESY_INTERSECTTOOL=<IntersectTool-path> python3 -m pygeodesy.geodesici --help"}.
15Both L{Intersectool} and L{Intersector} provide methods C{All}, C{Closest}, C{Next} and C{Segment} and produce
16L{XDict} instances with 4 or more items. Adjacent methods C{All5}, C{Closest5}, C{Next5} and C{Segment} return
17or yield L{Intersectool5Tuple} or L{Intersector5Tuple}s with the lat-, longitude and azimuth of each intersection
18as an extended, geodesic C{Position}-like L{GDict} instance.
20For more details, see the C++ U{GeographicLib<https://GeographicLib.SourceForge.io/C++/doc/index.html>}
21documentation, I{Charles F.F. Karney}'s paper U{Geodesics intersections<https://arxiv.org/abs/2308.00495>}
22and I{S. Baselga Moreno & J.C. Martinez-Llario}'s U{Intersection and point-to-line solutions for geodesics
23on the ellipsoid<https://riunet.UPV.ES/bitstream/handle/10251/122902/Revised_Manuscript.pdf>}.
24'''
25# make sure int/int division yields float quotient
26from __future__ import division as _; del _ # noqa: E702 ;
28from pygeodesy.basics import _copy, _enumereverse, map1, \
29 _xinstanceof, _xor, typename
30from pygeodesy.constants import EPS, INF, INT0, PI, PI2, PI_4, \
31 _0_0, _0_5, _1_0, _1_5, _2_0, _3_0, \
32 _45_0, _64_0, _90_0, isfinite, \
33 _EPSjam # PYCHOK used!
34from pygeodesy.ellipsoids import _EWGS84, Fmt, unstr
35from pygeodesy.errors import GeodesicError, IntersectionError, _an, \
36 _xgeodesics, _xkwds_get, _xkwds_kwds, \
37 _xkwds_pop2
38# from pygeodesy.errors import exception_chaining # _MODS
39from pygeodesy.fmath import euclid, fdot
40from pygeodesy.fsums import Fsum, fsum1_, _ceil
41# from pygeodesy.internals import typename # from .basics
42from pygeodesy.interns import NN, _A_, _B_, _c_, _COMMASPACE_, _DMAIN_, \
43 _HASH_, _M_, _not_, _SPACE_, _too_
44from pygeodesy.karney import Caps, _diff182, GDict, _sincos2de, _Xables
45from pygeodesy.lazily import _ALL_DOCS, _ALL_LAZY, _ALL_MODS as _MODS
46from pygeodesy.named import ADict, _NamedBase, _NamedTuple, _Pass
47# from pygeodesy.namedTuples import _LL4Tuple # _MODS
48from pygeodesy.props import deprecated_method, Property, \
49 Property_RO, property_RO, property_ROver
50from pygeodesy.solveBase import _SolveCapsBase, pairs
51# from pygeodesy.streprs import pairs # from .solveBase
52# from pygeodesy.streprs import Fmt, unstr # from .ellipsoids
53from pygeodesy.units import Azimuth as Azi, Degrees, Float, Int, \
54 _isDegrees, Lat, Lon, Meter, Meter_
55from pygeodesy.utily import atan2, sincos2, fabs, radians
57# from math import ceil as _ceil, fabs, radians # .fsums, .utily
59__all__ = _ALL_LAZY.geodesici
60__version__ = '25.05.12'
62_0t = 0, # int
63_1_1t = -1, +1
64_1_0_1t = -1, 0, +1
65_aAB_ = 'aAB'
66_c__ = '-c' # PYCHOK used!
67_cWGS84 = _EWGS84.a * PI2 # outer circumference
68_EPS3 = EPS * _3_0
69_EPSr5 = pow(EPS, 0.2) # PYCHOK used! 7.4e-4 or ~3"
70_i__ = '-i' # PYCHOK used!
71_latA_ = 'latA'
72_lonA_ = 'lonA'
73_n__ = '-n' # PYCHOK used!
74_o__ = '-o' # PYCHOK used!
75_R__ = '-R'
76_sAB_ = 'sAB'
77_sX0_ = 'sX0'
78_TRIPS = 128
81class XDict(ADict):
82 '''4+Item result from L{Intersectool} and L{Intersector} methods
83 C{All}, C{Closest}, C{Next} and C{Segment} with the intersection
84 offsets C{sA}, C{sB} and C{sX0} in C{meter} and the coincidence
85 indicator C{c}, an C{int}, +1 for parallel, -1 for anti-parallel
86 or 0 otherwise.
88 Offsets C{sA} and C{sB} are distances measured I{along} geodesic
89 line C{glA} respectively C{glB}, but C{sX0} is the I{L1-distance}
90 between the intersection and the I{origin} C{X0}.
92 If present, distance C{sAB} and angular distance C{aAB} represent
93 the difference between the intersection point on geodesic lines
94 C{glA} and C{glB} in C{meter} respectively C{degrees}, typically
95 below C{5e-9 meter} or C{5 nm} and C{5e-14 degrees} or C{1 n"}.
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
101 C{k} is I{Karney}'s C{segmode}, equal C{kA * 3 + kB}.
102 '''
103 _Delta = EPS # default margin, see C{Intersector._Delto}
105 def __add__(self, other):
106 X = _copy(self)
107 X += other
108 return X
110 def __eq__(self, other):
111 return not self.__ne__(other)
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
126 def __le__(self, other):
127 # _xinstanceof(XDict, other=other)
128 return self == other or self < other
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 self != other) # PYCHOK sB
135 def __ne__(self, other):
136 # _xinstanceof(XDict, other=other)
137 return self is not other and self.L1(other) > self._Delta
139 def _corners(self, sA, sB, T2):
140 # yield all corners further than C{T2}
141 a, b = self.sA, self.sB # PYCHOK sA, sB
142 for x in (0, sA):
143 for y in (0, sB):
144 if _L1(x - a, y - b) >= T2:
145 yield XDict_(x, y)
147 def _fixCoincident(self, X, c0=0):
148 # return the mid-point if C{X} is anti-/parallel
149 c = c0 or X.c
150 if c:
151 s = (self.sA - X.sA + # PYCHOK sA
152 (self.sB - X.sB) * c) * _0_5 # PYCHOK sB
153 X = X + (s, s * c) # NOT +=
154 return X
156 def _fixSegment(self, sA, sB): # PYCHOK no cover
157 # modify this anti-/parallel C{XDict}
158 a, b, c = self.sA, self.sB, self.c # PYCHOK sA, sB, c
160 def _g(): # intersection in smallest gap
161 if c > 0: # distance to [A, B] is |(a - b) - (A - B)|
162 t = a - b # consider corners [0, sB] and [sA, 0]
163 t = fabs(t + sB) < fabs(t - sA)
164 s = a + b
165 else: # distance to [A, B] is |(a + b) - (A + B)|
166 t = a + b # consider corner [0, 0] and [sA, sB]
167 t = fabs(t) < fabs(t - (sA + sB))
168 s = sB + (a - b)
169 return (sB if t else sA) - s
171 ta = -a
172 tb = sA - a
173 tc = -c * b
174 td = -c * (b - sB)
176 ga = 0 <= (b + c * ta) <= sB
177 gb = 0 <= (b + c * tb) <= sB
178 gc = 0 <= (a + tc) <= sA
179 gd = 0 <= (a + td) <= sA
181 # test opposite rectangle sides first
182 s = ((ta + tb) if ga and gb else (
183 (tc + td) if gc and gd else (
184 (ta + tc) if ga and gc else (
185 (ta + td) if ga and gd else (
186 (tb + tc) if gb and gc else (
187 (tb + td) if gb and gd else _g())))))) * _0_5
188 self += s, s * c
190 @property_RO
191 def _is00(self):
192 return not (self.sA or self.sB) # PYCHOK sA, sB
194 def L1(self, other=None):
195 '''Return the C{L1} distance.
196 '''
197 a, b = self.sA, self.sB # PYCHOK sA, sB
198 if other is not None:
199 # _xinstanceof(XDict, other=other)
200 a -= other.sA
201 b -= other.sB
202 return _L1(a, b)
204 def _nD1(self, D1):
205 # yield the C{Closest} starts
206 D_ = 0, D1, -D1
207 for a, b in zip((0, 1, -1, 0, 0),
208 (0, 0, 0, 1, -1)):
209 yield self + (D_[a], D_[b])
211 def _nD2(self, D2):
212 # yield the C{Next} starts
213 D22 = D2 * _2_0
214 D_ = 0, D2, D22, -D22, -D2
215 for a, b in zip((-1, -1, 1, 1, -2, 0, 2, 0),
216 (-1, 1, -1, 1, 0, 2, 0, -2)):
217 yield self + (D_[a], D_[b])
219 def _nmD3(self, n, m, D3): # d3 / 2
220 # yield the C{All} starts
221 yield self
222 for i in range(n, m, 2):
223 for j in range(n, m, 2):
224 if i or j: # skip self
225 yield self + ((i + j) * D3,
226 (i - j) * D3)
228 def _outSide(self, sA, sB):
229 # is this C{Xdist} outside one or both segments?
230 a, b = self.sA, self.sB # PYCHOK sA, sB
231 kA = -1 if a < 0 else (+1 if a > sA else INT0)
232 kB = -1 if b < 0 else (+1 if b > sB else INT0)
233 self.set_(kA=kA, kB=kB, k=(kA * 3 + kB) or INT0)
234 return bool(kA or kB)
236 def _skip(self, S_, T1_Delta):
237 # remove starts from list C{S_} near this C{XDict}
238 for j, S in _enumereverse(S_):
239 if S.L1(self) < T1_Delta:
240 S_.pop(j)
243def XDict_(sA=_0_0, sB=_0_0, c=INT0, sX0=_0_0):
244 '''(INTERNAL) New L{XDict} from positionals.
245 '''
246 return XDict(sA=sA, sB=sB, c=c, sX0=sX0)
248_X000 = XDict_() # PYCHOK origin
249_XINF = XDict_(INF)
252class _IntersectBase(_NamedBase):
253 '''(INTERNAL) Base class for L{Intersectool} and L{Intersector}.
254 '''
255 # _g = None
257 def __init__(self, geodesic, **name):
258 _xinstanceof(*_EWGS84._Geodesics, geodesic=geodesic)
259 self._g = geodesic
260 if name:
261 self.name = name
263 @Property_RO
264 def a(self):
265 '''Get the I{equatorial} radius, semi-axis (C{meter}).
266 '''
267 return self.ellipsoid.a
269 equatoradius = a # = Requatorial
271 def All(self, glA, glB, **kwds): # PYCHOK no cover
272 '''(INTERNAL) I{Must be overloaded}.'''
273 self._notOverloaded(glA, glB, **kwds)
275 @Property_RO
276 def _cHalf(self): # normalizer, semi-circumference
277 return self.R * PI # ~20K Km WGS84
279 @Property_RO
280 def _cMax(self): # outer circumference
281 return max(self.a, self.ellipsoid.b, self.R) * PI2
283 @property_RO
284 def datum(self):
285 '''Get the geodesic's datum (C{Datum}).
286 '''
287 return self.geodesic.datum
289 @Property_RO
290 def ellipsoid(self):
291 '''Get the C{geodesic}'s ellipsoid (C{Ellipsoid}).
292 '''
293 return self.geodesic.datum.ellipsoid
295 @Property_RO
296 def f(self):
297 '''Get the I{flattening} (C{scalar}), C{0} for spherical, negative for prolate.
298 '''
299 return self.ellipsoid.f
301 flattening = f
303 @property_RO
304 def geodesic(self):
305 '''Get the C{geodesic} (C{Geodesic...}).
306 '''
307 return self._g
309 def _illz2G(self, G, il):
310 '''(INTERNAL) Set C{InverseLine} 1-/2-attrs into C{G}, a C{GDict}.
311 '''
312 try:
313 G.set_(lat1=il.lat1, lon1=il.lon1, azi1=il.azi1, a12=il.a13, # .Arc()
314 lat2=il.lat2, lon2=il.lon2, azi2=il.azi2, s12=il.s13) # .Distance()
315 except AttributeError:
316 r = il.Position(il.s13, outmask=Caps.STANDARD_LINE) # isfinite(il.s13)
317 G.set_(**r)
318# for n, v in r.items():
319# if not hasattr(il, n):
320# setattr(il, n, v)
321 return G
323 def intersect7(self, start1, end1, start2, end2, X0=_X000, aMaX0=0, sMaX0=_cWGS84,
324 **LatLon_and_kwds):
325 '''Yield the intersection points of two lines, each defined by two (ellipsoidal)
326 points or by an (ellipsoidal) start point and an azimuth from North.
328 @arg start1: Start point of the first line (C{LatLon}).
329 @arg end1: End point of the first line (C{LatLon}) or the azimuth at the
330 B{C{start1}} point (compass C{degrees360}).
331 @arg start2: Start point of the second line (C{LatLon}).
332 @arg end2: End point of the second line (C{LatLon}) or the azimuth at the
333 B{C{start2}} point (compass C{degrees360}).
334 @kwarg X0: Optional I{origin} for I{L1-distances} (L{XDict}) or C{None} for
335 the L{Middle<Intersector.Middle>}, otherwise C{XDiff_(0, 0)}.
336 @kwarg aMaX0: Upper limit for the I{angular L1-distance}
337 (C{degrees}) or C{None} or C{0} for unlimited.
338 @kwarg sMaX0_C: Optional, upper limit C{B{sMaX0}=2*PI*R} for the
339 I{L1-distance} to B{C{X0}} (C{meter}).
340 @kwarg LatLon_and_kwds: Optional class C{B{LatLon}=None} to return intersection
341 points and optional, additional B{C{LatLon}} keyword arguments.
343 @note: The C{lat} and C{lon} attr of B{C{start1}}, B{C{end1}}, B{C{start2}} and
344 B{C{end2}} are used I{verbatim}, ignoring C{datum} or C{ellipsoid}.
346 @return: Yield an L{Intersect7Tuple}C{(A, B, sAB, aAB, c, kA, kB)} for every
347 intersection found, with C{A} and C{B} each a B{C{LatLon}} or if
348 C{B{LatLon} is None} or not specified, a L{LatLon4Tuple}C{(lat, lon,
349 height, datum)} with C{height 0} and this C{datum}.
351 @raise GeodesicError: Invalid B{C{start1}}, B{C{end1}}, B{C{start2}} or
352 B{C{end2}} or B{C{end1}} and B{C{end2}} differ in type.
354 @raise IntersectionError: No convergence.
355 '''
357 def _args(s, e):
358 t = (e,) if _isDegrees(e) else (e.lat, e.lon)
359 return (s.lat, s.lon) + t
361 try:
362 glA = self.Line(*_args(start1, end1))
363 glB = self.Line(*_args(start2, end2))
364 except Exception as x:
365 raise GeodesicError(start1=start1, end1=end1, start2=start2, end2=end2, cause=x)
367 LL, kwds = _xkwds_pop2(LatLon_and_kwds, LatLon=None)
368 d, kwds = _xkwds_pop2(kwds, datum=self.datum)
369 h, kwds = _xkwds_pop2(kwds, height=0)
371 _LL4T = _MODS.namedTuples._LL4Tuple
372 for X in self.All(glA, glB, X0=X0, aMaX0=aMaX0, sMaX0=sMaX0, _C=True):
373 A = B = _LL4T(X.latA, X.lonA, h, d, LL, kwds, iteration=X.iteration)
374 if X.sAB or X.latA != X.latB or X.lonA != X.lonB:
375 B = _LL4T(X.latB, X.lonB, h, d, LL, kwds, iteration=X.iteration)
376 yield Intersect7Tuple(A, B, X.sAB, X.aAB, X.c, _xkwds_get(X, kA=0),
377 _xkwds_get(X, kB=0))
379 def _Inversa12(self, A, B=None):
380 lls = (0, 0, A, 0) if B is None else (A.lat2, A.lon2,
381 B.lat2, B.lon2)
382 r = self._g.Inverse(*lls, outmask=Caps.DISTANCE)
383 return r.s12, r.a12 # .a12 always in r
385 def k2kAkB(self, k):
386 '''Unravel C{k} into C{kA} and C{kB}.
388 @arg k: Segment indicator C{kA * 3 + kB} (C{int}).
390 @return: An C{ADict(k=k, kA=kA, kB=kB)}.
392 @raise GeodesicError: Invalid B{C{k}}.
393 '''
394 for kA in range(-1, 2):
395 for kB in range(-1, 2):
396 if (kA * 3 + kB) == k:
397 return ADict(k=k, kA=kA, kB=kB)
398 raise GeodesicError(k=k)
400# def k2kAkB(self, k):
401# # unravel C{k} into C{kA} and C{kB}.
402# kA, kB = divmod(k, 3)
403# if kB > 1:
404# kA += 1
405# kB -= 3
406# return kA, kB
408 def Line(self, lat1, lon1, azi1_lat2, *lon2, **name): # PYCHOK no cover
409 '''(INTERNAL) I{Must be overloaded}.'''
410 self._notOverloaded(lat1, lon1, azi1_lat2, *lon2, **name)
412 def _ll3z4ll(self, lat1, lon1, azi1_lat2, *lon2):
413 t = Lat(lat1=lat1), Lon(lon1=lon1)
414 if lon2: # get azis for All, keep lat-/lons
415 t += Lat(lat2=azi1_lat2), Lon(lon2=lon2[0])
416 else:
417 t += Azi(azi1=azi1_lat2),
418 return t
420 @deprecated_method
421 def Next5s(self, glA, glB, X0=_X000, aMax=1801, sMax=0, **unused): # PYCHOK no cover
422 '''DEPRECATED on 2024.07.02, use method C{All5}.'''
423 return self.All5(glA, glB, X0=X0, aMaX0=aMax, sMaX0=sMax) # PYCHOK attr
425 @Property_RO
426 def R(self):
427 '''Get the I{authalic} earth radius (C{meter}).
428 '''
429 return self.ellipsoid.R2
431 def _sMaX0_C2(self, aMaX0=0, **sMaX0_C):
432 _g = _xkwds_get
433 s = _g(sMaX0_C, sMaX0=self._cMax)
434 s = _g(sMaX0_C, sMax=s) # for backward ...
435 a = _g(sMaX0_C, aMax=aMaX0) # ... compatibility
436 if a: # degrees to meter, approx.
437 s = min(s, self.R * radians(a)) # ellipsoid.degrees2m(a)
438 s = _g(sMaX0_C, _R=s)
439 if s < _EPS3:
440 s = _EPS3 # raise GeodesicError(sMaX0=s)
441 return s, _g(sMaX0_C, _C=False)
443 def _xNext(self, glA, glB, eps1, **eps_C): # PYCHOK no cover
444 eps1 = _xkwds_get(eps_C, eps=eps1) # eps for backward compatibility
445 if eps1 is not None:
446 a = glA.lat1 - glB.lat1
447 b = glA.lon1 - glB.lon1
448 if euclid(a, b) > eps1:
449 raise GeodesicError(lat_=a, lon_=b, eps1=eps1)
450 return _xkwds_kwds(eps_C, _C=False)
453class Intersectool(_IntersectBase, _SolveCapsBase):
454 '''Wrapper to invoke I{Karney}'s utility U{IntersectTool
455 <https://GeographicLib.SourceForge.io/C++/doc/IntersectTool.1.html>}
456 similar to class L{Intersector<geodesici.Intersector>}.
458 @note: Use property C{IntersectTool} or env variable C{PYGEODESY_INTERSECTTOOL}
459 to specify the (fully qualified) path to the C{IntersectTool} executable.
461 @note: This C{Intersectool} is intended I{for testing purposes only}, it invokes
462 the C{IntersectTool} executable for I{every} method call.
463 '''
464 _c_alt = _c__, # Closest latA lonA aziA latB lonB aziB
465 _C_option = '-C',
466 _Error = GeodesicError
467 _i_alt = _i__, # Segment latA1 lonA1 latA2 lonA2 latB1 lonB1 latB2 lonB2
468 _linelimit = 1200 # line printer width X 10
469 _n_alt = _n__, # Next latA lonA aziA aziB
470 _Names_ABs = _latA_, _lonA_, 'latB', 'lonB', _sAB_ # -C to stderr
471 _Names_XDict = 'sA', 'sB', _c_ # plus 'k' from -i or 'sX0' from -R
472 _o_alt = _o__, # Offset latA lonA aziA latB lonB aziB x0 y0
473 _Xable_name = _Xables.IntersectTool.__name__ # typename
474 _Xable_path = _Xables.IntersectTool()
476 def __init__(self, a_geodesic=None, f=None, **name):
477 '''New L{IntersectTool}.
479 @arg a_geodesic: Earth' equatorial axis (C{meter}) or a geodesic
480 (L{GeodesicExact<pygeodesy.geodesicx.GeodesicExact>},
481 wrapped L{Geodesic<pygeodesy.geodesicw.Geodesic>} or
482 L{GeodesicSolve<pygeodesy.geodsolve.GeodesicSolve>}).
483 @kwarg f: Earth' flattening (C{scalar}), required if B{C{a_geodesic}}
484 is in C{meter}, ignored otherwise.
485 @kwarg name: Optional C{B{name}=NN} (C{str}).
487 @raise GeodesicError: The eccentricity of the B{C{geodesic}}'s ellipsoid is too
488 large or no initial convergence.
490 @see: The B{Note} at I{Karney}'s C++ U{Intersect<https://GeographicLib.sourceforge.io/
491 C++/doc/classGeographicLib_1_1Intersect.html#ae41f54c9a44836f6c8f140f6994930cf>}.
492 '''
493 g = self._GeodesicExact() if a_geodesic is None else (a_geodesic if f is None else
494 self._GeodesicExact(a_geodesic, f))
495 _IntersectBase.__init__(self, g, **name)
497 def All(self, glA, glB, X0=_X000, eps1=_0_0, aMaX0=0, **sMaX0_C): # PYCHOK signature
498 '''Yield all intersection of two geodesic lines up to a limit.
500 @kwarg eps1: Optional margin for the L{euclid<pygeodesy.euclid>}ean distance
501 (C{degrees}) between the C{(lat1, lon1)} points of both lines for
502 using the L{IntersectTool<Intersectool.IntersectTool>}'s C{"-n"}
503 option, unless C{B{eps1}=None}.
505 @return: An L{XDict} for each intersection.
506 '''
507 for X, _ in self._All2(glA, glB, X0, eps1, aMaX0=aMaX0, **sMaX0_C):
508 yield X
510 def _All2(self, glA, glB, X0, eps1, **aMaX0_sMaX0_C): # MCCABE 13
511 '''(INTERNAL) Helper for methods C{.All} and C{.All5}.
512 '''
513 def _xz2(**gl):
514 try:
515 n, gl = gl.popitem() # _xkwds_item2(gl)
516 try:
517 return self._c_alt, (gl.azi1,)
518 except (AttributeError, KeyError):
519 return self._i_alt, (gl.lat2, gl.lon2)
520 except Exception as x:
521 raise GeodesicError(n, gl, cause=x)
523 _t, a = _xz2(glA=glA)
524 _x, b = _xz2(glB=glB)
525 if _x is not _t:
526 raise GeodesicError(glA=glA, glB=glB)
528 A = glA.lat1, glA.lon1
529 B = glB.lat1, glB.lon1
530 if _x is self._c_alt:
531 if X0 is _X000 or X0._is00:
532 if eps1 is not None and \
533 euclid(glA.lat1 - glB.lat1,
534 glA.lon1 - glB.lon1) <= eps1:
535 _x, B = self._n_alt, ()
536 else: # non-zero offset
537 _x = self._o_alt
538 b += X0.sA, X0.sB
540 sMaX0, _C = self._sMaX0_C2(**aMaX0_sMaX0_C)
541 for X in self._XDictInvoke(_x, _sX0_, (A + a + B + b),
542 _C=_C, _R=sMaX0):
543 if _C:
544 T = self._In5T(glA, glB, X, X)
545 if _aAB_ not in X:
546 X.set_(sAB=T.sAB, aAB=T.aAB)
547 else:
548 T = None
549 yield X.set_(c=int(X.c)), T
551 def All5(self, glA, glB, X0=_X000, **aMaX0_sMaX0):
552 '''Yield all intersection of two geodesic lines up to a limit.
554 @return: An L{Intersectool5Tuple} for each intersection.
555 '''
556 for _, T in self._All2(glA, glB, X0, _0_0, _C=True, **aMaX0_sMaX0):
557 yield T
559 @Property_RO
560 def _cmdBasic(self):
561 '''(INTERNAL) Get the basic C{IntersectTool} cmd (C{tuple}).
562 '''
563 return (self.IntersectTool,) + (self._e_option +
564 self._E_option +
565 self._p_option)
567 def Closest(self, glA, glB, X0=_X000, _C=False):
568 '''Find the closest intersection of two geodesic lines.
570 @kwarg _C: Use C{B{_C}=True} to include the C{"-C"} results (C{bool}).
572 @return: An L{XDict}.
573 '''
574 args = glA.lat1, glA.lon1, glA.azi1, \
575 glB.lat1, glB.lon1, glB.azi1
576 if X0 is _X000 or X0._is000:
577 _x = self._c_alt
578 else:
579 _x = self._o_alt
580 args += X0.sA, X0.sB
581 return self._XDictInvoke(_x, NN, args, _C=_C) # _R=None)
583 def Closest5(self, glA, glB, **unused):
584 '''Find the closest intersection of two geodesic lines.
586 @return: An L{Intersectool5Tuple}.
587 '''
588 X = self.Closest(glA, glB, _C=True)
589 return self._In5T(glA, glB, X, X)
591 @property_ROver
592 def _GeodesicExact(self):
593 '''Get the I{class} L{GeodesicExact}, I{once}.
594 '''
595 return _MODS.geodesicx.GeodesicExact # overwrite property_ROver
597 def _In5T(self, glA, glB, S, X, k2=False, **_2X):
598 A = GDict(glA).set_(lat2=X.latA, lon2=X.lonA, s12=S.sA)
599 B = GDict(glB).set_(lat2=X.latB, lon2=X.lonB, s12=S.sB)
600 if k2:
601 A.set_(k2=X.kA)
602 B.set_(k2=X.kB)
603 s, a = self._Inversa12(A, B)
604 sAB = _xkwds_get(X, sAB=s)
605 if a and s and s != sAB:
606 a *= sAB / s # adjust a
607 return Intersectool5Tuple(A._2X(glA, **_2X),
608 B._2X(glB, **_2X), sAB, a, X.c)
610 @Property
611 def IntersectTool(self):
612 '''Get the U{IntersectTool<https://GeographicLib.SourceForge.io/C++/doc/IntersectTool.1.html>}
613 executable (C{filename}).
614 '''
615 return self._Xable_path
617 @IntersectTool.setter # PYCHOK setter!
618 def IntersectTool(self, path):
619 '''Set the U{IntersectTool<https://GeographicLib.SourceForge.io/C++/doc/IntersectTool.1.html>}
620 executable (C{filename}), the (fully qualified) path to the C{IntersectTool} executable.
622 @raise GeodesicError: Invalid B{C{path}}, B{C{path}} doesn't exist or isn't the
623 C{IntersectTool} executable.
624 '''
625 self._setXable(path)
627 def Line(self, lat1, lon1, azi1_lat2, *lon2, **name):
628 '''Return a geodesic line from this C{Intersector}'s geodesic, specified by
629 two (goedetic) points or a (goedetic) point and an (forward) azimuth.
631 @return: A 3- or 6-item, named L{GDict}.
632 '''
633 args = self._ll3z4ll(lat1, lon1, azi1_lat2, *lon2)
634 gl = GDict((u.name, u) for u in args)
635# if lon2: # get azis for All, use lat-/lons as given
636# r = self._g.Inverse(outmask=Caps.AZIMUTH, *args)
637# gl.set_(azi1=Azi(azi1=r.azi1), azi2=Azi(azi2=r.azi2))
638 if name:
639 gl.name= name
640 return gl
642 def Middle(self, glA, glB, **_C):
643 '''Get the mid-points on two geodesic line segments.
645 @kwarg _C: Use C{B{_C}=True} to include the C{"-C"} results (C{bool}).
647 @return: An L{XDict}.
648 '''
649 X, _, _, _, _ = self._middle5(glA, glB, **_C)
650 return X
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]
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)
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
676 def Middle5(self, glA, glB, **unused):
677 '''Get the mid-points on two geodesic line segments and their distance.
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)
686 def Next(self, glA, glB, eps1=None, **_C): # PYCHOK no cover
687 '''Find the next intersection of two I{intersecting} geodesic lines.
689 @kwarg _C: Use C{B{_C}=True} to include the option C{"-C"} results (C{bool}).
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
699 def Next5(self, glA, glB, **eps1): # PYCHOK no cover
700 '''Find the next intersection of two I{intersecting} geodesic lines.
702 @return: An L{Intersectool5Tuple}.
703 '''
704 X = self.Next(glA, glB, _C=True, **eps1)
705 return self._In5T(glA, glB, X, X)
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
712 def Segment(self, glA, glB, **_C_unused):
713 '''Find the intersection between two geodesic line segments.
715 @kwarg _C: Use C{B{_C}=True} to include the option C{"-C"} results (C{bool}).
717 @return: An L{XDict}.
718 '''
719 X = self._XDictInvoke(self._i_alt, 'k',
720 (glA.lat1, glA.lon1, glA.lat2, glA.lon2,
721 glB.lat1, glB.lon1, glB.lat2, glB.lon2),
722 _C=_xkwds_get(_C_unused, _C=False)) # _R=None
723 try:
724 ks = self.k2kAkB(int(X.k))
725 except Exception as x:
726 raise GeodesicError(glA=glA, glB=glB, X=str(X), cause=x)
727 return X.set_(**ks)
729 def Segment5(self, glA, glB, **unused):
730 '''Find the next intersection of two I{intersecting} geodesic lines.
732 @return: An L{Intersectool5Tuple}.
733 '''
734 X = self.Segment(glA, glB, _C=True)
735 return self._In5T(glA, glB, X, X, k2=True)
737 def toStr(self, prec=6, sep=_COMMASPACE_, **unused): # PYCHOK signature
738 '''Return this C{Intersectool} as string.
740 @kwarg prec_sep: Keyword argumens C{B{prec}=6} and C{B{sep}=", "}
741 for the C{float} C{prec}ision, number of decimal digits
742 (0..9) and the C{sep}arator string to join. Trailing
743 zero decimals are stripped for B{C{prec}} values of 1
744 and above, but kept for negative B{C{prec}} values.
746 @return: Intersectool items (C{str}).
747 '''
748 d = dict(geodesic=self.geodesic, invokation=self.invokation,
749 status=self.status,
750 IntersectTool=self.IntersectTool)
751 return sep.join(pairs(d, prec=prec))
753 def _XDictInvoke(self, alt, _k_sX0, args, _C=False, **_R):
754 '''(INTERNAL) Invoke C{IntersectTool}, return results as C{XDict} or
755 a C{generator} if keyword argument C{B{_R}=sMaX0} is specified.
756 '''
757 # assert len(args) == {self._c_alt: 6,
758 # self._i_alt: 8,
759 # self._n_alt: 4,
760 # self._o_alt: 8}.get(alt, len(args))
761 cmd = self._cmdBasic
762 Names = self._Names_XDict # has _c_ always
763 if _k_sX0:
764 Names += _k_sX0,
765 if _C:
766 cmd += self._C_option
767 Names += self._Names_ABs
768 if _R:
769 cmd += self._R_option(**_R)
770 X, _R = self._DictInvoke2(cmd + alt, args, Names, XDict, **_R)
771 return X if _R else X.set_(c=int(X.c)) # generator or XDict
774class Intersector(_IntersectBase):
775 '''Finder of intersections between two goedesic lines, each an instance
776 of L{GeodesicLineExact<pygeodesy.geodesicx.GeodesicLineExact>},
777 wrapped L{GeodesicLine<pygeodesy.geodesicw.GeodesicLine>} or
778 L{GeodesicLineSolve<pygeodesy.geodsolve.GeodesicLineSolve>}.
780 @see: I{Karney}'s C++ class U{Intersect<https://GeographicLib.sourceforge.io/
781 C++/doc/classGeographicLib_1_1Intersect.html#details>} for more details.
782 '''
784 def __init__(self, geodesic, **name):
785 '''New L{Intersector}.
787 @arg geodesic: The geodesic (L{GeodesicExact<pygeodesy.geodesicx.GeodesicExact>},
788 wrapped L{Geodesic<pygeodesy.geodesicw.Geodesic>} or
789 L{GeodesicSolve<pygeodesy.geodsolve.GeodesicSolve>}).
790 @kwarg name: Optional C{B{name}=NN} (C{str}).
792 @raise GeodesicError: The eccentricity of the B{C{geodesic}}'s ellipsoid is too
793 large or no initial convergence.
795 @see: The B{Note} at I{Karney}'s C++ U{Intersect<https://GeographicLib.sourceforge.io/
796 C++/doc/classGeographicLib_1_1Intersect.html#ae41f54c9a44836f6c8f140f6994930cf>}.
797 '''
798 _IntersectBase.__init__(self, geodesic, **name)
799 E = self.ellipsoid
800 t1 = E.b * PI # min distance between intersects
801 t2 = self._polarDist2(_90_0)[0] * _2_0 # furthest, closest intersect
802 t5 = self._Inversa12( _90_0)[0] * _2_0 # longest, shortest geodesic
803 if self.f > 0:
804 t3 = self._obliqDist4()[0]
805 t4 = t1
806 else: # PYCHOK no cover
807 t1, t2, t3 = t2, t1, t5
808 t4, _, _ = self._polarB3()
810 self._D1 = d1 = t2 * _0_5 # ~E.L tile spacing for Closest
811 self._D2 = d2 = t3 / _1_5 # tile spacing for Next
812 self._D3 = d3 = t4 - self.Delta # tile spacing for All
813 self._T1 = t1 # min distance between intersects
814 self._T2 = t2 = t1 * _2_0
815# self._T5 = t5 # not used
816 if not (d1 < d3 and d2 < d3 and d2 < t2):
817 t = Fmt.PARENSPACED(_too_('eccentric'), E.e)
818 raise GeodesicError(ellipsoid=E.toStr(terse=2), txt=t)
820 def All(self, glA, glB, X0=None, aMaX0=0, **sMaX0_C): # MCCABE 13
821 '''Yield all intersection of two geodesic lines up to a limit.
823 @arg glA: A geodesic line (L{Line<Intersector.Line>}).
824 @arg glB: An other geodesic line (L{Line<Intersector.Line>}).
825 @kwarg X0: Optional I{origin} for I{L1-distances} (L{XDict}) or
826 C{None} for the L{Middle<Intersector.Middle>} of both
827 lines if both are a 4-C{args} L{Line<Intersector.Line>}
828 or C{InverseLine}, otherwise C{XDiff_(0, 0)}.
829 @kwarg aMaX0: Upper limit for the I{angular L1-distance}
830 (C{degrees}) or C{None} or C{0} for unlimited.
831 @kwarg sMaX0_C: Optional, upper limit C{B{sMaX0}=2*PI*R} for the
832 I{L1-distance} to B{C{X0}} (C{meter}) and option
833 C{B{_C}=False} to include the intersection lat-/
834 longitudes C{latA}, C{lonA}, C{latB}, C{lonB} and
835 distances C{sAB} and C{aSB}.
837 @return: Yield an L{XDict} for each intersection found.
839 @raise GeodesicError: Geodesic line B{C{glA}} or B{C{glB}}
840 invalid, incompatible or ill-configured.
842 @raise IntersectionError: No convergence.
843 '''
844 self._xLines(glA, glB)
845 if X0 is None:
846 try: # determine X0
847 X0, _, _ = self._middle3(glA, glB, True)
848 except GeodesicError: # no .Distance
849 X0 = _X000
850 sMaX0, _C = self._sMaX0_C2(aMaX0, **sMaX0_C)
852 D, _D = self.Delta, self._cHalf # C++ _d
853 xMaX0 = sMaX0 + D
854 m = int(_ceil(xMaX0 / self._D3)) # m x m tiles
855 d3 = xMaX0 / m
856 T2d3D = self._T2d3Delta(d3)
858 C_ = _List(D) # closest coincident
859 X_ = _List(D) # intersections found
860 c0 = 0
861 S_ = list(X0._nmD3(1 - m, m, d3 * _0_5))
862 # assert len(S_) == m * m + (m - 1) % 2
863 while S_:
864 Q, i = self._Basic2(glA, glB, S_.pop(0))
865 if Q in X_:
866 continue
867 if Q.c: # coincident intersection # PYCHOK no cover
868 _X0fx = X0._fixCoincident
869 Q = _X0fx(Q) # Q = Q'
870 if c0 and Q in C_:
871 continue
872 C_.addend(Q)
873 # elimate all existing intersections
874 # on this line (which didn't set c0)
875 c0 = Q.c
876 for j, X in _enumereverse(X_):
877 if _X0fx(X, c0).L1(Q) <= D: # X' == Q
878 X_.pop(j)
880 a, s0 = len(X_), Q.sA
881 args = self._m12_M12_M21(glA, s0)
882 _cjD = self._conjDist
883 for s in (-_D, _D):
884 s += s0
885 sa = 0
886 while True:
887 i += 1
888 sa = _cjD(glA, s + sa, *args) - s0
889 X = Q + (sa, sa * c0)
890 if X_.addend(X, X0.L1(X), i) > xMaX0:
891 break
893 elif c0 and Q in C_: # Q.c == 0
894 continue
895 else:
896 a = len(X_)
898 X_.addend(Q, X0.L1(Q), i + 1)
899 for X in X_[a:]: # addended Xs
900 X._skip(S_, T2d3D)
902 return X_.sorter(sMaX0, self._C, glA, glB, _C=_C) # generator
904 def All5(self, glA, glB, X0=_X000, **aMaX0_sMaX0_C):
905 '''Yield all intersection of two geodesic lines up to a limit.
907 @return: Yield an L{Intersector5Tuple}C{(A, B, sAB, aAB, c)}
908 for each intersection found.
910 @see: Methods L{All} for further details.
911 '''
912 for X in self.All(glA, glB, X0=X0, **aMaX0_sMaX0_C):
913 yield self._In5T(glA, glB, X, X)
915 def _Basic2(self, glA, glB, S, i=0):
916 '''(INTERNAL) Get a basic solution.
917 '''
918 X = _copy(S)
919 for _ in range(_TRIPS):
920 S = self._Spherical(glA, glB, X)
921 X += S
922 i += 1
923 if X.c or S.L1() <= self._Tol: # or isnan
924 return self._Delto(X), i
926 raise IntersectionError(Fmt.no_convergence(S.L1(), self._Tol))
928 def _C(self, X, glA, glB, _C=False, _MM=False):
929 # add the C{_C} items to C{X}, if requested.
930 if _C:
931 A = self._Position(glA, X.sA)
932 B = self._Position(glB, X.sB)
933 s, a = self._Inversa12(A, B)
934 X.set_(latA=A.lat2, lonA=A.lon2,
935 latB=B.lat2, lonB=B.lon2)
936 if _MM: # in .Middle5
937 X.set_(sMM=s, aMM=a)
938 else:
939 X.set_(sAB=s, aAB=a)
940 return X
942 def Closest(self, glA, glB, X0=_X000, **_C):
943 '''Find the closest intersection of two geodesic lines.
945 @arg glA: A geodesic line (L{Line<Intersector.Line>}).
946 @arg glB: An other geodesic line (L{Line<Intersector.Line>}).
947 @kwarg X0: Optional I{origin} for I{L1-closeness} (L{XDict}).
948 @kwarg _C: If C{True}, include the lat-/longitudes C{latA},
949 C{lonA}, C{latB}, C{lonB} oon and distances C{sAB}
950 and C{aSB} between the intersections.
952 @return: The intersection (L{XDict}) or C{None} if none found.
954 @raise GeodesicError: Geodesic line B{C{glA}} or B{C{glB}}
955 invalid, incompatible or ill-configured.
957 @raise IntersectionError: No convergence.
958 '''
959 self._xLines(glA, glB)
960 Q, d, S_, i = X0, INF, list(X0._nD1(self._D1)), 0
961 while S_:
962 X, i = self._Basic2(glA, glB, S_.pop(0), i)
963 X = X0._fixCoincident(X)
964 if X.L1(Q) > self.Delta: # X != Q
965 d0 = X.L1(X0)
966 if d0 < self._T1:
967 Q, d, q = X, d0, i
968 break
969 if d0 < d or Q is X0:
970 Q, d, q = X, d0, i
971 X._skip(S_, self._T2D1Delta)
973 return None if Q is X0 else self._C(Q, glA, glB, **_C).set_(sX0=d, iteration=q)
975 def Closest5(self, glA, glB, X0=_X000):
976 '''Find the closest intersection of two geodesic lines.
978 @return: An L{Intersector5Tuple}C{(A, B, sAB, aAB, c)}
979 or C{None} if none found.
981 @see: Method L{Closest} for further details.
982 '''
983 X = self.Closest(glA, glB, X0=X0)
984 return X if X is None else self._In5T(glA, glB, X, X)
986 def _conjDist(self, gl, s, m12=0, M12=1, M21=1, semi=False):
987 # Find semi-/conjugate point relative to s0 which is close to s1.
988 # if semi:
989 # solve for M23 = 0 using dM23 / ds3 = - (1 - M23 * M32) / m23
990 # else:
991 # solve for m23 = 0 using dm23 / ds3 = M32
992 _S2, _abs, _1 = Fsum(s).fsum2_, fabs, _1_0
993 for _ in range(_TRIPS):
994 m13, M13, M31 = self._m12_M12_M21(gl, s)
995 # see "Algorithms for geodesics", eqs. 31, 32, 33.
996 m23 = m13 * M12
997 M32 = M31 * M12
998 if m12: # PYCHOK no cover
999 m23 -= m12 * M13
1000 if m13:
1001 M32 += (_1 - M13 * M31) * m12 / m13
1002 if semi:
1003 M23 = M13 * M21
1004 # when m12 -> eps, (1 - M12 * M21) -> eps^2, I suppose.
1005 if m12 and m13:
1006 M23 += (_1 - M12 * M21) * m13 / m12
1007 d = m23 * M23 / (_1 - M23 * M32)
1008 else:
1009 d = -m23 / M32
1010 s, d = _S2(d)
1011 if _abs(d) <= self._Tol:
1012 break
1013 return s
1015 _gl3 = None
1017 @Property
1018 def _conjDist3s(self):
1019 gl, self._gl3, _D = self._gl3, None, self._cHalf
1020 return tuple(self._conjDist(gl, s) for s in (-_D, 0, _D))
1022 @_conjDist3s.setter # PYCHOK setter!
1023 def _conjDist3(self, gl):
1024 # _XLines(gl, gl)
1025 self._gl3 = gl
1027 def _conjDist3Tt_(self, c, X0=_X000):
1028 for s in self._conjDist3s:
1029 T = XDict_(s, s * c, c)
1030 yield self._Delto(T), T.L1(X0)
1032 def _conjDist5(self, azi):
1033 gl = self._Line(azi1=azi)
1034 s = self._conjDist(gl, self._cHalf)
1035 X, _ = self._Basic2(gl, gl, XDict_(s * _0_5, -s * _1_5))
1036 return s, (X.L1() - s * _2_0), azi, X.sA, X.sB
1038 @Property_RO
1039 def Delta(self):
1040 '''Get the equality and tiling margin (C{meter}).
1041 '''
1042 return self._cHalf * _EPSr5 # ~15 Km WGS84
1044 def _Delto(self, X):
1045 # copy Delta into X, overriding X's default
1046 X._Delta = self.Delta # NOT X.set_(self.Delta)
1047 return X
1049 @Property_RO
1050 def _EPS3R(self):
1051 return _EPS3 * self.R
1053 @Property_RO
1054 def _faPI_4(self):
1055 return (self.f + _2_0) * self.a * PI_4
1057 @Property_RO
1058 def _GeodesicLines(self):
1059 '''(INTERNAL) Get the C{Geodesic...Line} class(es).
1060 '''
1061 return type(self._Line()),
1063 def _In5T(self, glA, glB, S, X, k2=False, **_2X):
1064 # Return an intersection as C{Intersector5Tuple}.
1065 A = self._Position(glA, S.sA)
1066 B = self._Position(glB, S.sB)
1067 if k2:
1068 A.set_(k2=X.kA)
1069 B.set_(k2=X.kB)
1070 s, a = self._Inversa12(A, B)
1071 return Intersector5Tuple(A._2X(glA, **_2X),
1072 B._2X(glB, **_2X), s, a, X.c, iteration=X.iteration)
1074 def _Inverse(self, A, B): # caps=Caps.STANDARD
1075 return self._g.Inverse(A.lat2, A.lon2, B.lat2, B.lon2)
1077 def Line(self, lat1, lon1, azi1_lat2, *lon2, **name):
1078 '''Return a geodesic line from this C{Intersector}'s geodesic, specified by
1079 two (goedetic) points or a (goedetic) point and an (initial) azimuth.
1081 @arg lat1: Latitude of the first point (C{degrees}).
1082 @arg lon1: Longitude of the first point (C{degrees}).
1083 @arg azi1_lat2: Azimuth at the first point (compass C{degrees}) if no
1084 B{C{lon2}} argument is given, otherwise the latitude of
1085 the second point (C{degrees}).
1086 @arg lon2: If given, the longitude of the second point (C{degrees}).
1087 @kwarg name: Optional C{B{name}=NN} (C{str}).
1089 @return: A line (from L{geodesic<Intersector.geodesic>}C{.Line} or
1090 C{.InverseLine} method) with C{LINE_CAPS}.
1091 '''
1092 args = self._ll3z4ll(lat1, lon1, azi1_lat2, *lon2)
1093 gl = self._g.InverseLine(*args, caps=Caps.LINE_CAPS) if lon2 else \
1094 self._g.Line( *args, caps=Caps.LINE_CAPS)
1095 if name:
1096 gl.name= name
1097 return gl
1099 def _Line(self, lat1=0, lon1=0, azi1=0):
1100 return self._g.Line(lat1, lon1, azi1, caps=Caps.LINE_CAPS)
1102 def Middle(self, glA, glB, raiser=True, **_C):
1103 '''Get the mid-points on two geodesic line segments.
1105 @arg glA: A geodesic line (L{Line<Intersector.Line>}, 4-C{args}).
1106 @arg glB: An other geodesic line (L{Line<Intersector.Line>}, 4-C{args}).
1107 @kwarg raiser: If C{True}, check that B{C{glA}} and B{C{glB}} are a
1108 4-C{args} L{Line<Intersector.Line>} or C{InverseLine}
1109 (C{bool}).
1110 @kwarg _C: If C{True}, include the lat-/longitudes C{latA}, C{lonA},
1111 C{latB}, C{lonB} of the mid-points and half-lengths C{sA}
1112 and C{sB} in C{meter} of the respective line segments.
1114 @return: The mid-point and half-length of each segment (L{XDict}),
1115 B{C{_C}} above.
1117 @raise GeodesicError: Geodesic line B{C{glA}} or B{C{glB}} invalid,
1118 incompatible, ill-configured or not a 4-C{args
1119 Line} or other C{InverseLine}.
1120 '''
1121 M, _, _ = self._middle3(glA, glB, raiser)
1122 return self._C(M, glA, glB, **_C) if _C else M
1124 def _middle3(self, glA, glB, raiser): # in .All, .Segment
1125 # return segment length C{sA} and C{sB} and the
1126 # center C{X0} of rectangle [sA, sB]
1127 self._xLines(glA, glB, s13=raiser) # need .Arc, .Distance
1128 sA = glA.Distance()
1129 sB = glB.Distance()
1130 X = XDict_(sA * _0_5, sB * _0_5)
1131 # _ = X._outSide(sA, sB)
1132 return self._Delto(X), sA, sB
1134 def Middle5(self, glA, glB, raiser=True):
1135 '''Get the mid-points of two geodesic line segments and distances.
1137 @return: A L{Middle5Tuple}C{(A, B, sMM, aMM, c)}.
1139 @see: Method L{Middle} for further details.
1140 '''
1141 M, _, _ = self._middle3(glA, glB, raiser)
1142 M = self._C(M, glA, glB, _C=True, _MM=True)
1143 A, B, s, a, c = self._In5T(glA, glB, M, M, _2X=_M_)
1144 return Middle5Tuple(self._illz2G(A, glA),
1145 self._illz2G(B, glB), s, a, c)
1147 def _m12_M12_M21(self, gl, s):
1148 P = gl.Position(s, outmask=Caps._REDUCEDLENGTH_GEODESICSCALE)
1149 return P.m12, P.M12, P.M21
1151 def Next(self, glA, glB, eps1=None, **_C): # PYCHOK no cover
1152 '''Yield the next intersection of two I{intersecting} geodesic lines.
1154 @arg glA: A geodesic line (L{Line<Intersector.Line>}).
1155 @arg glB: An other geodesic line (L{Line<Intersector.Line>}).
1156 @kwarg eps1: Optional margin for the L{euclid<pygeodesy.euclid>}ean
1157 distance (C{degrees}) between the C{(lat1, lon1)} points
1158 of both lines or C{None} for unchecked.
1159 @kwarg _C: If C{True}, include the lat-/longitudes C{latA}, C{lonA},
1160 C{latB}, C{lonB} of and distances C{sAB} and C{aSB}
1161 between the intersections.
1163 @return: The intersection (L{XDict}) or C{None} if none found.
1165 @raise GeodesicError: Geodesic line B{C{glA}} or B{C{glB}} invalid,
1166 incompatible, ill-configured or C{(lat1, lon1)}
1167 not B{C{eps1}}-equal.
1169 @raise IntersectionError: No convergence.
1171 @note: Offset C{X0} is implicit, zeros.
1172 '''
1173 self._xLines(glA, glB)
1174 if eps1 or _C: # eps
1175 _C = self._xNext(glA, glB, eps1, **_C)
1177 X0, self._conjDist3s = _X000, glA # reset Property
1178 Q, d, S_, i = _XINF, INF, list(X0._nD2(self._D2)), 0
1179 while S_:
1180 X, i = self._Basic2(glA, glB, S_.pop(0), i)
1181 X = X0._fixCoincident(X)
1182 t = X.L1(X0) # == X.L1()
1183 c, z = X.c, (t <= self.Delta) # X == X0
1184 if z:
1185 if not c:
1186 continue
1187 Tt_ = self._conjDist3Tt_(c, X0)
1188 else:
1189 Tt_ = (X, t),
1191 for T, t in Tt_:
1192 if t < d or Q is _XINF:
1193 Q, d, q = T, t, i
1194 i += 1
1196 for s in ((_1_1t if z else _1_0_1t)
1197 if c else _0t):
1198 T = X
1199 if s and c:
1200 s *= self._D2
1201 T = X + (s, s * c) # NOT +=
1202 T._skip(S_, self._T2D2Delta)
1204 return None if Q is _XINF else self._C(Q, glA, glB, **_C).set_(sX0=d, iteration=q)
1206 def Next5(self, glA, glB, **eps1): # PYCHOK no cover
1207 '''Yield the next intersection of two I{intersecting} geodesic lines.
1209 @return: An L{Intersector5Tuple}C{(A, B, sAB, aAB, c)} or C{None}
1210 if none found.
1212 @see: Method L{Next} for further details.
1213 '''
1214 X = self.Next(glA, glB, **eps1)
1215 return X if X is None else self._In5T(glA, glB, X, X)
1217 def _obliqDist4(self):
1218 zx = _45_0
1219 if self.f:
1220 _abs, _cjD5 = fabs, self._conjDist5
1222 _, ds0, z0, _, _ = _cjD5(zx + _1_0)
1223 s1, ds1, z1, sAx, sBx = _cjD5(zx - _1_0)
1224 sx, dsx, zx = s1, _abs(ds1), z1
1225 # find ds(azi) = 0 by secant method
1226 for _ in range(16):
1227 if ds1 == ds0:
1228 break
1229 z = (z0 * ds1 - z1 * ds0) / (ds1 - ds0)
1230 _, ds0, z0 = s1, ds1, z1
1231 s1, ds1, z1, a, b = _cjD5(z)
1232 if _abs(ds1) < dsx:
1233 sx, dsx, zx, sAx, sBx = s1, _abs(ds1), z, a, b
1234 if not dsx:
1235 break
1236 else:
1237 sx, sAx, sBx = self._cHalf, _0_5, -_1_5
1238 return sx, zx, sAx, sBx
1240 def _polarB3(self, lats=False): # PYCHOK no cover
1241 latx = _64_0
1242 lat = _90_0 - latx
1243 if self.f:
1244 _d, _pD2 = fdot, self._polarDist2
1246 s0, lat0 = _pD2(latx - _1_0)
1247 s1, lat1 = _pD2(latx + _1_0)
1248 s2, lat2 = \
1249 sx, latx = _pD2(latx)
1250 prolate = self.f < 0
1251 # solve for ds(lat) / dlat = 0 with a quadratic fit
1252 for _ in range(_TRIPS):
1253 t = (lat1 - lat0), (lat0 - lat2), (lat2 - lat1)
1254 d = _d(t, s2, s1, s0) * _2_0
1255 if not d: # or isnan(d)
1256 break
1257 lat = _d(t, (lat1 + lat0) * s2,
1258 (lat0 + lat2) * s1,
1259 (lat2 + lat1) * s0) / d
1260 s0, lat0 = s1, lat1
1261 s1, lat1 = s2, lat2
1262 s2, lat2 = _pD2(lat)
1263 if (s2 < sx) if prolate else (s2 > sx):
1264 sx, latx = s2, lat2
1265 if lats:
1266 _, lat = _pD2(latx, lat2=True)
1267 sx += sx
1268 else:
1269 sx = self._cHalf
1270 return sx, latx, lat
1272 def _polarDist2(self, lat1, lat2=False):
1273 gl = self._Line(lat1=lat1)
1274 s = self._conjDist(gl, self._faPI_4, semi=True)
1275 if lat2:
1276 lat1 = gl.Position(s, outmask=Caps.LATITUDE).lat2
1277 return s, lat1
1279 def _Position(self, gl, s):
1280 return gl.Position(s, outmask=Caps.STANDARD_LINE)
1282 def Segment(self, glA, glB, proven=None, raiser=True, **_C):
1283 '''Find the intersection between two geodesic line segments.
1285 @kwarg proven: Conjecture is that whenever two geodesic line
1286 segments intersect, the intersection is the
1287 one closest to the mid-points of segments.
1288 If so, use C{B{proven}=True}, otherwise find
1289 intersections on the segments and specify
1290 C{B{proven}=None} to return the first or
1291 C{B{proven}=False} the closest (C{bool}).
1292 @kwarg raiser: If C{True}, check that B{C{glA}} and B{C{glB}}
1293 are a 4-C{args} L{Line<Intersector.Line>} or
1294 C{InverseLine} (C{bool}).
1295 @kwarg _C: If C{True}, include the lat-/longitudes C{latA},
1296 C{lonA}, C{latB}, C{lonB} of and distances C{sAB}
1297 and C{aSB} between the intersections.
1299 @return: The intersection of the segments (L{XDict}) with
1300 indicators C{kA}, C{kB} and C{k} set or if no
1301 intersection is found, C{None}.
1303 @raise GeodesicError: Geodesic line B{C{glA}} or B{C{glB}}
1304 invalid, incompatible, ill-configured or
1305 not an C{InverseLine} or 4-C{args Line}.
1307 @raise IntersectionError: No convergence.
1309 @see: Method L{Middle<Intersector.Middle>} for further details.
1310 '''
1311 X0, sA, sB = self._middle3(glA, glB, raiser)
1312 Q = self.Closest(glA, glB, X0) # to X0
1313 if Q is not None:
1314 if Q.c: # anti-/parallel
1315 Q._fixSegment(sA, sB)
1316 # are rectangle [sA, sB] corners further from X0 than Q?
1317 d0 = X0.L1(Q)
1318 if Q._outSide(sA, sB) and d0 <= X0.L1() and not proven:
1319 i = Q.iteration
1320 for T in Q._corners(sA, sB, self._T2):
1321 X, i = self._Basic2(glA, glB, T, i)
1322 X = T._fixCoincident(X)
1323 if not X._outSide(sA, sB):
1324 d = X0.L1(X)
1325 if d < d0 or proven is None:
1326 Q, d0 = X, d
1327 if proven is None:
1328 break
1329 Q.set_(iteration=i)
1331 Q = self._C(Q, glA, glB, **_C).set_(sX0=d0)
1332 return Q
1334 def Segment5(self, glA, glB, **proven_raiser):
1335 '''Find the intersection between two geodesic line segments.
1337 @return: An L{Intersector5Tuple}C{(A, B, sAB, aAB, c)}
1338 or C{None} if none found.
1340 @see: Method L{Segment} for further details.
1341 '''
1342 X = self.Segment(glA, glB, **proven_raiser)
1343 return X if X is None else self._In5T(glA, glB, X, X, k2=True)
1345 def _Spherical(self, glA, glB, S):
1346 '''(INTERNAL) Get solution based from a spherical triangle.
1347 '''
1348 # threshold for coincident geodesics/intersections ~4.3 nm WGS84.
1349 A = self._Position(glA, S.sA)
1350 B = self._Position(glB, S.sB)
1351 D = self._Inverse(A, B)
1353 a, da = _diff182(A.azi2, D.azi1) # interior angle at A
1354 b, db = _diff182(B.azi2, D.azi2) # exterior angle at B
1355 c, dc = _diff182(a, b)
1356 if fsum1_(dc, db, -da, c) < 0: # inverted triangle
1357 a, da = -a, -da
1358 b, db = -b, -db
1359 sa, ca = _sincos2de(a, da)
1360 sb, cb = _sincos2de(b, db)
1362 e, z, _abs = _EPS3, D.s12, fabs
1363 if _abs(z) <= self._EPS3R: # XXX z <= ...
1364 sA = sB = 0 # at intersection
1365 c = 1 if _abs(sa - sb) <= e and _abs(ca - cb) <= e else (
1366 -1 if _abs(sa + sb) <= e and _abs(ca + cb) <= e else 0)
1367 elif _abs(sa) <= e and _abs(sb) <= e: # coincident
1368 sA = ca * z * _0_5 # choose mid-point
1369 sB = -cb * z * _0_5
1370 c = 1 if (ca * cb) > 0 else -1
1371 # alt1: sA = ca * z; sB = 0
1372 # alt2: sB = -cb * z; sA = 0
1373 else: # general case
1374 sz, cz = sincos2(z / self.R)
1375 # [SKIP: Divide args by |sz| to avoid possible underflow
1376 # in {sa, sb} * sz; this is probably not necessary].
1377 # Definitely need to treat sz < 0 (z > PI*R) correctly in
1378 # order to avoid some convergence failures in _Basic2.
1379 sA = atan2(sb * sz, sb * ca * cz - sa * cb) * self.R
1380 sB = atan2(sa * sz, -sa * cb * cz + sb * ca) * self.R
1381 c = 0
1382 return XDict_(sA, sB, c) # no ._Delto
1384 @Property_RO
1385 def _T2D1Delta(self):
1386 return self._T2d3Delta(self._D1)
1388 @Property_RO
1389 def _T2D2Delta(self):
1390 return self._T2d3Delta(self._D2)
1392 def _T2d3Delta(self, d3):
1393 return self._T2 - d3 - self.Delta
1395 @Property_RO
1396 def _Tol(self): # convergence tolerance
1397 return self._cHalf * _EPSjam
1399 def toStr(self, **prec_sep_name): # PYCHOK signature
1400 '''Return this C{Intersector} as string.
1402 @see: L{Ellipsoid.toStr<pygeodesy.ellipsoids.Ellipsoid.toStr>}
1403 for further details.
1405 @return: C{Intersector} (C{str}).
1406 '''
1407 return self._instr(props=(Intersector.geodesic,), **prec_sep_name)
1409 def _xLines(self, glA, glB, s13=False):
1410 # check two geodesic lines vs this geodesic
1411 C, gls = Caps.LINE_CAPS, dict(glA=glA, glB=glB)
1412 _xinstanceof(*self._GeodesicLines, **gls)
1413 for n, gl in gls.items():
1414 try:
1415 _xgeodesics(gl.geodesic, self.geodesic)
1416 if s13 and not isfinite(gl.s13): # or not gl.caps & Caps.DISTANCE_IN
1417 t = _an(typename(gl.geodesic.InverseLine))
1418 raise TypeError(_not_(t))
1419 c = gl.caps & C
1420 if c != C: # not gl.caps_(C)
1421 c, C, x = map1(bin, c, C, _xor(c, C))
1422 t = _SPACE_(typename(_xor), repr(x))[1:]
1423 raise GeodesicError(caps=c, LINE_CAPS=C, txt=t)
1424 except Exception as x:
1425 raise GeodesicError(n, gl, cause=x)
1428class Intersect7Tuple(_NamedTuple):
1429 '''7-Tuple C{(A, B, sAB, aAB, c, kA, kB)} with C{A} and C{B} each
1430 a C{LatLon} or L{LatLon4Tuple}C{(lat, lon, height, datum)} of
1431 the intersection on each geodesic line, the distance C{sAB} in
1432 in C{meter} and angular distance C{aAB} in C{degrees} between
1433 C{A} and C{B}, coincidence indicator C{c} and segment indicators
1434 C{kA} and C{kB} all C{int}, see L{XDict} and method U{intersect7
1435 <_IntersectBase.intersect7>}.
1436 '''
1437 _Names_ = (_A_, _B_, _sAB_, _aAB_, _c_, 'kA', 'kB')
1438 _Units_ = (_Pass, _Pass, Meter, Degrees, Int, Int, Int)
1441class Intersectool5Tuple(_NamedTuple):
1442 '''5-Tuple C{(A, B, sAB, aAB, c)} with C{A} and C{B} the C{Position}
1443 of the intersection on each geodesic line, the distance C{sAB}
1444 between C{A} and C{B} in C{meter}, the angular distance C{aAB} in
1445 C{degrees} and coincidence indicator C{c} (C{int}), see L{XDict}.
1447 @note: C{A} and C{B} are each a C{GDict} with C{lat1}, C{lon1} and
1448 C{azi1} or C{lat2}, C{lon2} from the geodesic line C{glA}
1449 respectively C{glB} and the intersection location in C{latX},
1450 C{lonX}, distance C{s1X} in C{meter} and angular distance
1451 C{a1M} in C{degrees} and the segment indicator C{kX}. See
1452 L{XDict} for more details.
1453 '''
1454 _Names_ = Intersect7Tuple._Names_[:5]
1455 _Units_ = Intersect7Tuple._Units_[:5]
1458class Intersector5Tuple(Intersectool5Tuple):
1459 '''5-Tuple C{(A, B, sAB, aAB, c)} with C{A} and C{B} the C{Position}
1460 of the intersection on each geodesic line, the distance C{sAB}
1461 between C{A} and C{B} in C{meter}, angular distance C{aAB} in
1462 C{degrees} and coincidence indicator C{c} (C{int}), see L{XDict}.
1464 @note: C{A} and C{B} are each a C{GeodesicLine...Position} for
1465 C{outmask=Caps.STANDARD} with the intersection location in
1466 C{latX}, C{lonX}, azimuth in C{aziX}, distance C{s1X} in
1467 C{meter} and angular distance C{a1X} in C{degrees} and the
1468 segment indicator C{kX}. See L{XDict} for more details.
1469 '''
1470 _Names_ = Intersectool5Tuple._Names_
1473class Middle5Tuple(Intersectool5Tuple):
1474 '''5-Tuple C{(A, B, sMM, aMM, c)} with C{A} and C{B} the I{line segments}
1475 including the mid-point location in C{latM}, C{lonM}, distance C{s1M}
1476 in C{meter} and angular distance C{a1M} in C{degrees}, the distance
1477 between both mid-points C{sMM} in C{meter} and angular distance C{aMM}
1478 in C{degrees} and coincidence indicator C{c} (C{int}). See L{XDict}
1479 for more details.
1480 '''
1481 _Names_ = (_A_, _B_, 'sMM', 'aMM', _c_)
1484class _List(list):
1486 _Delta = 0 # equality margin
1488 def __init__(self, Delta):
1489 self._Delta = Delta
1490# list.__init__(self)
1492 def __contains__(self, other):
1493 # handle C{if X in this: ...}
1494 a, b = other.sA, other.sB
1495 D, _D1 = self._Delta, _L1
1496 for X in self:
1497 if _D1(X.sA - a, X.sB - b) <= D:
1498 return True
1499 return False
1501 def addend(self, X, *d0_i):
1502 # append an item, updated
1503 if d0_i:
1504 d0, i = d0_i
1505 X.set_(sX0=d0, iteration=i)
1506 self.append(X)
1507 return X.sX0
1509 def sorter(self, sMaX0, dot_C, glA, glB, **_C):
1510 # trim and sort the X items
1512 def _key(X):
1513 return X.sX0 # rank of X
1515 t = (X for X in self if X.sX0 <= sMaX0)
1516 for X in sorted(t, key=_key):
1517 yield dot_C(X, glA, glB, **_C) if _C else X
1520def _L1(a, b):
1521 '''(INTERNAL) Return the I{L1} distance.
1522 '''
1523 return fabs(a) + fabs(b)
1526__all__ += _ALL_DOCS(_IntersectBase)
1528if __name__ == _DMAIN_: # MCCABE 14
1530 from pygeodesy import printf
1531 __help_ = '--help'
1533 def _main(args):
1535 from pygeodesy import GeodesicExact
1536 from pygeodesy.internals import _plural, _usage
1537 from pygeodesy.interns import _COLONSPACE_, _DOT_, _EQUAL_, \
1538 _i_, _m_, _n_, _version_, _X_
1539 import re
1541 class XY0(Float):
1542 pass
1544 def _opts(_h): # for _usage()
1545 ll4 = ' latA1 lonA1'
1546 ll4 += ll4.replace('1', '2')
1547 ll4 += ll4.replace(_A_, _B_)
1548 llz = _SPACE_(NN, _latA_, _lonA_, 'aziA')
1549 llz2 = llz + llz.replace(_A_, _B_)
1550 return dict(opts='-Verbose|V--version|v--help|h--Tool|T--Check|C-R <meter>-',
1551 alts=((_c_ + llz2),
1552 (_i_ + ll4),
1553 (_m_ + ll4),
1554 (_n_ + llz + ' aziB'),
1555 ('o' + llz2 + ' x0 y0')),
1556 help=_h if isinstance(_h, str) else NN)
1558 def _starts(Opt, arg):
1559 return arg == Opt[1:3] or (len(arg) > 2 and Opt.startswith(arg))
1561 _isopt = re.compile('^[-]+[a-z]*$', flags=re.IGNORECASE).match
1563 I = Intersector(GeodesicExact()) # noqa: E741 I is eye
1564 M = m = _R = None
1565 _T = _V = _h = _C = False
1567 while args and _isopt(args[0]):
1568 arg = args.pop(0)
1569 if arg == _c__:
1570 M, m = I.Closest, 6 # latA lonA aziA latB lonB aziB
1571 elif _starts('--Check', arg):
1572 _C = True
1573 elif _starts(__help_, arg):
1574 _h = args[0] if args and _isopt(args[0]) else True
1575 elif arg == _i__:
1576 M, m = I.Segment, 8 # latA1 lonA1 latA2 lonA2 latB1 lonB1 latB2 lonB2
1577 elif arg == '-m':
1578 M, m = I.Middle, 8 # latA1 lonA1 latA2 lonA2 latB1 lonB1 latB2 lonB2
1579 _R = None # zap -R
1580 elif arg == _n__:
1581 M, m = I.Next, 4 # latA lonA aziA aziB
1582 elif arg == _o__:
1583 M, m = I.Closest, 8 # latA lonA aziA latB lonB aziB x0 y0
1584 elif arg == _R__ and args:
1585 _R = args.pop(0)
1586 elif _starts('--Tool', arg):
1587 I = Intersectool() # noqa: E741 I is eye
1588 if _V:
1589 I.verbose = True
1590 if not _Xables.X_OK(I.IntersectTool):
1591 I.IntersectTool = _Xables.IntersectTool(_Xables.bin_)
1592 elif _V:
1593 _ = I.version
1594 M, _T = None, True
1595 elif _starts('--Verbose', arg):
1596 _V = True
1597 if _T:
1598 I.verbose = True
1599 elif _starts('--version', arg):
1600 printf(_COLONSPACE_(*((_version_, I.version) if _T else
1601 (__version__, repr(I)))))
1602 else:
1603 raise ValueError('invalid option %r' % (arg,))
1605 if _h or M is None:
1606 printf(_usage(__file__, **_opts(_h)), nl=1)
1607 else:
1608 n = len(args)
1609 if n < m:
1610 n = _plural('only %s arg' % (n,), n) if n else 'no args'
1611 raise ValueError('%s, need %s' % (n, m))
1612 args[:] = args[:m]
1614 kwds = dict(_C=True) if _C else {}
1615 if M == I.Next: # -n
1616 # get latA lonA aziA latA lonA aziB
1617 args[3:] = args[:2] + args[3:4]
1618 elif M == I.Closest and m > 6: # -o
1619 y0 = Meter(y0=args.pop())
1620 x0 = Meter(x0=args.pop())
1621 kwds.update(X0=XDict_(x0, y0))
1622 if _R:
1623 m = Meter_(_R, name=_R__, low=0)
1624 kwds.update(sMaX0=m)
1625 M = I.All
1627 n = len(args) // 2
1628 glA = I.Line(*args[:n])
1629 glB = I.Line(*args[n:])
1631 m = _DOT_(*map1(typename, type(I), M))
1632 if _V:
1633 X = _SPACE_(_X_, _EQUAL_, m)
1634 printf(unstr(X, glA, glB, **kwds))
1636 X = M(glA, glB, **kwds)
1637 if X is None or isinstance(X, (XDict, tuple)):
1638 printf(_COLONSPACE_(m, repr(X)))
1639 else:
1640 for i, X in enumerate(X):
1641 printf(_COLONSPACE_(Fmt.INDEX(m, i), repr(X)))
1643 def _examples():
1645 from pygeodesy.internals import _usage_argv
1647 s = _SPACE_(*_usage_argv(__file__))
1648 for t in ('-h', '-h -n',
1649 '-c 0 0 45 40 10 135',
1650 '-C -c 0 0 45 40 10 135',
1651 '-T -R 2.6e7 -c 0 0 45 40 10 135',
1652 '-c 50 -4 -147.7 0 0 90',
1653 '-C -c 50 -4 -147.7 0 0 90',
1654 '# % echo 0 0 10 10 50 -4 50S 4W | IntersectTool -i -p 0 -C',
1655 '# -631414 5988887 0 -3',
1656 '# -4.05187 -4.00000 -4.05187 -4.00000 0',
1657 '-m 0 0 10 10 50 -4 50S 4W',
1658 '-C -m 0 0 10 10 50 -4 50S 4W',
1659 '-i 0 0 10 10 50 -4 50S 4W',
1660 '-T -i 0 0 10 10 50 -4 50S 4W',
1661 '-C -i 0 0 10 10 50 -4 50S 4W',
1662 '-T -C -i 0 0 10 10 50 -4 50S 4W',
1663 '-V -T -i 0 0 10 10 50 -4 -50 -4',
1664 '-C -R 4e7 -c 50 -4 -147.7 0 0 90',
1665 '-T -C -R 4e7 -c 50 -4 -147.7 0 0 90',
1666 '-R 4e7 -i 0 0 10 10 50 -4 -50 -4',
1667 '-T -R 4e7 -i 0 0 10 10 50 -4 -50 -4'):
1668 if t.startswith(_HASH_):
1669 printf(t, nl=int(t[2] == '%'))
1670 else:
1671 printf(_SPACE_(_HASH_, s, t), nl=1)
1672 argv[1:] = t = t.split()
1673 _main(t)
1675 from sys import argv, stderr
1676 try:
1677 if len(argv) == 2 and argv[1] == __help_:
1678 _examples()
1679 else:
1680 _main(argv[1:])
1682 except Exception as x:
1683 x = _SPACE_(x, NN, _HASH_, *argv)
1684 printf(x, file=stderr, nl=1)
1685 if '-V' in x or _MODS.errors.exception_chaining():
1686 raise
1687 exit(1)
1689# % env PYGEODESY_INTERSECTTOOL=... python3 -m pygeodesy.geodesici --help
1691# % python3 -m pygeodesy.geodesici -h
1692#
1693# usage: python3 -m ....pygeodesy.geodesici [--Verbose | -V] [--version | -v] [--help | -h] [--Tool | -T] [--Check | -C] [-R meter]
1694# [-c latA lonA aziA latB lonB aziB |
1695# -i latA1 lonA1 latA2 lonA2 latB1 lonB1 latB2 lonB2 |
1696# -m latA1 lonA1 latA2 lonA2 latB1 lonB1 latB2 lonB2 |
1697# -n latA lonA aziA aziB |
1698# -o latA lonA aziA latB lonB aziB x0 y0]
1700# % python3 -m ....pygeodesy.geodesici -h -n
1701#
1702# usage: python3 -m ....pygeodesy.geodesici -n latA lonA aziA aziB
1704# % python3 -m ....pygeodesy.geodesici -c 0 0 45 40 10 135
1705# Intersector.Closest: XDict(c=0, sA=3862290.547855, sB=2339969.547699, sX0=6202260.095554)
1707# % python3 -m ....pygeodesy.geodesici -C -c 0 0 45 40 10 135
1708# 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)
1710# % env PYGEODESY_INTERSECTTOOL=...python3 -m ....pygeodesy.geodesici -T -R 2.6e7 -c 0 0 45 40 10 135
1711# Intersectool.All[0]: XDict(c=0, sA=3862290.547855, sB=2339969.547699, sX0=6202260.095554)
1713# % python3 -m ....pygeodesy.geodesici -c 50 -4 -147.7 0 0 90
1714# Intersector.Closest: XDict(c=0, sA=6058048.653081, sB=-3311252.995823, sX0=9369301.648903)
1716# % python3 -m ....pygeodesy.geodesici -C -c 50 -4 -147.7 0 0 90
1717# 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)
1719# % echo 0 0 10 10 50 -4 50S 4W | IntersectTool -i -p 0 -C
1720# -631414 5988887 0 -3
1721# -4.05187 -4.00000 -4.05187 -4.00000 0
1723# % python3 -m ....pygeodesy.geodesici -m 0 0 10 10 50 -4 50S 4W
1724# Intersector.Middle: XDict(c=0, sA=782554.549609, sB=5536835.161499, sX0=0.0)
1726# % python3 -m ....pygeodesy.geodesici -C -m 0 0 10 10 50 -4 50S 4W
1727# 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)
1729# % python3 -m ....pygeodesy.geodesici -i 0 0 10 10 50 -4 50S 4W
1730# Intersector.Segment: XDict(c=0, k=-3, kA=-1, kB=0, sA=-631414.26877, sB=5988887.278435, sX0=1866020.935315)
1732# % env PYGEODESY_INTERSECTTOOL=... python3 -m ....pygeodesy.geodesici -T -i 0 0 10 10 50 -4 50S 4W
1733# Intersectool.Segment: XDict(c=0, k=-3, kA=-1, kB=0, sA=-631414.26877, sB=5988887.278435)
1735# % python3 -m ....pygeodesy.geodesici -C -i 0 0 10 10 50 -4 50S 4W
1736# 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)
1738# % env PYGEODESY_INTERSECTTOOL=... python3 -m ....pygeodesy.geodesici -T -C -i 0 0 10 10 50 -4 50S 4W
1739# 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)
1741# % env PYGEODESY_INTERSECTTOOL=... python3 -m ....pygeodesy.geodesici -V -T -i 0 0 10 10 50 -4 -50 -4
1742# Intersectool@1: /opt/local/bin/IntersectTool --version (invoke)
1743# Intersectool@1: '/opt/local/bin/IntersectTool: GeographicLib version 2.3' (0)
1744# Intersectool@1: /opt/local/bin/IntersectTool: GeographicLib version 2.3 (0)
1745# 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))
1746# 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)
1747# Intersectool@2: '-631414.2687702414 5988887.2784352796 0 -3' (0)
1748# Intersectool@2: sA=-631414.2687702414, sB=5988887.2784352796, c=0, k=-3 (0)
1749# Intersectool.Segment: XDict(c=0, k=-3, kA=-1, kB=0, sA=-631414.26877, sB=5988887.278435)
1751# % python3 -m ....pygeodesy.geodesici -C -R 4e7 -c 50 -4 -147.7 0 0 90
1752# 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)
1753# 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)
1754# 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)
1755# 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)
1757# % env PYGEODESY_INTERSECTTOOL=... python3 -m ....pygeodesy.geodesici -T -C -R 4e7 -c 50 -4 -147.7 0 0 90
1758# 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)
1759# 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)
1760# 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)
1761# 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)
1763# % python3 -m ....pygeodesy.geodesici -R 4e7 -i 0 0 10 10 50 -4 -50 -4
1764# Intersector.All[0]: XDict(c=0, sA=-631414.26877, sB=5988887.278435, sX0=1866020.935315)
1765# Intersector.All[1]: XDict(c=0, sA=19422725.117572, sB=-14062417.105648, sX0=38239422.83511)
1766# Intersector.All[2]: XDict(c=0, sA=19422725.117572, sB=25945445.811603, sX0=39048781.218067)
1767# Intersector.All[3]: XDict(c=0, sA=39476927.464575, sB=5894074.699478, sX0=39051612.452944)
1769# % env PYGEODESY_INTERSECTTOOL=... python3 -m ....pygeodesy.geodesici -T -R 4e7 -i 0 0 10 10 50 -4 -50 -4
1770# Intersectool.All[0]: XDict(c=0, sA=-631414.26877, sB=5988887.278435, sX0=1862009.05513)
1771# Intersectool.All[1]: XDict(c=0, sA=19422725.117572, sB=-14062417.105648, sX0=38243434.715295)
1772# Intersectool.All[2]: XDict(c=0, sA=19422725.117572, sB=25945445.811603, sX0=39044769.337882)
1773# Intersectool.All[3]: XDict(c=0, sA=39476927.464575, sB=5894074.699478, sX0=39047600.57276)
1776# **) MIT License
1777#
1778# Copyright (C) 2024-2025 -- mrJean1 at Gmail -- All Rights Reserved.
1779#
1780# Permission is hereby granted, free of charge, to any person obtaining a
1781# copy of this software and associated documentation files (the "Software"),
1782# to deal in the Software without restriction, including without limitation
1783# the rights to use, copy, modify, merge, publish, distribute, sublicense,
1784# and/or sell copies of the Software, and to permit persons to whom the
1785# Software is furnished to do so, subject to the following conditions:
1786#
1787# The above copyright notice and this permission notice shall be included
1788# in all copies or substantial portions of the Software.
1789#
1790# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
1791# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
1792# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
1793# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
1794# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
1795# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
1796# OTHER DEALINGS IN THE SOFTWARE.