pygeodesy 24.6.24__py2.py3-none-any.whl → 24.7.7__py2.py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
pygeodesy/geodesici.py CHANGED
@@ -1,16 +1,21 @@
1
1
 
2
2
  # -*- coding: utf-8 -*-
3
3
 
4
- u'''Class L{Intersector}, a pure Python version of parts of I{Karney}'s C++ class U{Intersect
5
- <https://GeographicLib.SourceForge.io/C++/doc/classGeographicLib_1_1Intersect.html>} to intersect
6
- geodesic lines.
4
+ u'''Classes L{Intersectool} and L{Intersector}, a pure Python version of I{Karney}'s C++ class
5
+ U{Intersect<https://GeographicLib.SourceForge.io/C++/doc/classGeographicLib_1_1Intersect.html>}
6
+ to find the intersections of two geodesic lines or line segments.
7
7
 
8
- Only C++ member functions C{All}, C{Closest} and C{All} have been transcoded into Python as methods
9
- L{Intersector.All}, L{Intersector.Closest} and L{Intersector.Next} producing 4-item L{XDist}s.
8
+ L{Intersectool} and L{Intersector} methods C{All}, C{Closest}, C{Next} and C{Segment} produce
10
9
 
11
- Adjacent methods L{Intersector.All5}, L{Intersector.Closest5}, L{Intersector.Next5} and
12
- L{Intersector.Next5s} return or yield L{Intersector5Tuple}s with the lat-, longitude, azimuth of
13
- each intersection as a C{Position} L{GDict} on each geodesic line.
10
+ L{XDict} instances with 4 or more items. Adjacent methods C{All5}, C{Closest5}, C{Next5} and
11
+ C{Segment} return or yield L{Intersectool5Tuple} or L{Intersector5Tuple}s with the lat-, longitude
12
+ azimuth of each intersection as an extended or C{Position}-like L{GDict}.
13
+
14
+ Class L{Intersectool} is a wrapper to invoke I{Karney}'s U{IntersectTool
15
+ <https://GeographicLib.SourceForge.io/C++/doc/IntersectTool.1.html>} utility like class L{Intersector},
16
+ but intended I{for testing purposes only}.
17
+
18
+ Set env variable C{PYGEODESY_INTERSECTTOOL} to the (fully qualified) path of the C{IntersectTool} executable.
14
19
 
15
20
  For more details, see the C++ U{GeographicLib<https://GeographicLib.SourceForge.io/C++/doc/index.html>}
16
21
  documentation, I{Charles F.F. Karney}'s paper U{Geodesics intersections<https://arxiv.org/abs/2308.00495>}
@@ -22,58 +27,78 @@ from __future__ import division as _; del _ # PYCHOK semicolon
22
27
 
23
28
  from pygeodesy.basics import _copy, _enumereverse, map1, \
24
29
  _xinstanceof, _xor
25
- from pygeodesy.constants import EPS, INF, INT0, PI, PI2, PI_4, _0_0, \
26
- _0_5, _1_0, _1_5, _2_0, _3_0, _90_0
27
- from pygeodesy.ellipsoids import _EWGS84, Fmt
28
- from pygeodesy.errors import GeodesicError, IntersectionError, \
29
- _xgeodesics, _xkwds_get
30
- from pygeodesy.fmath import euclid, favg, fdot
30
+ from pygeodesy.constants import EPS, INF, INT0, PI, PI2, PI_4, \
31
+ _0_0, _0_5, _1_0, _1_5, _2_0, _3_0, \
32
+ _90_0, isfinite
33
+ from pygeodesy.ellipsoids import _EWGS84, Fmt, unstr
34
+ from pygeodesy.errors import GeodesicError, IntersectionError, _an, \
35
+ _xgeodesics, _xkwds_get, _xkwds_kwds
36
+ # from pygeodesy.errors import exception_chaining # _MODS
37
+ from pygeodesy.fmath import euclid, fdot
31
38
  from pygeodesy.fsums import Fsum, fsum1_, _ceil
32
- from pygeodesy.interns import _A_, _B_, _c_, _SPACE_, _too_
33
- from pygeodesy.karney import Caps, _diff182, _sincos2de
34
- from pygeodesy.lazily import _ALL_LAZY # _ALL_MODS as _MODS
35
- from pygeodesy.named import ADict, _NamedBase, _NamedTuple
36
- from pygeodesy.namedTuples import Degrees, Int, Meter, _Pass
37
- from pygeodesy.props import Property, Property_RO, property_RO
38
- # from pygeodesy.streprs import Fmt # from .ellipsoids
39
- # from pygeodesy.units import Degrees, Int, Meter # from .namedTuples
40
- from pygeodesy.utily import sincos2, atan2, fabs
41
-
42
- # from math import atan2, ceil as _ceil, fabs # .fsums, .utily
39
+ from pygeodesy.interns import NN, _A_, _B_, _c_, _COMMASPACE_, \
40
+ _HASH_, _M_, _not_, _SPACE_, _too_
41
+ from pygeodesy.interns import _m_ # PYCHOK used!
42
+ from pygeodesy.karney import Caps, _diff182, GDict, _sincos2de
43
+ from pygeodesy.lazily import _ALL_LAZY, _ALL_MODS as _MODS, \
44
+ _getenv, _PYGEODESY_INTERSECTTOOL_
45
+ from pygeodesy.named import ADict, _NamedBase, _NamedTuple, _Pass
46
+ from pygeodesy.props import deprecated_method, Property, \
47
+ Property_RO, property_RO
48
+ from pygeodesy.solveBase import _SolveCapsBase, pairs
49
+ # from pygeodesy.streprs import pairs # from .solveBase
50
+ # from pygeodesy.streprs import Fmt, unstr # from .ellipsoids
51
+ from pygeodesy.units import Degrees, Float, Int, Lat, Lon, \
52
+ Meter, Meter_
53
+ from pygeodesy.utily import sincos2, atan2, fabs, radians
54
+
55
+ # from math import atan2, ceil as _ceil, fabs, radians # .fsums, .utily
43
56
 
44
57
  __all__ = _ALL_LAZY.geodesici
45
- __version__ = '24.06.27'
58
+ __version__ = '24.07.09'
46
59
 
47
60
  _0t = 0, # int
48
61
  _1_1t = -1, +1
49
62
  _1_0_1t = -1, 0, +1
63
+ _c__ = '-c' # PYCHOK used!
64
+ _C__ = '-C' # PYCHOK used!
50
65
  _EPS3 = EPS * _3_0
51
- _EPSr5 = pow(EPS, 0.2) # PYCHOK used!
66
+ _EPSr5 = pow(EPS, 0.2) # PYCHOK used! 7.4e-4 or ~3"
67
+ _i__ = '-i' # PYCHOK used!
68
+ _latA_ = 'latA'
69
+ _lonA_ = 'lonA'
70
+ _n__ = '-n' # PYCHOK used!
71
+ _o__ = '-o' # PYCHOK used!
72
+ _R__ = '-R'
73
+ _sAB_ = 'sAB'
74
+ _sX0_ = 'sX0'
52
75
  _TRIPS = 128
53
76
 
54
77
 
55
- def _L1(a, b):
56
- return fabs(a) + fabs(b)
78
+ class Azi(Degrees):
79
+ '''(INTERNAL) Azimuth C{Unit}.
80
+ '''
81
+ pass
57
82
 
58
83
 
59
- class XDist(ADict):
60
- '''4-Item result from L{Intersector.All}, L{Intersector.Closest} and
61
- L{Intersector.Next} with the intersection offsets C{sA}, C{sB} and
62
- C{sX0} in C{meter} and the coincidence indicator C{c}, an C{int},
63
- +1 for parallel, -1 for anti-parallel, 0 otherwise.
64
- '''
65
- _Delta = EPS # default margin, see C{Intersector._Delto}
84
+ class XDict(ADict):
85
+ '''4+Item result from L{Intersectool} and L{Intersector} methods
86
+ C{All}, C{Closest}, C{Next} and C{Segment} with the intersection
87
+ offsets C{sA}, C{sB} and C{sX0} in C{meter} and the coincidence
88
+ indicator C{c}, an C{int}, +1 for parallel, -1 for anti-parallel
89
+ or 0 otherwise.
66
90
 
67
- def __init__(self, sA=0, sB=0, c=0, sX0=INT0):
68
- '''New L{XDist}.
91
+ Offsets C{sA} and C{sB} are distances measured I{along} geodesic
92
+ line C{glA} respectively C{glB}, but C{sX0} is the I{L1-distance}
93
+ between the intersection and the I{origin} C{X0}.
69
94
 
70
- @kwarg sA: Offset on geodesic line A (C{meter}).
71
- @kwarg sB: Offset on geodesic line B (C{meter}).
72
- @kwarg c: Coincidence indicator (C{int}, +1 for parallel
73
- -1 for anti-parallel, 0 otherwise.
74
- @kwarg sX0: Offset to C{X0} ({Cmeter}) or L{INT0}.
75
- '''
76
- self.set_(sA=sA, sB=sB, c=c, sX0=sX0)
95
+ Segment indicators C{kA} and C{kB} are C{0} if the segments
96
+ intersect or C{-1} or C{+1} if the intersection is I{before}
97
+ the start, respectively I{after} the end of the segment, like
98
+ L{Intersection3Tuple<Intersection3Tuple>}. Segment indicator
99
+ C{k} is I{Karney}'s C{segmode}, equal C{kA * 3 + kB}.
100
+ '''
101
+ _Delta = EPS # default margin, see C{Intersector._Delto}
77
102
 
78
103
  def __add__(self, other):
79
104
  X = _copy(self)
@@ -87,7 +112,7 @@ class XDist(ADict):
87
112
  if isinstance(other, tuple): # and len(other) == 2:
88
113
  a, b = other
89
114
  else:
90
- # _xinstanceof(XDist, other=other)
115
+ # _xinstanceof(XDict, other=other)
91
116
  a = other.sA
92
117
  b = other.sB
93
118
  if other.c:
@@ -97,33 +122,79 @@ class XDist(ADict):
97
122
  return self
98
123
 
99
124
  def __le__(self, other):
100
- # _xinstanceof(XDist, other=other)
125
+ # _xinstanceof(XDict, other=other)
101
126
  return self == other or self < other
102
127
 
103
128
  def __lt__(self, other):
104
- # _xinstanceof(XDist, other=other)
129
+ # _xinstanceof(XDict, other=other)
105
130
  return (self.sA < other.sA or (self.sA == other.sA and # PYCHOK sA
106
131
  self.sB < other.sB) and self != other) # PYCHOK sB
107
132
 
108
133
  def __ne__(self, other):
109
- # _xinstanceof(XDist, other=other)
134
+ # _xinstanceof(XDict, other=other)
110
135
  return self is not other and self.L1(other) > self._Delta
111
136
 
112
- def _fixCoincident(self, X, *c0):
137
+ def _corners(self, sA, sB, T2):
138
+ # yield all corners further than C{T2}
139
+ a, b = self.sA, self.sB # PYCHOK sA, sB
140
+ for x in (0, sA):
141
+ for y in (0, sB):
142
+ if _L1(x - a, y - b) >= T2:
143
+ yield XDict_(x, y)
144
+
145
+ def _fixCoincident(self, X, c0=0):
113
146
  # return the mid-point if C{X} is anti-/parallel
114
- c = c0[0] if c0 else X.c
147
+ c = c0 or X.c
115
148
  if c:
116
149
  s = (self.sA - X.sA + # PYCHOK sA
117
150
  (self.sB - X.sB) * c) * _0_5 # PYCHOK sB
118
151
  X = X + (s, s * c) # NOT +=
119
152
  return X
120
153
 
154
+ def _fixSegment(self, sA, sB): # PYCHOK no cover
155
+ # modify this anti-/parallel C{XDict}
156
+ a, b, c = self.sA, self.sB, self.c # PYCHOK sA, sB, c
157
+
158
+ def _g(): # intersection in smallest gap
159
+ if c > 0: # distance to [A, B] is |(a - b) - (A - B)|
160
+ t = a - b # consider corners [0, sB] and [sA, 0]
161
+ t = fabs(t + sB) < fabs(t - sA)
162
+ s = a + b
163
+ else: # distance to [A, B] is |(a + b) - (A + B)|
164
+ t = a + b # consider corner [0, 0] and [sA, sB]
165
+ t = fabs(t) < fabs(t - (sA + sB))
166
+ s = sB + (a - b)
167
+ return (sB if t else sA) - s
168
+
169
+ ta = -a
170
+ tb = sA - a
171
+ tc = -c * b
172
+ td = -c * (b - sB)
173
+
174
+ ga = 0 <= (b + c * ta) <= sB
175
+ gb = 0 <= (b + c * tb) <= sB
176
+ gc = 0 <= (a + tc) <= sA
177
+ gd = 0 <= (a + td) <= sA
178
+
179
+ # test opposite rectangle sides first
180
+ s = ((ta + tb) if ga and gb else (
181
+ (tc + td) if gc and gd else (
182
+ (ta + tc) if ga and gc else (
183
+ (ta + td) if ga and gd else (
184
+ (tb + tc) if gb and gc else (
185
+ (tb + td) if gb and gd else _g())))))) * _0_5
186
+ self += s, s * c
187
+
188
+ @property_RO
189
+ def _is00(self):
190
+ return not (self.sA or self.sB) # PYCHOK sA, sB
191
+
121
192
  def L1(self, other=None):
122
193
  '''Return the C{L1} distance.
123
194
  '''
124
195
  a, b = self.sA, self.sB # PYCHOK sA, sB
125
196
  if other is not None:
126
- # _xinstanceof(XDist, other=other)
197
+ # _xinstanceof(XDict, other=other)
127
198
  a -= other.sA
128
199
  b -= other.sB
129
200
  return _L1(a, b)
@@ -143,25 +214,473 @@ class XDist(ADict):
143
214
  (-1, 1, -1, 1, 0, 2, 0, -2)):
144
215
  yield self + (D_[a], D_[b])
145
216
 
146
- def _nmD3(self, n, m, D3):
217
+ def _nmD3(self, n, m, D3): # d3 / 2
147
218
  # yield the C{All} starts
219
+ yield self
148
220
  for i in range(n, m, 2):
149
221
  for j in range(n, m, 2):
150
- if i or j:
222
+ if i or j: # skip self
151
223
  yield self + ((i + j) * D3,
152
224
  (i - j) * D3)
153
225
 
226
+ def _outSide(self, sA, sB):
227
+ # is this C{Xdist} outside one or both segments?
228
+ a, b = self.sA, self.sB # PYCHOK sA, sB
229
+ kA = -1 if a < 0 else (+1 if a > sA else INT0)
230
+ kB = -1 if b < 0 else (+1 if b > sB else INT0)
231
+ self.set_(kA=kA, kB=kB, k=(kA * 3 + kB) or INT0)
232
+ return bool(kA or kB)
233
+
154
234
  def _skip(self, S_, T1_Delta):
155
- # remove starts from C{S_} near this C{XDist}
235
+ # remove starts from list C{S_} near this C{XDict}
156
236
  for j, S in _enumereverse(S_):
157
237
  if S.L1(self) < T1_Delta:
158
238
  S_.pop(j)
159
239
 
160
- _X000 = XDist() # PYCHOK origin
161
- _XINF = XDist(INF)
162
240
 
241
+ def XDict_(sA=_0_0, sB=_0_0, c=INT0, sX0=_0_0):
242
+ '''(INTERNAL) New L{XDict} from positionals.
243
+ '''
244
+ return XDict(sA=sA, sB=sB, c=c, sX0=sX0)
245
+
246
+ _X000 = XDict_() # PYCHOK origin
247
+ _XINF = XDict_(INF)
248
+
249
+
250
+ class _IntersectBase(_NamedBase):
251
+ '''(INTERNAL) Base class for L{Intersectool} and L{Intersector}.
252
+ '''
253
+ # _g = None
254
+
255
+ def __init__(self, geodesic, **name):
256
+ _xinstanceof(*_EWGS84._Geodesics, geodesic=geodesic)
257
+ self._g = geodesic
258
+ if name:
259
+ self.name = name
260
+
261
+ @Property_RO
262
+ def a(self):
263
+ '''Get the I{equatorial} radius, semi-axis (C{meter}).
264
+ '''
265
+ return self.ellipsoid.a
266
+
267
+ equatoradius = a # = Requatorial
268
+
269
+ @Property_RO
270
+ def _cHalf(self): # normalizer, semi-circumference
271
+ return self.R * PI # ~20K Km WGS84
272
+
273
+ @Property_RO
274
+ def _cMax(self): # outer circumference
275
+ return max(self.a, self.ellipsoid.b, self.R) * PI2
276
+
277
+ @property_RO
278
+ def datum(self):
279
+ '''Get the geodesic's datum (C{Datum}).
280
+ '''
281
+ return self.geodesic.datum
282
+
283
+ @Property_RO
284
+ def ellipsoid(self):
285
+ '''Get the C{geodesic}'s ellipsoid (C{Ellipsoid}).
286
+ '''
287
+ return self.geodesic.datum.ellipsoid
288
+
289
+ @Property_RO
290
+ def f(self):
291
+ '''Get the I{flattening} (C{scalar}), C{0} for spherical, negative for prolate.
292
+ '''
293
+ return self.ellipsoid.f
294
+
295
+ flattening = f
296
+
297
+ @property_RO
298
+ def geodesic(self):
299
+ '''Get the C{geodesic} (C{Geodesic...}).
300
+ '''
301
+ return self._g
302
+
303
+ def _Inversa12(self, A, B=None):
304
+ lls = (0, 0, A, 0) if B is None else (A.lat2, A.lon2,
305
+ B.lat2, B.lon2)
306
+ r = self._g.Inverse(*lls, outmask=Caps.DISTANCE)
307
+ return r.s12, r.a12 # .a12 always in r
308
+
309
+ def _ll3z4ll(self, lat1, lon1, azi1_lat2, *lon2):
310
+ t = Lat(lat1=lat1), Lon(lon1=lon1)
311
+ if lon2: # get azis for All, keep lat-/lons
312
+ t += Lat(lat2=azi1_lat2), Lon(lon2=lon2[0])
313
+ else:
314
+ t += Azi(azi1=azi1_lat2),
315
+ return t
316
+
317
+ @deprecated_method
318
+ def Next5s(self, glA, glB, X0=_X000, aMax=1801, sMax=0, **unused):
319
+ '''DEPRECATED on 2024.07.02, use method L{All5}.'''
320
+ return self.All5(glA, glB, X0=X0, aMaX0=aMax, sMaX0=sMax) # PYCHOK attr
321
+
322
+ @Property_RO
323
+ def R(self):
324
+ '''Get the I{authalic} earth radius (C{meter}).
325
+ '''
326
+ return self.ellipsoid.R2
327
+
328
+ def _sMaX0_C2(self, aMaX0, **sMaX0_C):
329
+ _g = _xkwds_get
330
+ s = _g(sMaX0_C, sMaX0=self._cMax)
331
+ s = _g(sMaX0_C, sMax=s) # for backward ...
332
+ a = _g(sMaX0_C, aMax=aMaX0) # ... compatibility
333
+ if a: # degrees to meter, approx.
334
+ s = max(s, self.R * radians(a)) # ellipsoid.degrees2m(a)
335
+ s = _g(sMaX0_C, _R=s)
336
+ if s < _EPS3:
337
+ s = _EPS3 # raise GeodesicError(sMaX0=s)
338
+ return s, _xkwds_kwds(sMaX0_C, _C=False)
339
+
340
+ def _xNext(self, glA, glB, eps1, **eps_C): # PYCHOK no cover
341
+ eps1 = _xkwds_get(eps_C, eps=eps1) # eps for backward compatibility
342
+ if eps1 is not None:
343
+ a = glA.lat1 - glB.lat1
344
+ b = glA.lon1 - glB.lon1
345
+ if euclid(a, b) > eps1:
346
+ raise GeodesicError(lat=a, lon=b, eps1=eps1)
347
+ return _xkwds_kwds(eps_C, _C=False)
348
+
349
+
350
+ class Intersectool(_IntersectBase, _SolveCapsBase):
351
+ '''Wrapper to invoke I{Karney}'s utility U{IntersectTool
352
+ <https://GeographicLib.SourceForge.io/C++/doc/IntersectTool.1.html>}
353
+ similar to class L{Intersector<geodesici.Intersector>}.
354
+
355
+ @note: Use property C{IntersectTool} or env variable C{PYGEODESY_INTERSECTTOOL}
356
+ to specify the (fully qualified) path to the C{IntersectTool} executable.
357
+
358
+ @note: This C{Intersectool} is intended I{for testing purposes only}, it invokes
359
+ the C{IntersectTool} executable for I{every} method call.
360
+ '''
361
+ _Error = GeodesicError
362
+ _linelimit = 1200 # line printer width X 10
363
+ _Names_ABs = _latA_, _lonA_, 'latB', 'lonB', _sAB_ # -C to stderr
364
+ _Names_XDict = 'sA', 'sB', _c_ # plus 'k' from -i or 'sX0' from -R
365
+ _Xable_name = 'IntersectTool'
366
+ _Xable_path = _getenv(_PYGEODESY_INTERSECTTOOL_, _PYGEODESY_INTERSECTTOOL_)
367
+
368
+ def __init__(self, a_geodesic=None, f=None, **name):
369
+ '''New L{IntersectTool}.
370
+
371
+ @arg a_geodesic: Earth' equatorial axis (C{meter}) or a geodesic
372
+ (L{GeodesicExact<pygeodesy.geodesicx.GeodesicExact>},
373
+ wrapped L{Geodesic<pygeodesy.geodesicw.Geodesic>} or
374
+ L{GeodesicSolve<pygeodesy.geodsolve.GeodesicSolve>}).
375
+ @kwarg f: Earth' flattening (C{scalar}), required if B{C{a_geodesic}}
376
+ is in C{meter}, ignored otherwise.
377
+ @kwarg name: Optional C{B{name}=NN} (C{str}).
378
+
379
+ @raise GeodesicError: The eccentricity of the B{C{geodesic}}'s ellipsoid is too
380
+ large or no initial convergence.
381
+
382
+ @see: The B{Note} at I{Karney}'s C++ U{Intersect<https://GeographicLib.sourceforge.io/
383
+ C++/doc/classGeographicLib_1_1Intersect.html#ae41f54c9a44836f6c8f140f6994930cf>}.
384
+ '''
385
+ g = self._GeodesicExact() if a_geodesic is None else (a_geodesic if f is None else
386
+ self._GeodesicExact(a_geodesic, f))
387
+ _IntersectBase.__init__(self, g, **name)
388
+
389
+ def All(self, glA, glB, X0=_X000, eps1=_0_0, aMaX0=0, **sMaX0_C):
390
+ '''Yield all intersection of two geodesic lines up to a limit.
391
+
392
+ @kwarg eps1: Optional margin for the L{euclid<pygeodesy.euclid>}ean distance
393
+ (C{degrees}) between the C{(lat1, lon1)} points of both lines for
394
+ using the L{IntersectTool<Intersectool.IntersectTool>}'s C{"-n"}
395
+ option, unless C{B{eps1}=None}.
396
+
397
+ @return: An L{XDict} for each intersection.
398
+ '''
399
+ def _xz2(**gl):
400
+ try:
401
+ n, gl = gl.popitem() # _xkwds_item2(gl)
402
+ try:
403
+ return self._c_alt, (gl.azi1,)
404
+ except (AttributeError, KeyError):
405
+ return self._i_alt, (gl.lat2, gl.lon2)
406
+ except Exception as x:
407
+ raise GeodesicError(n, gl, cause=x)
408
+
409
+ _t, a = _xz2(glA=glA)
410
+ _x, b = _xz2(glB=glB)
411
+ if _x is not _t:
412
+ raise GeodesicError(glA=glA, glB=glB)
413
+
414
+ A = glA.lat1, glA.lon1
415
+ B = glB.lat1, glB.lon1
416
+ if _x is self._c_alt:
417
+ if X0 is _X000 or X0._is00:
418
+ if eps1 is not None and \
419
+ euclid(glA.lat1 - glB.lat1,
420
+ glA.lon1 - glB.lon1) <= eps1:
421
+ _x, B = self._n_alt, ()
422
+ else: # non-zero offset
423
+ _x = self._o_alt
424
+ b += X0.sA, X0.sB
425
+
426
+ sMaX0, _C = self._sMaX0_C2(aMaX0, **sMaX0_C)
427
+ for X in self._XDictInvoke(_x, _sX0_, (A + a + B + b),
428
+ _R=sMaX0, **_C):
429
+ yield X.set_(c=int(X.c))
430
+
431
+ def All5(self, glA, glB, X0=_X000, **aMaX0_sMaX0):
432
+ '''Yield all intersection of two geodesic lines up to a limit.
433
+
434
+ @return: An L{Intersectool5Tuple} for each intersection.
435
+ '''
436
+ for X in self.All(glA, glB, X0=X0, _C=True, **aMaX0_sMaX0):
437
+ yield self._In5T(glA, glB, X, X)
438
+
439
+ @Property_RO
440
+ def _C_option(self):
441
+ return (_C__,)
442
+
443
+ @Property_RO
444
+ def _cmdBasic(self):
445
+ '''(INTERNAL) Get the basic C{IntersectTool} cmd (C{tuple}).
446
+ '''
447
+ return (self.IntersectTool,) + (self._e_option +
448
+ self._E_option +
449
+ self._p_option)
450
+
451
+ @Property_RO
452
+ def _c_alt(self):
453
+ '''(INTERNAL) Get the C{Closest} format (C{tuple}).
454
+ '''
455
+ return _c__, # latA lonA aziA latB lonB aziB
456
+
457
+ def Closest(self, glA, glB, X0=_X000, _C=False):
458
+ '''Find the closest intersection of two geodesic lines.
459
+
460
+ @kwarg _C: Use C{B{_C}=True} to include the C{"-C"} results (C{bool}).
461
+
462
+ @return: An L{XDict}.
463
+ '''
464
+ args = glA.lat1, glA.lon1, glA.azi1, \
465
+ glB.lat1, glB.lon1, glB.azi1
466
+ if X0 is _X000 or X0._is000:
467
+ _x = self._c_alt
468
+ else:
469
+ _x = self._o_alt
470
+ args += X0.sA, X0.sB
471
+ return self._XDictInvoke(_x, NN, args, _C=_C) # _R=None)
472
+
473
+ def Closest5(self, glA, glB, **unused):
474
+ '''Find the closest intersection of two geodesic lines.
475
+
476
+ @return: An L{Intersectool5Tuple}.
477
+ '''
478
+ X = self.Closest(glA, glB, _C=True)
479
+ return self._In5T(glA, glB, X, X)
480
+
481
+ @property_RO
482
+ def _GeodesicExact(self):
483
+ '''Get the I{class} L{GeodesicExact}, I{once}.
484
+ '''
485
+ Intersectool._GeodesicExact = G = _MODS.geodesicx.GeodesicExact # overwrite property_RO
486
+ return G
487
+
488
+ @Property_RO
489
+ def _i_alt(self):
490
+ '''(INTERNAL) Get the C{Segment} format (C{tuple}).
491
+ '''
492
+ return _i__, # latA1 lonA1 latA2 lonA2 latB1 lonB1 latB2 lonB2
493
+
494
+ def _In5T(self, glA, glB, S, X, k2=False, **_2X):
495
+ A = GDict(glA).set_(lat2=X.latA, lon2=X.lonA, s12=S.sA)
496
+ B = GDict(glB).set_(lat2=X.latB, lon2=X.lonB, s12=S.sB)
497
+ if k2:
498
+ A.set_(k2=X.kA)
499
+ B.set_(k2=X.kB)
500
+ s, a = self._Inversa12(A, B)
501
+ sAB = _xkwds_get(X, sAB=s)
502
+ if a and s and s != sAB:
503
+ a *= sAB / s # adjust a
504
+ return Intersectool5Tuple(A._2X(glA, **_2X),
505
+ B._2X(glB, **_2X), sAB, a, X.c)
506
+
507
+ @Property
508
+ def IntersectTool(self):
509
+ '''Get the U{IntersectTool<https://GeographicLib.SourceForge.io/C++/doc/IntersectTool.1.html>}
510
+ executable (C{filename}).
511
+ '''
512
+ return self._Xable_path
513
+
514
+ @IntersectTool.setter # PYCHOK setter!
515
+ def IntersectTool(self, path):
516
+ '''Set the U{IntersectTool<https://GeographicLib.SourceForge.io/C++/doc/IntersectTool.1.html>}
517
+ executable (C{filename}), the (fully qualified) path to the C{IntersectTool} executable.
518
+
519
+ @raise GeodesicError: Invalid B{C{path}}, B{C{path}} doesn't exist or isn't the
520
+ C{IntersectTool} executable.
521
+ '''
522
+ self._setXable(path)
523
+
524
+ def Line(self, lat1, lon1, azi1_lat2, *lon2, **name):
525
+ '''Return a geodesic line from this C{Intersector}'s geodesic, specified by
526
+ two (goedetic) points or a (goedetic) point and an (initial) azimuth.
527
+
528
+ @return: A 3- or 6-item, named L{GDict}.
529
+ '''
530
+ args = self._ll3z4ll(lat1, lon1, azi1_lat2, *lon2)
531
+ gl = GDict((u.name, u) for u in args)
532
+ # if lon2: # get azis for All, use lat-/lons as given
533
+ # r = self._g.Inverse(outmask=Caps.AZIMUTH, *args)
534
+ # gl.set_(azi1=Azi(azi1=r.azi1), azi2=Azi(azi2=r.azi2))
535
+ if name:
536
+ gl.name= name
537
+ return gl
538
+
539
+ def Middle(self, glA, glB, **_C):
540
+ '''Get the mid-points on two geodesic line segments.
541
+
542
+ @kwarg _C: Use C{B{_C}=True} to include the C{"-C"} results (C{bool}).
543
+
544
+ @return: An L{XDict}.
545
+ '''
546
+ X, _, _, _, _ = self._middle5(glA, glB, **_C)
547
+ return X
548
+
549
+ def _middle5(self, glA, glB, _C=False, **unused):
550
+ # return intersections C{A} and C{B} and the
551
+ # center C{X0} of rectangle [sA, sB]
552
+
553
+ def _smi4(**gl):
554
+ try:
555
+ n, gl = gl.popitem()
556
+ il = self._g.InverseLine(gl.lat1, gl.lon1, gl.lat2, gl.lon2)
557
+ except Exception as x:
558
+ raise GeodesicError(n, gl, cause=x)
559
+ s = il.s13
560
+ m = s * _0_5
561
+ return s, m, il, (il.Position(m, outmask=Caps._STD_LINE) if _C else None)
562
+
563
+ sA, mA, iA, A = _smi4(glA=glA)
564
+ sB, mB, iB, B = _smi4(glB=glB)
565
+ X = XDict_(mA, mB) # center
566
+ _ = X._outSide(sA, sB)
567
+ if _C: # _Names_ABs
568
+ s, a = self._Inversa12(A, B)
569
+ X.set_(latA=A.lat2, lonA=A.lon2, aMM=a, # assert sA == A.s12
570
+ latB=B.lat2, lonB=B.lon2, sMM=s) # assert sB == B.s12
571
+ return X, A, iA, B, iB
572
+
573
+ def Middle5(self, glA, glB, **unused):
574
+ '''Get the mid-points on two geodesic line segments and their distance.
575
+
576
+ @return: A L{Middle5Tuple}.
577
+ '''
578
+ X, A, iA, B, iB = self._middle5(glA, glB, _C=True)
579
+ A, B, s, a, c = self._In5T(A, B, X, X, _2X=_M_)
580
+ return Middle5Tuple(_llz2G(A, glA, iA), _llz2G(B, glB, iB), s, a, c)
581
+
582
+ @Property_RO
583
+ def _n_alt(self):
584
+ '''(INTERNAL) Get the C{Next} format (C{tuple}).
585
+ '''
586
+ return _n__, # latA lonA aziA aziB
587
+
588
+ def Next(self, glA, glB, eps1=None, **_C): # PYCHOK no cover
589
+ '''Find the next intersection of two I{intersecting} geodesic lines.
590
+
591
+ @kwarg _C: Use C{B{_C}=True} to include the option C{"-C"} results (C{bool}).
592
+
593
+ @return: An L{XDict}.
594
+ '''
595
+ if eps1 or _C:
596
+ _C = self._xNext(glA, glB, eps1, **_C)
597
+ return self._XDictInvoke(self._n_alt, NN,
598
+ (glA.lat1, glA.lon1, glA.azi1, glB.azi1),
599
+ **_C) # _R=None
600
+
601
+ def Next5(self, glA, glB, **eps1): # PYCHOK no cover
602
+ '''Find the next intersection of two I{intersecting} geodesic lines.
603
+
604
+ @return: An L{Intersectool5Tuple}.
605
+ '''
606
+ X = self.Next(glA, glB, _C=True, **eps1)
607
+ return self._In5T(glA, glB, X, X)
608
+
609
+ @Property_RO
610
+ def _o_alt(self):
611
+ '''(INTERNAL) Get the C{Offset} format (C{tuple}).
612
+ '''
613
+ return _o__, # latA lonA aziA latB lonB aziB x0 y0
614
+
615
+ def _R_option(self, _R=None):
616
+ '''(INTERNAL) Get the C{-R maxdist} option.
617
+ '''
618
+ return () if _R is None else (_R__, str(_R)) # -R maxdist
619
+
620
+ def Segment(self, glA, glB, **_C_unused):
621
+ '''Find the intersection between two geodesic line segments.
163
622
 
164
- class Intersector(_NamedBase):
623
+ @kwarg _C: Use C{B{_C}=True} to include the option C{"-C"} results (C{bool}).
624
+
625
+ @return: An L{XDict}.
626
+ '''
627
+ X = self._XDictInvoke(self._i_alt, 'k',
628
+ (glA.lat1, glA.lon1, glA.lat2, glA.lon2,
629
+ glB.lat1, glB.lon1, glB.lat2, glB.lon2),
630
+ _C=_xkwds_get(_C_unused, _C=False)) # _R=None
631
+ k = int(X.k)
632
+ for kA in range(-1, 2):
633
+ for kB in range(-1, 2):
634
+ if (kA * 3 + kB) == k:
635
+ return X.set_(k=k, kA=kA, kB=kB)
636
+ raise GeodesicError(k=k, glA=glA, glB=glB, txt=repr(X))
637
+
638
+ def Segment5(self, glA, glB, **unused):
639
+ '''Find the next intersection of two I{intersecting} geodesic lines.
640
+
641
+ @return: An L{Intersectool5Tuple}.
642
+ '''
643
+ X = self.Segment(glA, glB, _C=True)
644
+ return self._In5T(glA, glB, X, X, k2=True)
645
+
646
+ def toStr(self, prec=6, sep=_COMMASPACE_, **unused): # PYCHOK signature
647
+ '''Return this C{Intersectool} as string.
648
+
649
+ @kwarg prec_sep: Keyword argumens C{B{prec}=6} and C{B{sep}=", "}
650
+ for the C{float} C{prec}ision, number of decimal digits
651
+ (0..9) and the C{sep}arator string to join. Trailing
652
+ zero decimals are stripped for B{C{prec}} values of 1
653
+ and above, but kept for negative B{C{prec}} values.
654
+
655
+ @return: Intersectool items (C{str}).
656
+ '''
657
+ d = dict(geodesic=self.geodesic, invokation=self.invokation,
658
+ status=self.status,
659
+ IntersectTool=self.IntersectTool)
660
+ return sep.join(pairs(d, prec=prec))
661
+
662
+ def _XDictInvoke(self, alt, _k_sX0, args, _C=False, **_R):
663
+ '''(INTERNAL) Invoke C{IntersectTool}, return results as C{XDict} or
664
+ a C{generator} if keyword argument C{B{_R}=sMaX0} is specified.
665
+ '''
666
+ # assert len(args) == {self._c_alt: 6,
667
+ # self._i_alt: 8,
668
+ # self._n_alt: 4,
669
+ # self._o_alt: 8}.get(alt, len(args))
670
+ cmd = self._cmdBasic
671
+ Names = self._Names_XDict # has _c_ always
672
+ if _k_sX0:
673
+ Names += _k_sX0,
674
+ if _C:
675
+ cmd += self._C_option
676
+ Names += self._Names_ABs
677
+ if _R:
678
+ cmd += self._R_option(**_R)
679
+ X, _R = self._DictInvoke2(cmd + alt, Names, XDict, args, **_R)
680
+ return X if _R else X.set_(c=int(X.c)) # generator or XDict
681
+
682
+
683
+ class Intersector(_IntersectBase):
165
684
  '''Finder of intersections between two goedesic lines, each an instance
166
685
  of L{GeodesicLineExact<pygeodesy.geodesicx.GeodesicLineExact>},
167
686
  wrapped L{GeodesicLine<pygeodesy.geodesicw.GeodesicLine>} or
@@ -172,7 +691,6 @@ class Intersector(_NamedBase):
172
691
  '''
173
692
  # _D1 = 0
174
693
  # _D2 = 0
175
- # _g = None
176
694
  # _T1 = 0
177
695
  # _T5 = 0
178
696
 
@@ -190,53 +708,46 @@ class Intersector(_NamedBase):
190
708
  @see: The B{Note} at I{Karney}'s C++ U{Intersect<https://GeographicLib.sourceforge.io/
191
709
  C++/doc/classGeographicLib_1_1Intersect.html#ae41f54c9a44836f6c8f140f6994930cf>}.
192
710
  '''
193
- _xinstanceof(*_EWGS84._Geodesics, geodesic=geodesic)
194
- self._g = geodesic
195
- if name:
196
- self.name = name
197
- E = self.ellipsoid
198
-
711
+ _IntersectBase.__init__(self, geodesic, **name)
712
+ E = self.ellipsoid
199
713
  t1 = E.b * PI # min distance between intersects
200
- t2 = self._polarDist2(_90_0)[0] * _2_0 # furthest closest intersect
201
- t5 = self._Inversa12(_90_0)[0] * _2_0 # longest shortest geodesic
714
+ t2 = self._polarDist2(_90_0)[0] * _2_0 # furthest, closest intersect
715
+ t5 = self._Inversa12( _90_0)[0] * _2_0 # longest, shortest geodesic
202
716
  if self.f > 0:
203
717
  t3 = self._obliqDist4()[0]
204
718
  t4 = t1
205
719
  else: # PYCHOK no cover
206
720
  t1, t2, t3 = t2, t1, t5
207
- t4 = self._polarB3()[0]
208
- d1 = t2 * _0_5
209
- d2 = t3 / _1_5
210
- d3 = t4 - self.Delta
211
- t2 = t1 * _2_0
721
+ t4, _, _ = self._polarB3()
722
+
723
+ self._D1 = d1 = t2 * _0_5 # ~E.L tile spacing for Closest
724
+ self._D2 = d2 = t3 / _1_5 # tile spacing for Next
725
+ self._D3 = d3 = t4 - self.Delta # tile spacing for All
726
+ self._T1 = t1 # min distance between intersects
727
+ self._T2 = t2 = t1 * _2_0
728
+ # self._T5 = t5 # not used
212
729
  if not (d1 < d3 and d2 < d3 and d2 < t2):
213
730
  t = Fmt.PARENSPACED(_too_('eccentric'), E.e)
214
731
  raise GeodesicError(ellipsoid=E.toStr(terse=2), txt=t)
215
- self._D1 = d1 # tile spacing for Closest
216
- self._D2 = d2 # tile spacing for Next
217
- self._D3 = d3 # tile spacing for All
218
- self._T1 = t1 # min distance between intersects
219
- self._T2 = t2
220
- # self._T5 = t5
221
732
 
222
- @Property_RO
223
- def a(self):
224
- '''Get the I{equatorial} radius, semi-axis (C{meter}).
225
- '''
226
- return self.ellipsoid.a
227
-
228
- equatoradius = a # = Requatorial
229
-
230
- def All(self, glA, glB, X0=_X000, **sMax):
231
- '''Find all intersection of two geodesic lines up to a limit.
733
+ def All(self, glA, glB, X0=None, aMaX0=0, **sMaX0_C): # MCCABE 13
734
+ '''Yield all intersection of two geodesic lines up to a limit.
232
735
 
233
736
  @arg glA: A geodesic line (L{Line<Intersector.Line>}).
234
737
  @arg glB: An other geodesic line (L{Line<Intersector.Line>}).
235
- @kwarg X0: Optional offsets along the geodesic lines (L{XDist}).
236
- @kwarg sMax: Optional, upper limit C{B{sMax}=2*PI*R} for the
237
- distance (C{meter}).
238
-
239
- @return: Yield an L{XDist} for each intersection found.
738
+ @kwarg X0: Optional I{origin} for I{L1-distances} (L{XDict}) or
739
+ C{None} for the L{Middle<Intersector.Middle>} of both
740
+ lines if both are a 4-C{args} L{Line<Intersector.Line>}
741
+ or C{InverseLine}, otherwise C{XDiff_(0, 0)}.
742
+ @kwarg aMaX0: Upper limit for the I{angular L1-distance}
743
+ (C{degrees}) or C{None} or C{0} for unlimited.
744
+ @kwarg sMaX0_C: Optional, upper limit C{B{sMaX0}=2*PI*R} for the
745
+ I{L1-distance} to B{C{X0}} (C{meter}) and option
746
+ C{B{_C}=False} to include the intersection lat-/
747
+ longitudes C{latA}, C{lonA}, C{latB}, C{lonB} and
748
+ distances C{sAB} and C{aSB}.
749
+
750
+ @return: Yield an L{XDict} for each intersection found.
240
751
 
241
752
  @raise GeodesicError: Geodesic line B{C{glA}} or B{C{glB}}
242
753
  invalid, incompatible or ill-configured.
@@ -244,35 +755,37 @@ class Intersector(_NamedBase):
244
755
  @raise IntersectionError: No convergence.
245
756
  '''
246
757
  self._xLines(glA, glB)
247
- sMax = _xkwds_get(sMax, sMax=self.R * PI2)
248
- if sMax < _EPS3:
249
- sMax = _EPS3 # raise GeodesicError(sMax=sMax)
250
-
251
- D, _D = self.Delta, self._C_2
252
- xMax = sMax + D
253
- m = int(_ceil(xMax / self._D3)) # m x m tiles
254
- d3 = xMax / m
758
+ if X0 is None:
759
+ try: # determine X0
760
+ X0, _, _ = self._middle3(glA, glB, True)
761
+ except GeodesicError: # no .Distance
762
+ X0 = _X000
763
+ sMaX0, _C = self._sMaX0_C2(aMaX0, **sMaX0_C)
764
+
765
+ D, _D = self.Delta, self._cHalf # C++ _d
766
+ xMaX0 = sMaX0 + D
767
+ m = int(_ceil(xMaX0 / self._D3)) # m x m tiles
768
+ d3 = xMaX0 / m
255
769
  T2d3D = self._T2d3Delta(d3)
256
- _X0fx = X0._fixCoincident
257
770
 
258
- c0 = 0
259
771
  C_ = _List(D) # closest coincident
260
772
  X_ = _List(D) # intersections found
773
+ c0 = 0
261
774
  S_ = list(X0._nmD3(1 - m, m, d3 * _0_5))
262
- # assert len(s_) + 1 == m * m + (m - 1) % 2
775
+ # assert len(S_) == m * m + (m - 1) % 2
263
776
  while S_:
264
777
  Q, i = self._Basic2(glA, glB, S_.pop(0))
265
778
  if Q in X_:
266
779
  continue
267
- # assert Q.c == c0 or not c0
268
- a, c0 = len(X_), Q.c
269
- if c0: # coincident intersection
270
- Q = _X0fx(Q)
271
- if Q in C_:
780
+ if Q.c: # coincident intersection # PYCHOK no cover
781
+ _X0fx = X0._fixCoincident
782
+ Q = _X0fx(Q) # Q = Q'
783
+ if c0 and Q in C_:
272
784
  continue
273
785
  C_.addend(Q)
274
786
  # elimate all existing intersections
275
787
  # on this line (which didn't set c0)
788
+ c0 = Q.c
276
789
  for j, X in _enumereverse(X_):
277
790
  if _X0fx(X, c0).L1(Q) <= D: # X' == Q
278
791
  X_.pop(j)
@@ -284,38 +797,33 @@ class Intersector(_NamedBase):
284
797
  s += s0
285
798
  sa = 0
286
799
  while True:
800
+ i += 1
287
801
  sa = _cjD(glA, s + sa, *args) - s0
288
802
  X = Q + (sa, sa * c0)
289
- i += 1
290
- if X_.addend(X, X0.L1(X), i) > xMax:
803
+ if X_.addend(X, X0.L1(X), i) > xMaX0:
291
804
  break
292
805
 
806
+ elif c0 and Q in C_: # Q.c == 0
807
+ continue
808
+ else:
809
+ a = len(X_)
810
+
293
811
  X_.addend(Q, X0.L1(Q), i + 1)
294
812
  for X in X_[a:]: # addended Xs
295
813
  X._skip(S_, T2d3D)
296
814
 
297
- return X_.sortrim(X0, sMax) # generator!
298
-
299
- def All5(self, glA, glB, X0=_X000, aMax=0, **sMax):
300
- '''Find all intersection of two geodesic lines up to a limit.
815
+ return X_.sorter(sMaX0, self._C, glA, glB, **_C) # generator
301
816
 
302
- @kwarg aMax: Upper limit for the angular distance (C{degrees})
303
- or C{None} or C{0} for unlimited.
817
+ def All5(self, glA, glB, X0=_X000, **aMaX0_sMaX0_C):
818
+ '''Yield all intersection of two geodesic lines up to a limit.
304
819
 
305
820
  @return: Yield an L{Intersector5Tuple}C{(A, B, sAB, aAB, c)}
306
821
  for each intersection found.
307
822
 
308
823
  @see: Methods L{All} for further details.
309
824
  '''
310
- aA = aB = _0_0
311
- for X in self.All(glA, glB, X0=X0, **sMax):
312
- r = self._In5T(glA, glB, X, X)
313
- yield r
314
- if aMax:
315
- aA += r.A.a12
316
- aB += r.B.a12
317
- if fabs(aA) > aMax or fabs(aB) > aMax:
318
- break
825
+ for X in self.All(glA, glB, X0=X0, **aMaX0_sMaX0_C):
826
+ yield self._In5T(glA, glB, X, X)
319
827
 
320
828
  def _Basic2(self, glA, glB, S, i=0):
321
829
  '''(INTERNAL) Get a basic solution.
@@ -330,18 +838,31 @@ class Intersector(_NamedBase):
330
838
 
331
839
  raise IntersectionError(Fmt.no_convergence(S.L1(), self._Tol))
332
840
 
333
- @Property_RO
334
- def _C_2(self): # normalizer, semi-circumference, C++ _d
335
- return self.R * PI # ~20K Km WGS84
841
+ def _C(self, X, glA, glB, _C=False, _AB=True):
842
+ # add the C{_C} items to C{X}, if requested.
843
+ if _C:
844
+ A = self._Position(glA, X.sA)
845
+ B = self._Position(glB, X.sB)
846
+ s, a = self._Inversa12(A, B)
847
+ X.set_(latA=A.lat2, lonA=A.lon2,
848
+ latB=B.lat2, lonB=B.lon2)
849
+ if _AB:
850
+ X.set_(sAB=s, aAB=a)
851
+ else:
852
+ X.set_(sMM=s, aMM=a)
853
+ return X
336
854
 
337
- def Closest(self, glA, glB, X0=_X000):
855
+ def Closest(self, glA, glB, X0=_X000, **_C):
338
856
  '''Find the closest intersection of two geodesic lines.
339
857
 
340
858
  @arg glA: A geodesic line (L{Line<Intersector.Line>}).
341
859
  @arg glB: An other geodesic line (L{Line<Intersector.Line>}).
342
- @kwarg X0: Optional offsets along the geodesic lines (L{XDist}).
860
+ @kwarg X0: Optional I{origin} for I{L1-closeness} (L{XDict}).
861
+ @kwarg _C: If C{True}, include the lat-/longitudes C{latA},
862
+ C{lonA}, C{latB}, C{lonB} oon and distances C{sAB}
863
+ and C{aSB} between the intersections.
343
864
 
344
- @return: The intersection (L{XDist}) or C{None} if none found.
865
+ @return: The intersection (L{XDict}) or C{None} if none found.
345
866
 
346
867
  @raise GeodesicError: Geodesic line B{C{glA}} or B{C{glB}}
347
868
  invalid, incompatible or ill-configured.
@@ -362,7 +883,7 @@ class Intersector(_NamedBase):
362
883
  Q, d, q = X, d0, i
363
884
  X._skip(S_, self._T2D1Delta)
364
885
 
365
- return None if Q is X0 else Q.set_(sX0=d, iteration=q)
886
+ return None if Q is X0 else self._C(Q, glA, glB, **_C).set_(sX0=d, iteration=q)
366
887
 
367
888
  def Closest5(self, glA, glB, X0=_X000):
368
889
  '''Find the closest intersection of two geodesic lines.
@@ -387,7 +908,7 @@ class Intersector(_NamedBase):
387
908
  # see "Algorithms for geodesics", eqs. 31, 32, 33.
388
909
  m23 = m13 * M12
389
910
  M32 = M31 * M12
390
- if m12:
911
+ if m12: # PYCHOK no cover
391
912
  m23 -= m12 * M13
392
913
  if m13:
393
914
  M32 += (_1 - M13 * M31) * m12 / m13
@@ -408,7 +929,7 @@ class Intersector(_NamedBase):
408
929
 
409
930
  @Property
410
931
  def _conjDist3s(self):
411
- gl, self._gl3, _D = self._gl3, None, self._C_2
932
+ gl, self._gl3, _D = self._gl3, None, self._cHalf
412
933
  return tuple(self._conjDist(gl, s) for s in (-_D, 0, _D))
413
934
 
414
935
  @_conjDist3s.setter # PYCHOK setter!
@@ -418,93 +939,70 @@ class Intersector(_NamedBase):
418
939
 
419
940
  def _conjDist3Tt_(self, c, X0=_X000):
420
941
  for s in self._conjDist3s:
421
- T = XDist(s, s * c, c)
942
+ T = XDict_(s, s * c, c)
422
943
  yield self._Delto(T), T.L1(X0)
423
944
 
424
945
  def _conjDist5(self, azi):
425
946
  gl = self._Line(azi1=azi)
426
- s = self._conjDist(gl, self._C_2)
427
- X, _ = self._Basic2(gl, gl, XDist(s * _0_5, -s * _1_5))
947
+ s = self._conjDist(gl, self._cHalf)
948
+ X, _ = self._Basic2(gl, gl, XDict_(s * _0_5, -s * _1_5))
428
949
  return s, (X.L1() - s * _2_0), azi, X.sA, X.sB
429
950
 
430
951
  @Property_RO
431
952
  def Delta(self):
432
953
  '''Get the equality and tiling margin (C{meter}).
433
954
  '''
434
- return self._C_2 * _EPSr5 # ~15 Km WGS84
955
+ return self._cHalf * _EPSr5 # ~15 Km WGS84
435
956
 
436
957
  def _Delto(self, X):
437
958
  # copy Delta into X, overriding X's default
438
959
  X._Delta = self.Delta # NOT X.set_(self.Delta)
439
960
  return X
440
961
 
441
- @Property_RO
442
- def ellipsoid(self):
443
- '''Get the C{geodesic}'s ellipsoid (C{Ellipsoid}).
444
- '''
445
- return self.geodesic.datum.ellipsoid
446
-
447
962
  @Property_RO
448
963
  def _EPS3R(self):
449
964
  return _EPS3 * self.R
450
965
 
451
- @Property_RO
452
- def f(self):
453
- '''Get the I{flattening} (C{scalar}), C{0} for spherical, negative for prolate.
454
- '''
455
- return self.ellipsoid.f
456
-
457
- flattening = f
458
-
459
966
  @Property_RO
460
967
  def _faPI_4(self):
461
968
  return (self.f + _2_0) * self.a * PI_4
462
969
 
463
- @property_RO
464
- def geodesic(self):
465
- '''Get the C{geodesic} (C{Geodesic...}).
466
- '''
467
- return self._g
468
-
469
970
  @Property_RO
470
971
  def _GeodesicLines(self):
471
972
  '''(INTERNAL) Get the C{Geodesic...Line} class(es).
472
973
  '''
473
974
  return type(self._Line()),
474
975
 
475
- def _In5T(self, glA, glB, S, X):
976
+ def _In5T(self, glA, glB, S, X, k2=False, **_2X):
476
977
  # Return an intersection as C{Intersector5Tuple}.
477
- A = self._Position(glA, S.sA, S.sX0)
478
- B = self._Position(glB, S.sB, S.sX0)
978
+ A = self._Position(glA, S.sA)
979
+ B = self._Position(glB, S.sB)
980
+ if k2:
981
+ A.set_(k2=X.kA)
982
+ B.set_(k2=X.kB)
479
983
  s, a = self._Inversa12(A, B)
480
- r = Intersector5Tuple(A, B, s, a, X.c, iteration=X.iteration)
481
- return r
482
-
483
- def _Inversa12(self, A, B=None):
484
- lls = (0, 0, A, 0) if B is None else (A.lat2, A.lon2,
485
- B.lat2, B.lon2)
486
- r = self._g.Inverse(*lls, outmask=Caps.DISTANCE)
487
- return r.s12, r.a12 # .a12 always in r
984
+ return Intersector5Tuple(A._2X(glA, **_2X),
985
+ B._2X(glB, **_2X), s, a, X.c, iteration=X.iteration)
488
986
 
489
987
  def _Inverse(self, A, B): # caps=Caps.STANDARD
490
988
  return self._g.Inverse(A.lat2, A.lon2, B.lat2, B.lon2)
491
989
 
492
- def Line(self, lat1, lon1, azi_lat2, *lon2, **name):
990
+ def Line(self, lat1, lon1, azi1_lat2, *lon2, **name):
493
991
  '''Return a geodesic line from this C{Intersector}'s geodesic, specified by
494
992
  two (goedetic) points or a (goedetic) point and an (initial) azimuth.
495
993
 
496
994
  @arg lat1: Latitude of the first point (C{degrees}).
497
995
  @arg lon1: Longitude of the first point (C{degrees}).
498
- @arg azi_lat2: Azimuth at the first point (compass C{degrees}) if no
996
+ @arg azi1_lat2: Azimuth at the first point (compass C{degrees}) if no
499
997
  B{C{lon2}} argument is given, otherwise the latitude of
500
998
  the second point (C{degrees}).
501
999
  @arg lon2: If given, the longitude of the second point (C{degrees}).
502
1000
  @kwarg name: Optional C{B{name}=NN} (C{str}).
503
1001
 
504
1002
  @return: A line (from L{geodesic<Intersector.geodesic>}C{.Line} or
505
- C{-.InverseLine}), properly configured for L{Intersector}.
1003
+ C{.InverseLine} method) with C{LINE_CAPS}.
506
1004
  '''
507
- args = (lat1, lon1, azi_lat2) + lon2
1005
+ args = self._ll3z4ll(lat1, lon1, azi1_lat2, *lon2)
508
1006
  gl = self._g.InverseLine(*args, caps=Caps.LINE_CAPS) if lon2 else \
509
1007
  self._g.Line( *args, caps=Caps.LINE_CAPS)
510
1008
  if name:
@@ -514,89 +1012,80 @@ class Intersector(_NamedBase):
514
1012
  def _Line(self, lat1=0, lon1=0, azi1=0):
515
1013
  return self._g.Line(lat1, lon1, azi1, caps=Caps.LINE_CAPS)
516
1014
 
1015
+ def Middle(self, glA, glB, raiser=True, **_C):
1016
+ '''Get the mid-points on two geodesic line segments.
1017
+
1018
+ @arg glA: A geodesic line (L{Line<Intersector.Line>}, 4-C{args}).
1019
+ @arg glB: An other geodesic line (L{Line<Intersector.Line>}, 4-C{args}).
1020
+ @kwarg raiser: If C{True}, check that B{C{glA}} and B{C{glB}} are a
1021
+ 4-C{args} L{Line<Intersector.Line>} or C{InverseLine}
1022
+ (C{bool}).
1023
+ @kwarg _C: If C{True}, include the lat-/longitudes C{latA}, C{lonA},
1024
+ C{latB}, C{lonB} of the mid-points and half-lengths C{sA}
1025
+ and C{sB} in C{meter} of the respective line segments.
1026
+
1027
+ @return: The mid-point and half-length of each segment (L{XDict}),
1028
+ B{C{_C}} above.
1029
+
1030
+ @raise GeodesicError: Geodesic line B{C{glA}} or B{C{glB}} invalid,
1031
+ incompatible, ill-configured or not a 4-C{args
1032
+ Line} or other C{InverseLine}.
1033
+ '''
1034
+ M, _, _ = self._middle3(glA, glB, raiser)
1035
+ # _ = X._outSide(sA, sB)
1036
+ return self._C(M, glA, glB, _AB=False, **_C) if _C else M
1037
+
1038
+ def _middle3(self, glA, glB, raiser): # in .All
1039
+ # return segment length C{sA} and C{sB} and the
1040
+ # center C{X0} of rectangle [sA, sB]
1041
+ self._xLines(glA, glB, s13=raiser) # need .Distance
1042
+ sA = glA.Distance()
1043
+ sB = glB.Distance()
1044
+ X = XDict_(sA * _0_5, sB * _0_5) # center
1045
+ return self._Delto(X), sA, sB
1046
+
1047
+ def Middle5(self, glA, glB, **raiser):
1048
+ '''Get the mid-points of two geodesic line segments and distances.
1049
+
1050
+ @return: A L{Middle5Tuple}C{(A, B, sMM, aMM, c)}.
1051
+
1052
+ @see: Method L{Middle} for further details.
1053
+ '''
1054
+ M = self.Middle(glA, glB, _C=True, **raiser)
1055
+ A, B, s, a, c = self._In5T(glA, glB, M, M, _2X=_M_)
1056
+ return Middle5Tuple(_llz2G(A, glA, glA), _llz2G(B, glB, glB), s, a, c)
1057
+
517
1058
  def _m12_M12_M21(self, gl, s):
518
1059
  P = gl.Position(s, outmask=Caps._REDUCEDLENGTH_GEODESICSCALE)
519
1060
  return P.m12, P.M12, P.M21
520
1061
 
521
- def Next(self, glA, glB, **eps):
1062
+ def Next(self, glA, glB, eps1=None, **_C): # PYCHOK no cover
522
1063
  '''Yield the next intersection of two I{intersecting} geodesic lines.
523
1064
 
524
1065
  @arg glA: A geodesic line (L{Line<Intersector.Line>}).
525
1066
  @arg glB: An other geodesic line (L{Line<Intersector.Line>}).
526
- @kwarg eps: Optional equality margin C{B{eps}=Delta} (C{degrees}).
1067
+ @kwarg eps1: Optional margin for the L{euclid<pygeodesy.euclid>}ean
1068
+ distance (C{degrees}) between the C{(lat1, lon1)} points
1069
+ of both lines or C{None} for unchecked.
1070
+ @kwarg _C: If C{True}, include the lat-/longitudes C{latA}, C{lonA},
1071
+ C{latB}, C{lonB} of and distances C{sAB} and C{aSB}
1072
+ between the intersections.
527
1073
 
528
- @return: The intersection (L{XDist}) or C{None} if none found.
1074
+ @return: The intersection (L{XDict}) or C{None} if none found.
529
1075
 
530
- @raise GeodesicError: Geodesic line B{C{glA}} or B{C{glB}}
531
- invalid, incompatible, ill-configured or
532
- C{(lat1, lon1)} not B{C{eps}}-equal.
1076
+ @raise GeodesicError: Geodesic line B{C{glA}} or B{C{glB}} invalid,
1077
+ incompatible, ill-configured or C{(lat1, lon1)}
1078
+ not B{C{eps1}}-equal.
533
1079
 
534
1080
  @raise IntersectionError: No convergence.
535
1081
 
536
1082
  @note: Offset C{X0} is implicit, zeros.
537
1083
  '''
538
1084
  self._xLines(glA, glB)
539
- e = _xkwds_get(eps, eps=self.Delta)
540
- a = glA.lat1 - glB.lat1
541
- b = glA.lon1 - glB.lon1
542
- if fabs(a) > e or fabs(b) > e:
543
- raise GeodesicError(lat1=a, lon1=b, eps=e)
544
- return self._Next(glA, glB)
545
-
546
- def Next5(self, glA, glB, eps=_EPS3):
547
- '''Yield the next intersection of two I{intersecting} geodesic lines.
1085
+ if eps1 or _C: # eps
1086
+ _C = self._xNext(glA, glB, eps1, **_C)
548
1087
 
549
- @return: An L{Intersector5Tuple}C{(A, B, sAB, aAB, c)} or C{None}
550
- if none found.
551
-
552
- @see: Method L{Next} for further details.
553
- '''
554
- X = self.Next(glA, glB, eps=eps)
555
- return X if X is None else self._In5T(glA, glB, X, X)
556
-
557
- def Next5s(self, glA, glB, X0=_X000, aMax=1801, sMax=0, avg=False, **Delta):
558
- '''Yield C{Next} intersections up to a maximal (angular) distance.
559
-
560
- @kwarg aMax: Upper limit for the angular distance (C{degrees}) or
561
- C{None} or C{0} for unlimited.
562
- @kwarg sMax: Upper limit for the distance (C{meter}) or C{None} or
563
- C{0} for unlimited.
564
- @kwarg avg: If C{True}, set the next intersection lat- and longitude
565
- to the mid-point of the previous ones (C{bool}).
566
- @kwarg Delta: Optional, margin overrding this margin (C{meter}), see
567
- prpoerty L{Delta<Intersector.Delta>}.
568
-
569
- @return: Yield an L{Intersector5Tuple}C{(A, B, sAB, aAB, c)} for
570
- every intersection found.
571
-
572
- @see: Methods L{Next5} for further details.
573
- '''
574
- X = self.Closest(glA, glB, X0=X0)
575
- if X is not None:
576
- D = _xkwds_get(Delta, Delta=self.Delta)
577
- S, _L, _abs = X, self._Line, fabs
578
- while True:
579
- A, B, _, _, _ = r = self._In5T(glA, glB, S, X)
580
- yield r
581
- if (aMax and (_abs(A.a12) > aMax or _abs(B.a12) > aMax)) or \
582
- (sMax and (_abs(A.s12) > sMax or _abs(B.s12) > sMax)):
583
- break
584
- latA, lonA = A.lat2, A.lon2
585
- latB, lonB = B.lat2, B.lon2
586
- if avg:
587
- latA = latB = favg(latA, latB)
588
- lonA = lonB = favg(lonA, lonB)
589
- X = self._Next(_L(latA, lonA, A.azi2),
590
- _L(latB, lonB, B.azi2))
591
- if X is None or X.L1() < D:
592
- break
593
- S += X.sA, X.sB
594
- S.set_(sX0=X.sX0 + S.sX0)
595
-
596
- def _Next(self, glA, glB):
597
- '''(INTERNAL) Find the next intersection.
598
- '''
599
- X0, self._conjDist3s = _X000, glA
1088
+ X0, self._conjDist3s = _X000, glA # reset Property
600
1089
  Q, d, S_, i = _XINF, INF, list(X0._nD2(self._D2)), 0
601
1090
  while S_:
602
1091
  X, i = self._Basic2(glA, glB, S_.pop(0), i)
@@ -623,7 +1112,18 @@ class Intersector(_NamedBase):
623
1112
  T = X + (s, s * c) # NOT +=
624
1113
  T._skip(S_, self._T2D2Delta)
625
1114
 
626
- return None if Q is _XINF else Q.set_(sX0=d, iteration=q)
1115
+ return None if Q is _XINF else self._C(Q, glA, glB, **_C).set_(sX0=d, iteration=q)
1116
+
1117
+ def Next5(self, glA, glB, **eps1): # PYCHOK no cover
1118
+ '''Yield the next intersection of two I{intersecting} geodesic lines.
1119
+
1120
+ @return: An L{Intersector5Tuple}C{(A, B, sAB, aAB, c)} or C{None}
1121
+ if none found.
1122
+
1123
+ @see: Method L{Next} for further details.
1124
+ '''
1125
+ X = self.Next(glA, glB, **eps1)
1126
+ return X if X is None else self._In5T(glA, glB, X, X)
627
1127
 
628
1128
  def _obliqDist4(self):
629
1129
  zx = 45.0
@@ -645,7 +1145,7 @@ class Intersector(_NamedBase):
645
1145
  if not dsx:
646
1146
  break
647
1147
  else:
648
- sx, sAx, sBx = self._C_2, _0_5, -_1_5
1148
+ sx, sAx, sBx = self._cHalf, _0_5, -_1_5
649
1149
  return sx, zx, sAx, sBx
650
1150
 
651
1151
  def _polarB3(self, lats=False): # PYCHOK no cover
@@ -677,7 +1177,7 @@ class Intersector(_NamedBase):
677
1177
  _, lat = _pD2(latx, lat2=True)
678
1178
  sx += sx
679
1179
  else:
680
- sx = self._C_2
1180
+ sx = self._cHalf
681
1181
  return sx, latx, lat
682
1182
 
683
1183
  def _polarDist2(self, lat1, lat2=False):
@@ -687,19 +1187,71 @@ class Intersector(_NamedBase):
687
1187
  lat1 = gl.Position(s, outmask=Caps.LATITUDE).lat2
688
1188
  return s, lat1
689
1189
 
690
- def _Position(self, gl, s, *sX0):
691
- P = gl.Position(s, outmask=Caps._STD_LINE)
692
- if sX0:
693
- X = gl.Position(*sX0, outmask=Caps._STD_LINE)
694
- P.set_(lat0=X.lat2, lon0=X.lon2,
695
- azi0=X.azi2, s10=X.s12, a10=X.a12)
696
- return P
1190
+ def _Position(self, gl, s):
1191
+ return gl.Position(s, outmask=Caps._STD_LINE)
1192
+
1193
+ def Segment(self, glA, glB, proven=None, raiser=True, **_C):
1194
+ '''Find the intersection between two geodesic line segments.
1195
+
1196
+ @kwarg proven: Conjecture is that whenever two geodesic line
1197
+ segments intersect, the intersection is the
1198
+ one closest to the mid-points of segments.
1199
+ If so, use C{B{proven}=True}, otherwise find
1200
+ intersections on the segments and specify
1201
+ C{B{proven}=None} to return the first or
1202
+ C{B{proven}=False} the closest (C{bool}).
1203
+ @kwarg raiser: If C{True}, check that B{C{glA}} and B{C{glB}}
1204
+ are an 4-C{args} L{Line<Intersector.Line>} or
1205
+ C{InverseLine} (C{bool}).
1206
+ @kwarg _C: If C{True}, include the lat-/longitudes C{latA},
1207
+ C{lonA}, C{latB}, C{lonB} of and distances C{sAB}
1208
+ and C{aSB} between the intersections.
1209
+
1210
+ @return: The intersection of the segments (L{XDict}) with
1211
+ indicators C{kA}, C{kB} and C{k} set or if no
1212
+ intersection is found, C{None}.
697
1213
 
698
- @Property_RO
699
- def R(self):
700
- '''Get the I{authalic} earth radius (C{meter}).
1214
+ @raise GeodesicError: Geodesic line B{C{glA}} or B{C{glB}}
1215
+ invalid, incompatible, ill-configured or
1216
+ not an C{InverseLine} or 4-C{args Line}.
1217
+
1218
+ @raise IntersectionError: No convergence.
1219
+
1220
+ @see: Method L{Middle<Intersector.Middle>} for further details.
701
1221
  '''
702
- return self.ellipsoid.R2
1222
+ X0, sA, sB = self._middle3(glA, glB, raiser)
1223
+ Q = self.Closest(glA, glB, X0) # to X0
1224
+ if Q is not None:
1225
+ if Q.c: # anti-/parallel
1226
+ Q._fixSegment(sA, sB)
1227
+ # are rectangle [sA, sB] corners further from X0 than Q?
1228
+ d0 = X0.L1(Q)
1229
+ if Q._outSide(sA, sB) and d0 <= X0.L1() and not proven:
1230
+ i = Q.iteration
1231
+ for T in X0._corners(sA, sB, self._T2):
1232
+ X, i = self._Basic2(glA, glB, T, i)
1233
+ X = T._fixCoincident(X)
1234
+ if not X._outSide(sA, sB):
1235
+ d = X0.L1(X)
1236
+ if d < d0 or proven is None:
1237
+ Q, d0 = X, d
1238
+ if proven is None:
1239
+ break
1240
+ Q.set_(iteration=i)
1241
+
1242
+ Q = self._C(Q, glA, glB, **_C).set_(sX0=d0)
1243
+ return Q
1244
+
1245
+ def Segment5(self, glA, glB, **proven_raiser):
1246
+ '''Find the intersection between two geodesic line segments.
1247
+
1248
+ @return: An L{Intersector5Tuple}C{(A, B, sAB, aAB, c)}
1249
+ or C{None} if none found.
1250
+
1251
+ @see: Method L{Segment} for further details.
1252
+ '''
1253
+ X = self.Segment(glA, glB, **proven_raiser)
1254
+ return X if X is None else self._In5T(glA, glB, X, X, k2=True)
703
1255
 
704
1256
  def _Spherical(self, glA, glB, S):
705
1257
  '''(INTERNAL) Get solution based from a spherical triangle.
@@ -735,10 +1287,10 @@ class Intersector(_NamedBase):
735
1287
  # in {sa, sb} * sz; this is probably not necessary].
736
1288
  # Definitely need to treat sz < 0 (z > PI*R) correctly in
737
1289
  # order to avoid some convergence failures in _Basic2.
738
- sA = atan2(sb * sz, sb * ca * cz - cb * sa) * self.R
739
- sB = atan2(sa * sz, -sa * cb * cz + ca * sb) * self.R
1290
+ sA = atan2(sb * sz, sb * ca * cz - sa * cb) * self.R
1291
+ sB = atan2(sa * sz, -sa * cb * cz + sb * ca) * self.R
740
1292
  c = 0
741
- return XDist(sA, sB, c) # no ._Delto
1293
+ return XDict_(sA, sB, c) # no ._Delto
742
1294
 
743
1295
  @Property_RO
744
1296
  def _T2D1Delta(self):
@@ -753,7 +1305,7 @@ class Intersector(_NamedBase):
753
1305
 
754
1306
  @Property_RO
755
1307
  def _Tol(self): # convergence tolerance
756
- return self._C_2 * pow(EPS, 0.75) # _0_75
1308
+ return self._cHalf * pow(EPS, 0.75) # _0_75
757
1309
 
758
1310
  def toStr(self, **prec_sep_name): # PYCHOK signature
759
1311
  '''Return this C{Intersector} as string.
@@ -765,13 +1317,16 @@ class Intersector(_NamedBase):
765
1317
  '''
766
1318
  return self._instr(props=(Intersector.geodesic,), **prec_sep_name)
767
1319
 
768
- def _xLines(self, glA, glB):
1320
+ def _xLines(self, glA, glB, s13=False):
769
1321
  # check two geodesic lines vs this geodesic
770
1322
  C, gls = Caps.LINE_CAPS, dict(glA=glA, glB=glB)
771
1323
  _xinstanceof(*self._GeodesicLines, **gls)
772
1324
  for n, gl in gls.items():
773
1325
  try:
774
1326
  _xgeodesics(gl.geodesic, self.geodesic)
1327
+ if s13 and not isfinite(gl.s13): # or not gl.caps & Caps.DISTANCE_IN
1328
+ t = gl.geodesic.InverseLine.__name__
1329
+ raise TypeError(_not_(_an(t)))
775
1330
  c = gl.caps & C
776
1331
  if c != C: # not gl.caps_(C)
777
1332
  c, C, x = map1(bin, c, C, _xor(c, C))
@@ -781,21 +1336,47 @@ class Intersector(_NamedBase):
781
1336
  raise GeodesicError(n, gl, cause=x)
782
1337
 
783
1338
 
784
- class Intersector5Tuple(_NamedTuple):
1339
+ class Intersectool5Tuple(_NamedTuple):
1340
+ '''5-Tuple C{(A, B, sAB, aAB, c)} with C{A} and C{B} the C{Position}
1341
+ of the intersection on each geodesic line, the distance C{sAB}
1342
+ between C{A} and C{B} in C{meter}, the angular distance C{aAB} in
1343
+ C{degrees} and coincidence indicator C{c} (C{int}), see L{XDict}.
1344
+
1345
+ @note: C{A} and C{B} are each a C{GDict} with C{lat1}, C{lon1} and
1346
+ C{azi1} or C{lat2}, C{lon2} from the geodesic line C{glA}
1347
+ respectively C{glB} and the intersection location in C{latX},
1348
+ C{lonX}, distance C{s1X} in C{meter} and angular distance
1349
+ C{a1M} in C{degrees} and the segment indicator C{kX}. See
1350
+ L{XDict} for more details.
1351
+ '''
1352
+ _Names_ = (_A_, _B_, _sAB_, 'aAB', _c_)
1353
+ _Units_ = (_Pass, _Pass, Meter, Degrees, Int)
1354
+
1355
+
1356
+ class Intersector5Tuple(Intersectool5Tuple):
785
1357
  '''5-Tuple C{(A, B, sAB, aAB, c)} with C{A} and C{B} the C{Position}
786
1358
  of the intersection on each geodesic line, the distance C{sAB}
787
1359
  between C{A} and C{B} in C{meter}, angular distance C{aAB} in
788
- C{degrees} and coincidence indicator C{c} (C{int}), see L{XDist}.
1360
+ C{degrees} and coincidence indicator C{c} (C{int}), see L{XDict}.
789
1361
 
790
1362
  @note: C{A} and C{B} are each a C{GeodesicLine...Position} for
791
1363
  C{outmask=Caps.STANDARD} with the intersection location in
792
- C{lat2}, C{lon2}, azimuth in C{azi2}, the distance C{s12}
793
- in C{meter} and angular distance C{a12} in C{degrees} and
794
- extended with the C{X0} offset location in C{lat0}, C{lon0},
795
- C{azi0}, C{s10} and C{a10}.
1364
+ C{latX}, C{lonX}, azimuth in C{aziX}, distance C{s1X} in
1365
+ C{meter} and angular distance C{a1X} in C{degrees} and the
1366
+ segment indicator C{kX}. See L{XDict} for more details.
796
1367
  '''
797
- _Names_ = (_A_, _B_, 'sAB', 'aAB', _c_)
798
- _Units_ = (_Pass, _Pass, Meter, Degrees, Int)
1368
+ _Names_ = Intersectool5Tuple._Names_
1369
+
1370
+
1371
+ class Middle5Tuple(Intersectool5Tuple):
1372
+ '''5-Tuple C{(A, B, sMM, aMM, c)} with C{A} and C{B} the I{line segments}
1373
+ including the mid-point location in C{latM}, C{lonM}, distance C{s1M}
1374
+ in C{meter} and angular distance C{a1M} in C{degrees}, the distance
1375
+ between both mid-points C{sMM} in C{meter} and angular distance C{aMM}
1376
+ in C{degrees} and coincidence indicator C{c} (C{int}). See L{XDict}
1377
+ for more details.
1378
+ '''
1379
+ _Names_ = (_A_, _B_, 'sMM', 'aMM', _c_)
799
1380
 
800
1381
 
801
1382
  class _List(list):
@@ -823,68 +1404,274 @@ class _List(list):
823
1404
  self.append(X)
824
1405
  return X.sX0
825
1406
 
826
- def sortrim(self, X0, sMax):
1407
+ def sorter(self, sMaX0, dot_C, glA, glB, **_C):
827
1408
  # trim and sort the X items
828
1409
 
829
- def _key(Xk):
830
- _, k = Xk
831
- return k # rank of X
1410
+ def _key(X):
1411
+ return X.sX0 # rank of X
832
1412
 
833
- for X, _ in sorted(self.trim(X0, sMax), key=_key):
834
- yield X # de-tuple (X, k)
1413
+ t = (X for X in self if X.sX0 <= sMaX0)
1414
+ for X in sorted(t, key=_key):
1415
+ yield dot_C(X, glA, glB, **_C) if _C else X
835
1416
 
836
- def trim(self, X0, sMax):
837
- # trim and yield 2-tuple (X, rank)
838
- a, b, _eu = X0.sA, X0.sB, euclid
839
1417
 
840
- for X in self:
841
- k = X.sX0
842
- if k <= sMax:
843
- k += _eu(X.sA - a, X.sB - b)
844
- yield X, k # rank of X
845
-
846
-
847
- if __name__ == '__main__':
848
-
849
- from pygeodesy import GeodesicExact, printf
850
-
851
- I = Intersector(GeodesicExact(), name='Test') # PYCHOK I
852
-
853
- # <https://GeographicLib.sourceforge.io/C++/doc/classGeographicLib_1_1Intersect.html>
854
- a = I.Line( 0, 0, 45)
855
- b = I.Line(45, 10, 135)
856
- printf('Closest: %r', I.Closest(a, b))
857
- printf('Closest5: %r', I.Closest5(a, b), nt=1)
858
-
859
- for i, t in enumerate(I.Next5s(a, b)):
860
- printf('Next5s %s: %r (%s)', i, t, t.iteration)
861
-
862
- # <https://GeographicLib.sourceforge.io/C++/doc/IntersectTool.1.html>
863
- a = I.Line(50, -4, -147.7)
864
- b = I.Line( 0, 0, 90)
865
- printf('Closest: %r', I.Closest(a, b), nl=1)
866
- printf('Closest5: %r', I.Closest5(a, b), nt=1)
867
-
868
- a = I.Line(50, -4, -147.7)
869
- b = I.Line( 0, 180, 0)
870
- for i, X in enumerate(I.All(a, b)):
871
- printf('All %s: %r (%s)', i, X, X.iteration)
872
- if i > 9:
873
- break
874
- printf('')
875
- for i, t in enumerate(I.All5(a, b)):
876
- printf('All5 %s: %r (%s)', i, t, t.iteration)
877
- if i > 9:
878
- break
879
-
880
- # % echo 50N 4W 147.7W 0 0 90 | IntersectTool -e 6371e3 0 -c -p 0 -C
881
- # 6077191 -3318019 0
882
- # -0.00000 -29.83966 -0.00000 -29.83966 0
883
- I = Intersector(GeodesicExact(6371e3, 0), name='Test') # PYCHOK I
884
- a = I.Line(50, -4, -147.7)
885
- b = I.Line( 0, 0, 90)
886
- printf('Closest: %r', I.Closest(a, b), nl=1)
887
- printf('Closest5: %r', I.Closest5(a, b))
1418
+ def _L1(a, b):
1419
+ '''(INTERNAL) Return the I{L1} distance.
1420
+ '''
1421
+ return fabs(a) + fabs(b)
1422
+
1423
+
1424
+ def _llz2G(G, gl, il):
1425
+ '''(INTERNAL) Set C{InverseLine} attrs in C{G}, a C{GDict}.
1426
+ '''
1427
+ return G.set_(lat1=gl.lat1, lon1=gl.lon1, azi1=il.azi1, a12=il.a13, # .Arc()
1428
+ lat2=gl.lat2, lon2=gl.lon2, azi2=il.azi2, s12=il.s13) # .Distance()
1429
+
1430
+
1431
+ if __name__ == '__main__': # MCCABE 14
1432
+
1433
+ from pygeodesy import printf
1434
+
1435
+ def _main(args):
1436
+
1437
+ from pygeodesy import GeodesicExact
1438
+ from pygeodesy.internals import _plural, _usage
1439
+ from pygeodesy.interns import _COLONSPACE_, _DASH_, _DOT_, \
1440
+ _EQUAL_, _i_, _n_, _version_, _X_
1441
+ import re
1442
+
1443
+ class XY0(Float):
1444
+ pass
1445
+
1446
+ def _opts(_h): # for _usage()
1447
+ ll4 = ' latA1 lonA1'
1448
+ ll4 += ll4.replace('1', '2')
1449
+ ll4 += ll4.replace(_A_, _B_)
1450
+ llz = _SPACE_(NN, _latA_, _lonA_, 'aziA')
1451
+ llz2 = llz + llz.replace(_A_, _B_)
1452
+ return dict(opts='-Verbose|V--version|v--help|h--Tool|T--Check|C-R meter-',
1453
+ alts=((_c_ + llz2),
1454
+ (_i_ + ll4),
1455
+ (_m_ + ll4),
1456
+ (_M_ + ll4),
1457
+ (_n_ + llz + ' aziB'),
1458
+ ('o' + llz2 + ' x0 y0')),
1459
+ help=_h if isinstance(_h, str) else NN)
1460
+
1461
+ def _starts(opt, arg, n=0):
1462
+ return opt.startswith(arg) and len(arg.lstrip(_DASH_)) > n
1463
+
1464
+ _isopt = re.compile('^[-]+[a-z]*$', flags=re.IGNORECASE).match
1465
+
1466
+ I = Intersector(GeodesicExact()) # PYCHOK I
1467
+ M = m = _R = None
1468
+ _T = _V = _h = _C = False
1469
+
1470
+ while args and _isopt(args[0]):
1471
+ arg = args.pop(0)
1472
+ if arg == _C__ or _starts('--Check', arg):
1473
+ _C = True
1474
+ elif arg == _c__:
1475
+ M, m = I.Closest, 6 # latA lonA aziA latB lonB aziB
1476
+ elif arg == '-h' or _starts('--help', arg):
1477
+ _h = args[0] if args and _isopt(args[0]) else True
1478
+ elif arg == _i__:
1479
+ M, m = I.Segment, 8 # latA1 lonA1 latA2 lonA2 latB1 lonB1 latB2 lonB2
1480
+ elif arg == '-m':
1481
+ M, m = I.Middle, 8 # latA1 lonA1 latA2 lonA2 latB1 lonB1 latB2 lonB2
1482
+ _R = None
1483
+ elif arg == '-M':
1484
+ M, m = I.Middle5, 8 # latA1 lonA1 latA2 lonA2 latB1 lonB1 latB2 lonB2
1485
+ _R = _C = None
1486
+ elif arg == _n__:
1487
+ M, m = I.Next, 4 # latA lonA aziA aziB
1488
+ elif arg == _o__:
1489
+ M, m = I.Closest, 8 # latA lonA aziA latB lonB aziB x0 y0
1490
+ elif arg == _R__ and args:
1491
+ _R = args.pop(0)
1492
+ elif arg == '-T' or _starts('--Tool', arg):
1493
+ I = Intersectool() # PYCHOK I
1494
+ if _V:
1495
+ I.verbose = True
1496
+ if I.IntersectTool in (_PYGEODESY_INTERSECTTOOL_, None): # not set
1497
+ I.IntersectTool = '/opt/local/bin/IntersectTool' # '/opt/local/Cellar/geographiclib/2.3/bin/IntersectTool' # HomeBrew
1498
+ M, _T = None, True
1499
+ elif arg == '-V' or _starts('--Verbose', arg):
1500
+ _V = True
1501
+ if _T:
1502
+ I.verbose = True
1503
+ elif arg == '-v' or _starts('--version', arg):
1504
+ printf(_COLONSPACE_(*((_version_, I.version) if _T else
1505
+ (__version__, repr(I)))))
1506
+ else:
1507
+ raise ValueError('invalid option %r' % (arg,))
1508
+
1509
+ if _h or M is None:
1510
+ printf(_usage(__file__, **_opts(_h)), nl=1)
1511
+ # exit(0)
1512
+ else:
1513
+ n = len(args)
1514
+ if n < m:
1515
+ n = _plural('only %s arg' % (n,), n) if n else 'no args'
1516
+ raise ValueError('%s, need %s' % (n, m))
1517
+ args[:] = args[:m]
1518
+
1519
+ kwds = dict(_C=True) if _C else {}
1520
+ if M == I.Next: # -n
1521
+ # get latA lonA aziA latA lonA aziB
1522
+ args[3:] = args[:2] + args[3:4]
1523
+ elif M == I.Closest and m > 6: # -o
1524
+ y0 = Meter(y0=args.pop())
1525
+ x0 = Meter(x0=args.pop())
1526
+ kwds.update(X0=XDict_(x0, y0))
1527
+ if _R:
1528
+ m = Meter_(_R, name=_R__, low=0)
1529
+ kwds.update(sMaX0=m)
1530
+ M = I.All
1531
+
1532
+ n = len(args) // 2
1533
+ glA = I.Line(*args[:n])
1534
+ glB = I.Line(*args[n:])
1535
+
1536
+ m = _DOT_(I.__class__.__name__, M.__name__)
1537
+ if _V:
1538
+ X = _SPACE_(_X_, _EQUAL_, m)
1539
+ printf(unstr(X, glA, glB, **kwds))
1540
+
1541
+ X = M(glA, glB, **kwds)
1542
+ if X is None or isinstance(X, (XDict, tuple)):
1543
+ printf(_COLONSPACE_(m, repr(X)))
1544
+ else:
1545
+ for i, X in enumerate(X):
1546
+ printf(_COLONSPACE_(Fmt.INDEX(m, i), repr(X)))
1547
+
1548
+ from sys import argv, stderr
1549
+ try:
1550
+ if len(argv) == 2 and argv[1] == _HASH_:
1551
+ from pygeodesy.internals import _usage_argv
1552
+
1553
+ s = _SPACE_(*_usage_argv(__file__))
1554
+ for t in ('-h', '-h -n',
1555
+ '-c 0 0 45 40 10 135',
1556
+ '-C -c 0 0 45 40 10 135',
1557
+ '-T -R 2.6e7 -c 0 0 45 40 10 135',
1558
+ '-c 50 -4 -147.7 0 0 90',
1559
+ '-C -c 50 -4 -147.7 0 0 90',
1560
+ '# % echo 0 0 10 10 50 -4 50S 4W | IntersectTool -i -p 0 -C',
1561
+ '# -631414 5988887 0 -3',
1562
+ '# -4.05187 -4.00000 -4.05187 -4.00000 0',
1563
+ '-C -m 0 0 10 10 50 -4 50S 4W',
1564
+ '-M 0 0 10 10 50 -4 50S 4W',
1565
+ '-i 0 0 10 10 50 -4 50S 4W',
1566
+ '-T -i 0 0 10 10 50 -4 50S 4W',
1567
+ '-C -i 0 0 10 10 50 -4 50S 4W',
1568
+ '-T -C -i 0 0 10 10 50 -4 50S 4W',
1569
+ '-V -T -i 0 0 10 10 50 -4 -50 -4',
1570
+ '-C -R 4e7 -c 50 -4 -147.7 0 0 90',
1571
+ '-T -C -R 4e7 -c 50 -4 -147.7 0 0 90',
1572
+ '-R 4e7 -i 0 0 10 10 50 -4 -50 -4',
1573
+ '-T -R 4e7 -i 0 0 10 10 50 -4 -50 -4'):
1574
+ if t.startswith(_HASH_):
1575
+ printf(t, nl=int(t[2] == '%'))
1576
+ else:
1577
+ printf(_SPACE_(_HASH_, s, t), nl=1)
1578
+ argv[1:] = t = t.split()
1579
+ _main(t)
1580
+ else:
1581
+ _main(argv[1:])
1582
+
1583
+ except Exception as x:
1584
+ x = _SPACE_(x, NN, _HASH_, *argv)
1585
+ printf(x, file=stderr, nl=1)
1586
+ if '-V' in x or _MODS.errors.exception_chaining():
1587
+ raise
1588
+ exit(1)
1589
+
1590
+ # % env PYGEODESY_INTERSECTTOOL=... python3 -m pygeodesy.geodesici -h
1591
+ #
1592
+ # usage: python3 -m ....pygeodesy.geodesici [--Verbose | -V] [--version | -v] [--help | -h] [--Tool | -T] [--Check | -C] [-R meter]
1593
+ # [-c latA lonA aziA latB lonB aziB |
1594
+ # -i latA1 lonA1 latA2 lonA2 latB1 lonB1 latB2 lonB2 |
1595
+ # -m latA1 lonA1 latA2 lonA2 latB1 lonB1 latB2 lonB2 |
1596
+ # -M latA1 lonA1 latA2 lonA2 latB1 lonB1 latB2 lonB2 |
1597
+ # -n latA lonA aziA aziB |
1598
+ # -o latA lonA aziA latB lonB aziB x0 y0]
1599
+
1600
+ # % python3 -m ....pygeodesy.geodesici -h -n
1601
+ #
1602
+ # usage: python3 -m ....pygeodesy.geodesici -n latA lonA aziA aziB
1603
+
1604
+ # % python3 -m ....pygeodesy.geodesici -c 0 0 45 40 10 135
1605
+ # Intersector.Closest: XDict(c=0, sA=3862290.547855, sB=2339969.547699, sX0=6202260.095554)
1606
+
1607
+ # % python3 -m ....pygeodesy.geodesici -C -c 0 0 45 40 10 135
1608
+ # 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)
1609
+
1610
+ # % env PYGEODESY_INTERSECTTOOL=...python3 -m ....pygeodesy.geodesici -T -R 2.6e7 -c 0 0 45 40 10 135
1611
+ # Intersectool.All[0]: XDict(c=0, sA=3862290.547855, sB=2339969.547699, sX0=6202260.095554)
1612
+
1613
+ # % python3 -m ....pygeodesy.geodesici -c 50 -4 -147.7 0 0 90
1614
+ # Intersector.Closest: XDict(c=0, sA=6058048.653081, sB=-3311252.995823, sX0=9369301.648903)
1615
+
1616
+ # % python3 -m ....pygeodesy.geodesici -C -c 50 -4 -147.7 0 0 90
1617
+ # 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)
1618
+
1619
+ # % echo 0 0 10 10 50 -4 50S 4W | IntersectTool -i -p 0 -C
1620
+ # -631414 5988887 0 -3
1621
+ # -4.05187 -4.00000 -4.05187 -4.00000 0
1622
+
1623
+ # % python3 -m ....pygeodesy.geodesici -C -m 0 0 10 10 50 -4 50S 4W
1624
+ # Intersector.Middle: XDict(aMM=10.262308, c=0, latA=5.019509, latB=0.036282, lonA=4.961883, lonB=-4.0, sA=782554.549609, sB=5536835.161499, sMM=1138574.546746, sX0=0.0)
1625
+
1626
+ # % python3 -m ....pygeodesy.geodesici -M 0 0 10 10 50 -4 50S 4W
1627
+ # Intersector.Middle5: Middle5Tuple(A=GDict(a12=14.106434, a1M=7.053396, azi1=44.75191, azi2=45.629037, aziM=44.969535, lat1=0.0, lat2=10.0, latM=5.019509, lon1=0.0, lon2=10.0, lonM=4.961883, s12=1565109.099218, s1M=782554.549609), B=GDict(a12=99.810444, a1M=49.869061, azi1=180.0, azi2=180.0, aziM=180.0, lat1=50.0, lat2=-50.0, latM=0.036282, lon1=-4.0, lon2=-4.0, lonM=-4.0, s12=11073670.322999, s1M=5536835.161499), sMM=1138574.546746, aMM=10.262308, c=0)
1628
+
1629
+ # % python3 -m ....pygeodesy.geodesici -i 0 0 10 10 50 -4 50S 4W
1630
+ # Intersector.Segment: XDict(c=0, k=-3, kA=-1, kB=0, sA=-631414.26877, sB=5988887.278435, sX0=1866020.935315)
1631
+
1632
+ # % env PYGEODESY_INTERSECTTOOL=... python3 -m ....pygeodesy.geodesici -T -i 0 0 10 10 50 -4 50S 4W
1633
+ # Intersectool.Segment: XDict(c=0, k=-3, kA=-1, kB=0, sA=-631414.26877, sB=5988887.278435)
1634
+
1635
+ # % python3 -m ....pygeodesy.geodesici -C -i 0 0 10 10 50 -4 50S 4W
1636
+ # 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)
1637
+
1638
+ # % env PYGEODESY_INTERSECTTOOL=... python3 -m ....pygeodesy.geodesici -T -C -i 0 0 10 10 50 -4 50S 4W
1639
+ # 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)
1640
+
1641
+ # % env PYGEODESY_INTERSECTTOOL=... python3 -m ....pygeodesy.geodesici -V -T -i 0 0 10 10 50 -4 -50 -4
1642
+ # Intersectool@1: /opt/local/bin/IntersectTool --version (invoke)
1643
+ # Intersectool@1: '/opt/local/bin/IntersectTool: GeographicLib version 2.3' (0)
1644
+ # Intersectool@1: /opt/local/bin/IntersectTool: GeographicLib version 2.3 (0)
1645
+ # 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))
1646
+ # 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)
1647
+ # Intersectool@2: '-631414.2687702414 5988887.2784352796 0 -3' (0)
1648
+ # Intersectool@2: sA=-631414.2687702414, sB=5988887.2784352796, c=0, k=-3 (0)
1649
+ # Intersectool.Segment: XDict(c=0, k=-3, kA=-1, kB=0, sA=-631414.26877, sB=5988887.278435)
1650
+
1651
+ # % python3 -m ....pygeodesy.geodesici -C -R 4e7 -c 50 -4 -147.7 0 0 90
1652
+ # 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)
1653
+ # 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)
1654
+ # 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)
1655
+ # 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)
1656
+
1657
+ # % env PYGEODESY_INTERSECTTOOL=... python3 -m ....pygeodesy.geodesici -T -C -R 4e7 -c 50 -4 -147.7 0 0 90
1658
+ # 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)
1659
+ # 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)
1660
+ # 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)
1661
+ # 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)
1662
+
1663
+ # % python3 -m ....pygeodesy.geodesici -R 4e7 -i 0 0 10 10 50 -4 -50 -4
1664
+ # Intersector.All[0]: XDict(c=0, sA=-631414.26877, sB=5988887.278435, sX0=1866020.935315)
1665
+ # Intersector.All[1]: XDict(c=0, sA=19422725.117572, sB=-14062417.105648, sX0=38239422.83511)
1666
+ # Intersector.All[2]: XDict(c=0, sA=19422725.117572, sB=25945445.811603, sX0=39048781.218067)
1667
+ # Intersector.All[3]: XDict(c=0, sA=39476927.464575, sB=5894074.699478, sX0=39051612.452944)
1668
+
1669
+ # % env PYGEODESY_INTERSECTTOOL=... python3 -m ....pygeodesy.geodesici -T -R 4e7 -i 0 0 10 10 50 -4 -50 -4
1670
+ # Intersectool.All[0]: XDict(c=0, sA=-631414.26877, sB=5988887.278435, sX0=1862009.05513)
1671
+ # Intersectool.All[1]: XDict(c=0, sA=19422725.117572, sB=-14062417.105648, sX0=38243434.715295)
1672
+ # Intersectool.All[2]: XDict(c=0, sA=19422725.117572, sB=25945445.811603, sX0=39044769.337882)
1673
+ # Intersectool.All[3]: XDict(c=0, sA=39476927.464575, sB=5894074.699478, sX0=39047600.57276)
1674
+
888
1675
 
889
1676
  # **) MIT License
890
1677
  #