Coverage for pygeodesy/ellipsoidalBaseDI.py: 91%
331 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'''(INTERNAL) Private, ellipsoidal Direct/Inverse geodesy base
5class C{LatLonEllipsoidalBaseDI} and functions.
6'''
7# make sure int/int division yields float quotient, see .basics
8from __future__ import division as _; del _ # noqa: E702 ;
10# from pygeodesy.azimuthal import _Equidistants # _MODS
11from pygeodesy.basics import isLatLon, _xsubclassof, typename
12from pygeodesy.constants import EPS, MAX, PI, PI2, PI_4, isnear0, isnear1, \
13 _EPSqrt as _TOL, _0_0, _0_01, _1_0, _1_5, _3_0
14# from pygeodesy.dms import F_DMS # _MODS
15from pygeodesy.ellipsoidalBase import LatLonEllipsoidalBase, _TOL_M, property_RO
16from pygeodesy.errors import _AssertionError, IntersectionError, _IsnotError, _or, \
17 _ValueError, _xellipsoidal, _xError, _xkwds_not
18from pygeodesy.fmath import favg, fmean_
19from pygeodesy.fsums import Fmt, fsumf_
20from pygeodesy.formy import _isequalTo, opposing, _radical2
21# from pygeodesy.geodesicw import _Intersecant2, _PlumbTo # _MODS
22# from pygeodesy.internals import typename # from .basics
23from pygeodesy.interns import _antipodal_, _concentric_, _ellipsoidal_, _low_, \
24 _exceed_PI_radians_, _near_, _SPACE_, _too_
25from pygeodesy.lazily import _ALL_DOCS, _ALL_LAZY, _ALL_MODS as _MODS
26from pygeodesy.namedTuples import Bearing2Tuple, Destination2Tuple, Intersection3Tuple, \
27 NearestOn2Tuple, NearestOn8Tuple, _LL4Tuple
28# from pygeodesy.props import property_RO # from .ellipsoidalBase
29# from pygeodesy.sphericalNvector import _intersects2 # _MODS
30# from pygeodesy.sphericalTrigonometry import _intersect, LatLon # _MODS
31# from pygeodesy.streprs import Fmt # from .fsums
32from pygeodesy.units import _fi_j2, _isDegrees, _isHeight, _isRadius, Radius_, Scalar
33from pygeodesy.utily import m2km, unroll180, _unrollon, _unrollon3, _Wrap, wrap360
34# from pygeodesy.vector3d import _intersects2, _intersect3d3, _nearestOn2, Vector3d # _MODS
36from math import degrees, radians
38__all__ = _ALL_LAZY.ellipsoidalBaseDI
39__version__ = '25.05.23'
41_polar__ = 'polar?'
42_TRIPS = 33 # _intersect3, _intersects2, _nearestOn interations, 6..9 sufficient?
45class LatLonEllipsoidalBaseDI(LatLonEllipsoidalBase):
46 '''(INTERNAL) Base class for C{ellipsoidal*.LatLon} classes
47 with I{overloaded} C{Direct} and C{Inverse} methods.
48 '''
50 def bearingTo2(self, other, wrap=False):
51 '''Compute the initial and final bearing (forward and reverse
52 azimuth) from this to an other point, using this C{Inverse}
53 method. See methods L{initialBearingTo} and L{finalBearingTo}
54 for more details.
56 @arg other: The other point (this C{LatLon}).
57 @kwarg wrap: If C{True}, wrap or I{normalize} and unroll the
58 B{C{other}} point (C{bool}).
60 @return: A L{Bearing2Tuple}C{(initial, final)}.
62 @raise TypeError: If B{C{other}} not this C{LatLon} class.
64 @raise ValueError: If this and the B{C{other}} point's L{Datum}
65 ellipsoids are not compatible.
66 '''
67 r = self._Inverse(other, wrap)
68 return Bearing2Tuple(r.initial, r.final, name=self.name)
70 def destination(self, distance, bearing, height=None):
71 '''Compute the destination point after having travelled for
72 the given distance from this point along a geodesic given
73 by an initial bearing. See method L{destination2} for
74 further details.
76 @return: The destination point (C{LatLon}).
77 '''
78 return self._Direct(distance, bearing, self.classof, height).destination
80 def destination2(self, distance, bearing, height=None):
81 '''Compute the destination point and the final bearing (reverse
82 azimuth) after having travelled for the given distance from
83 this point along a geodesic (line) given by an initial bearing
84 at this point.
86 The distance must be in the same units as this point's datum's
87 ellipsoid's axes, conventionally C{meter}. The distance is
88 measured on the surface of the ellipsoid, ignoring this point's
89 height.
91 The initial and final bearing (forward and reverse azimuth)
92 are in compass C{degrees360}, clockwise from North.
94 The destination point's height and datum are set to this
95 point's height and datum, unless the former is overridden.
97 @arg distance: Distance (C{meter}).
98 @arg bearing: Initial bearing (compass C{degrees360}).
99 @kwarg height: Optional height, overriding the default
100 height (C{meter}, same units as C{distance}).
102 @return: A L{Destination2Tuple}C{(destination, final)}.
103 '''
104 return self._Direct(distance, bearing, self.classof, height)
106 def _Direct(self, distance, bearing, LL, height): # overloaded by I{Vincenty}
107 '''(INTERNAL) I{Karney}'s C{Direct} method.
109 @return: A L{Destination2Tuple}C{(destination, final)} or a
110 L{Destination3Tuple}C{(lat, lon, final)} if C{B{LL} is None}.
111 '''
112 g = self.geodesic
113 r = g.Direct3(self.lat, self.lon, bearing, distance)
114 if LL:
115 r = self._Direct2Tuple(LL, height, r)
116 return r
118 def _Direct2Tuple(self, LL, height, r): # in .ellipsoidalVincenty.py
119 '''(INTERNAL) Helper for C{._Direct} result L{Destination2Tuple}.
120 '''
121 h = self._heigHt(height)
122 n = self.name
123 d = _xkwds_not(None, datum=self.datum, height=h, name=n,
124 epoch=self.epoch, reframe=self.reframe)
125 d = LL(*_Wrap.latlon(r.lat, r.lon), **d)
126 return Destination2Tuple(d, wrap360(r.final), name=n)
128 def distanceTo(self, other, wrap=False, **unused): # radius=R_M
129 '''Compute the distance between this and an other point along
130 a geodesic. See method L{distanceTo3} for more details.
132 @arg other: The other point (this C{LatLon}).
133 @kwarg wrap: If C{True}, wrap or I{normalize} and unroll the
134 B{C{other}} point (C{bool}).
136 @return: Distance (C{meter}).
138 @raise TypeError: If B{C{other}} not this C{LatLon} class.
140 @raise ValueError: This and the B{C{other}} point's L{Datum}
141 ellipsoids are incompatible.
142 '''
143 return self._Inverse(other, wrap, azis=False).distance
145 def distanceTo3(self, other, wrap=False):
146 '''Compute the distance, the initial and final bearing along
147 a geodesic between this and an other point, using this
148 C{Inverse} method.
150 The distance is in the same units as this point's datum's
151 ellipsoid's axes, conventionally meter. The distance is
152 measured on the surface of the ellipsoid, ignoring this
153 point's height.
155 The initial and final bearing (forward and reverse azimuth)
156 are in compass C{degrees360}, clockwise from North.
158 @arg other: Destination point (C{LatLon}).
159 @kwarg wrap: If C{True}, wrap or I{normalize} and unroll the
160 B{C{other}} point (C{bool}).
162 @return: A L{Distance3Tuple}C{(distance, initial, final)}.
164 @raise TypeError: If B{C{other}} not this C{LatLon} class.
166 @raise ValueError: This and the B{C{other}} point's L{Datum}
167 ellipsoids are not compatible.
168 '''
169 return self._xnamed(self._Inverse(other, wrap))
171 def finalBearingOn(self, distance, bearing):
172 '''Compute the final bearing (reverse azimuth) after having
173 travelled for the given distance along a geodesic given
174 by an initial bearing from this point. See method
175 L{destination2} for more details.
177 @arg distance: Distance (C{meter}).
178 @arg bearing: Initial bearing (compass C{degrees360}).
180 @return: Final bearing (compass C{degrees360}).
181 '''
182 return self._Direct(distance, bearing, None, None).final
184 def finalBearingTo(self, other, wrap=False):
185 '''Compute the final bearing (reverse azimuth) after having
186 travelled along a geodesic from this point to an other
187 point. See method L{distanceTo3} for more details.
189 @arg other: The other point (C{LatLon}).
190 @kwarg wrap: If C{True}, wrap or I{normalize} and unroll
191 the B{C{other}} point (C{bool}).
193 @return: Final bearing (compass C{degrees360}).
195 @raise TypeError: If B{C{other}} not this C{LatLon} class.
197 @raise ValueError: This and the B{C{other}} point's L{Datum}
198 ellipsoids are incompatible.
199 '''
200 return self._Inverse(other, wrap).final
202 @property_RO
203 def geodesic(self): # overloaded by I{Karney}'s, N/A for I{Vincenty}
204 '''N/A, invalid (C{None} I{always}).
205 '''
206 return None # PYCHOK no cover
208 def _g_gl_p3(self, start, end, exact, wrap):
209 '''(INTERNAL) Helper for methods C{.intersecant2} and C{.plumbTo}.
210 '''
211 p = _unrollon(self, self.others(start=start), wrap=wrap)
212 g = self.datum.ellipsoid.geodesic_(exact=exact)
213 gl = g._DirectLine( p, end) if _isDegrees(end) else \
214 g._InverseLine(p, self.others(end=end), wrap)
215 return g, gl, p
217 def initialBearingTo(self, other, wrap=False):
218 '''Compute the initial bearing (forward azimuth) to travel
219 along a geodesic from this point to an other point. See
220 method L{distanceTo3} for more details.
222 @arg other: The other point (this C{LatLon}).
223 @kwarg wrap: If C{True}, wrap or I{normalize} and unroll
224 the B{C{other}} point (C{bool}).
226 @return: Initial bearing (compass C{degrees360}).
228 @raise TypeError: If B{C{other}} not this C{LatLon} class.
230 @raise ValueError: If this and the B{C{other}} point's L{Datum}
231 ellipsoids are incompatible.
232 '''
233 return self._Inverse(other, wrap).initial
235 def intermediateTo(self, other, fraction, height=None, wrap=False):
236 '''Return the point at given fraction along the geodesic between
237 this and an other point.
239 @arg other: The other point (this C{LatLon}).
240 @arg fraction: Fraction between both points (C{scalar}, 0.0
241 at this and 1.0 at the other point.
242 @kwarg height: Optional height, overriding the fractional
243 height (C{meter}).
244 @kwarg wrap: If C{True}, wrap or I{normalize} and unroll the
245 B{C{other}} point (C{bool}).
247 @return: Intermediate point (C{LatLon}).
249 @raise TypeError: If B{C{other}} not this C{LatLon} class.
251 @raise UnitError: Invalid B{C{fraction}} or B{C{height}}.
253 @raise ValueError: This and the B{C{other}} point's L{Datum}
254 ellipsoids are incompatible.
256 @see: Methods L{distanceTo3}, L{destination}, C{midpointTo} and
257 C{rhumbMidpointTo}.
258 '''
259 f = Scalar(fraction=fraction)
260 if isnear0(f):
261 r = self
262 elif isnear1(f) and not wrap:
263 r = self.others(other)
264 else: # negative fraction OK
265 t = self.distanceTo3(other, wrap=wrap)
266 h = self._havg(other, f=f, h=height)
267 r = self.destination(t.distance * f, t.initial, height=h)
268 return r
270 def intersecant2(self, circle, start, end, exact=False, height=None, # PYCHOK signature
271 wrap=False, tol=_TOL):
272 '''Compute the intersections of a circle and a geodesic (line) given as two
273 points or as a point and a bearing from North.
275 @arg circle: Radius of the circle centered at this location (C{meter},
276 conventionally) or a point on the circle (this C{LatLon}).
277 @arg start: Start point of the geodesic (line) (this C{LatLon}).
278 @arg end: End point of the geodesic (line) (this C{LatLon}) or the initial
279 bearing at the B{C{start}} point (compass C{degrees360}).
280 @kwarg exact: Exact C{geodesic...} to use (C{bool} or C{Geodesic...}), see
281 method L{geodesic_<Ellipsoid.geodesic_>}.
282 @kwarg height: Optional height for the intersection points (C{meter},
283 conventionally) or C{None} for interpolated heights.
284 @kwarg wrap: If C{True}, wrap or I{normalize} and unroll the B{C{circle}},
285 B{C{start}} and/or B{C{end}} (C{bool}).
286 @kwarg tol: Convergence tolerance (C{scalar}).
288 @return: 2-Tuple of the intersection points (representing a geodesic chord),
289 each an instance of this C{LatLon} class. Both points are the same
290 instance if the geodesic (line) is tangential to the circle.
292 @raise IntersectionError: The circle and geodesic do not intersect.
294 @raise TypeError: Invalid B{C{circle}}, B{C{start}} or B{C{end}}.
296 @raise UnitError: Invalid B{C{circle}}, B{C{end}}, B{C{exact}} or B{C{height}}.
298 @see: Method L{rhumbIntersecant2<LatLonBase.rhumbIntersecant2>}.
299 '''
300 try:
301 g, gl, p = self._g_gl_p3(start, end, exact, wrap)
302 r = Radius_(circle=circle) if _isRadius(circle) else \
303 g._Inverse(self, self.others(circle=circle), wrap).s12
305 P, Q = _MODS.geodesicw._Intersecant2(gl, self.lat, self.lon, r, tol=tol,
306 form=_MODS.dms.F_DMS)
307 return self._intersecend2(p, end, wrap, height, g, P, Q,
308 self.intersecant2)
309 except (TypeError, ValueError) as x:
310 raise _xError(x, center=self, circle=circle, start=start, end=end,
311 exact=exact, wrap=wrap)
313 def _Inverse(self, other, wrap, **unused): # azis=False, overloaded by I{Vincenty}
314 '''(INTERNAL) I{Karney}'s C{Inverse} method.
316 @return: A L{Distance3Tuple}C{(distance, initial, final)}.
317 '''
318 _ = self.ellipsoids(other)
319 g = self.geodesic
320 _, lon = unroll180(self.lon, other.lon, wrap=wrap)
321 return g.Inverse3(self.lat, self.lon, other.lat, lon)
323 def nearestOn8(self, points, closed=False, height=None, wrap=False,
324 equidistant=None, tol=_TOL_M):
325 '''I{Iteratively} locate the point on a path or polygon closest
326 to this point.
328 @arg points: The path or polygon points (C{LatLon}[]).
329 @kwarg closed: Optionally, close the polygon (C{bool}).
330 @kwarg height: Optional height, overriding the height of this and all
331 other points (C{meter}, conventionally). If C{B{height}
332 is None}, each point's height is taken into account to
333 compute distances.
334 @kwarg wrap: If C{True}, wrap or I{normalize} and unroll the B{C{points}}
335 (C{bool}).
336 @kwarg equidistant: An azimuthal equidistant projection (I{class} or function
337 L{pygeodesy.equidistant}) or C{None} for the preferred
338 L{Equidistant<pygeodesy.ellipsoidalBase.Equidistant>}.
339 @kwarg tol: Convergence tolerance (C{meter}, conventionally).
341 @return: A L{NearestOn8Tuple}C{(closest, distance, fi, j, start, end,
342 initial, final)} with C{distance} in C{meter}, conventionally
343 and with the C{closest}, the C{start} the C{end} point each
344 an instance of this C{LatLon} class.
346 @raise PointsError: Insufficient number of B{C{points}}.
348 @raise TypeError: Some B{C{points}} or B{C{equidistant}} invalid.
350 @raise ValueError: Some B{C{points}}' datum or ellipsoid incompatible
351 or no convergence for the given B{C{tol}}.
353 @see: Function L{pygeodesy.nearestOn6} and method C{nearestOn6}.
354 '''
355 _d3 = self.distanceTo3 # Distance3Tuple
356 _n3 = _nearestOn3
357 try:
358 Ps = self.PointsIter(points, loop=1, wrap=wrap)
359 p1 = c = s = e = Ps[0]
360 _ = self.ellipsoids(p1)
361 c3 = _d3(c, wrap=wrap) # XXX wrap=False?
363 except (TypeError, ValueError) as x:
364 raise _xError(x, Fmt.INDEX(points=0), p1, this=self, tol=tol,
365 closed=closed, height=height, wrap=wrap)
367 # get the azimuthal equidistant projection, once
368 A = _Equidistant00(equidistant, c)
369 b = _Box(c, c3.distance)
370 m = f = i = 0 # p1..p2 == points[i]..[j]
372 kwds = dict(within=True, height=height, tol=tol,
373 LatLon=self.classof, # this LatLon
374 datum=self.datum, epoch=self.epoch, reframe=self.reframe)
375 try:
376 for j, p2 in Ps.enumerate(closed=closed):
377 if wrap and j != 0: # not Ps.looped
378 p2 = _unrollon(p1, p2)
379 # skip edge if no overlap with box around closest
380 if j < 4 or b.overlaps(p1.lat, p1.lon, p2.lat, p2.lon):
381 p, t, _ = _n3(self, p1, p2, A, **kwds)
382 d3 = _d3(p, wrap=False) # already unrolled
383 if d3.distance < c3.distance:
384 c3, c, s, e, f = d3, p, p1, p2, (i + t)
385 b = _Box(c, c3.distance)
386 m = max(m, c.iteration)
387 p1, i = p2, j
389 except (TypeError, ValueError) as x:
390 raise _xError(x, Fmt.INDEX(points=i), p1,
391 Fmt.INDEX(points=j), p2, this=self, tol=tol,
392 closed=closed, height=height, wrap=wrap)
394 f, j = _fi_j2(f, len(Ps)) # like .vector3d.nearestOn6
396 n = typename(self)
397 c.rename(n)
398 if s is not c:
399 s = s.copy(name=n)
400 if e is not c:
401 e = e.copy(name=n) # name__=self.nearestOn8
402 return NearestOn8Tuple(c, c3.distance, f, j, s, e, c3.initial, c3.final,
403 iteration=m) # ._iteration for tests
405 def plumbTo(self, start, end, exact=False, height=None, # PYCHOK signature
406 wrap=False, tol=_TOL):
407 '''Compute the intersection of a geodesic from this point I{perpendicular} to
408 a geodesic (line) given as two points or as a point and a bearing from North.
410 @arg start: Start point of the geodesic (line) (this C{LatLon}).
411 @arg end: End point of the geodesic (line) (this C{LatLon}) or the initial
412 bearing at the B{C{start}} point (compass C{degrees360}).
413 @kwarg exact: Exact C{geodesic...} to use (C{bool} or C{Geodesic...}),
414 see method L{Ellipsoid.geodesic_}.
415 @kwarg height: Optional height for the intersection point (C{meter},
416 conventionally) or C{None} for an interpolated height.
417 @kwarg wrap: If C{True}, wrap or I{normalize} and unroll the B{C{start}}
418 and/or B{C{end}} point (C{bool}).
419 @kwarg tol: Convergence tolerance (C{meter}).
421 @return: The intersection point, an instance of this C{LatLon} class.
423 @raise TypeError: If B{C{start}} or B{C{end}} not this C{LatLon} class.
425 @raise UnitError: Invalid B{C{end}}, B{C{exact}} or B{C{height}}.
426 '''
427 try:
428 g, gl, p = self._g_gl_p3(start, end, exact, wrap)
430 P = _MODS.geodesicw._PlumbTo(gl, self.lat, self.lon, tol=tol)
431 h = self._havg(p, h=height)
432 p = self.classof(P.lat2, P.lon2, datum=self.datum, height=h) # name=n
433 p._iteration = P.iteration
434 except (TypeError, ValueError) as x:
435 raise _xError(x, plumb=self, start=start, end=end,
436 exact=exact, wrap=wrap)
437 return p
440class _Box(object):
441 '''Bounding box around a C{LatLon} point.
443 @see: Function C{_box4} in .clipy.py.
444 '''
445 _1_01 = _1_0 + _0_01 # ~1% margin
447 def __init__(self, center, distance):
448 '''New L{_Box} around point.
450 @arg center: The center point (C{LatLon}).
451 @arg distance: Radius, half-size of the box
452 (C{meter}, conventionally)
453 '''
454 m = Radius_(distance=distance)
455 E = center.ellipsoid()
456 d = E.m2degrees(m) * self._1_01
457 self._N = center.lat + d
458 self._S = center.lat - d
459 self._E = center.lon + d
460 self._W = center.lon - d
462 def overlaps(self, lat1, lon1, lat2, lon2):
463 '''Check whether this box overlaps an other box.
465 @arg lat1: Latitude of a box corner (C{degrees}).
466 @arg lon1: Longitude of a box corner (C{degrees}).
467 @arg lat2: Latitude of the opposing corner (C{degrees}).
468 @arg lon2: Longitude of the opposing corner (C{degrees}).
470 @return: C{True} if there is some overlap, C{False}
471 otherwise (C{bool}).
472 '''
473 if lat1 > lat2:
474 lat1, lat2 = lat2, lat1
475 if lat1 > self._N or lat2 < self._S:
476 return False
477 if lon1 > lon2:
478 lon1, lon2 = lon2, lon1
479 if lon1 > self._E or lon2 < self._W:
480 return False
481 return True
484class _Tol(object):
485 '''Handle a tolerance in C{meter} as C{degrees} and C{meter}.
486 '''
487 _deg = 0 # tol in degrees
488 _lat = 0
489 _m = 0 # tol in meter
490 _min = MAX # degrees
491 _prev = None
492 _r = 0
494 def __init__(self, tol_m, E, lat, *lats):
495 '''New L{_Tol}.
497 @arg tol_m: Tolerance (C{meter}, only).
498 @arg E: Earth ellipsoid (L{Ellipsoid}).
499 @arg lat: Latitude (C{degrees}).
500 @arg lats: Additional latitudes (C{degrees}).
501 '''
502 self._lat = fmean_(lat, *lats) if lats else lat
503 self._r = max(EPS, E.rocMean(self._lat))
504 self._m = max(EPS, tol_m)
505 self._deg = max(EPS, degrees(self._m / self._r)) # NOT E.m2degrees!
507 @property_RO
508 def degrees(self):
509 '''Get this tolerance in C{degrees}.
510 '''
511 return self._deg
513 def degrees2m(self, deg):
514 '''Convert B{C{deg}} to meter at the same C{lat} and earth radius.
515 '''
516 return self.radius * radians(deg) / PI2 # NOT E.degrees2m!
518 def degError(self, Error=_ValueError):
519 '''Compose an error with the C{deg}rees minimum.
520 '''
521 return self.mError(self.degrees2m(self._min), Error=Error)
523 def done(self, deg):
524 '''Check C{deg} vs tolerance and previous value.
525 '''
526 if deg < self._deg or deg == self._prev:
527 return True
528 self._min = min(self._min, deg)
529 self._prev = deg
530 return False
532 @property_RO
533 def lat(self):
534 '''Get the mean latitude in C{degrees}.
535 '''
536 return self._lat
538 def mError(self, m, Error=_ValueError):
539 '''Compose an error with B{C{m}}eter minimum.
540 '''
541 t = _SPACE_(Fmt.tolerance(self.meter), _too_(_low_))
542 if m2km(m) > self.meter:
543 t = _or(t, _antipodal_, _near_(_polar__))
544 return Error(Fmt.no_convergence(m), txt=t)
546 @property_RO
547 def meter(self):
548 '''Get this tolerance in C{meter}.
549 '''
550 return self._m
552 @property_RO
553 def radius(self):
554 '''Get the earth radius in C{meter}.
555 '''
556 return self._r
558 def reset(self):
559 '''Reset tolerances.
560 '''
561 self._min = MAX # delattrof()
562 self._prev = None # delattrof()
565def _Equidistant00(equidistant, p1):
566 '''(INTERNAL) Get an C{Equidistant*(0, 0, ...)} instance.
567 '''
568 if equidistant is None or not callable(equidistant):
569 equidistant = p1.Equidistant
570 else:
571 _xsubclassof(*_MODS.azimuthal._Equidistants,
572 equidistant=equidistant)
573 return equidistant(0, 0, p1.datum)
576def intersecant2(center, circle, point, other, **exact_height_wrap_tol):
577 '''Compute the intersections of a circle and a geodesic given as two points
578 or as a point and (forward) bearing.
580 @arg center: Center of the circle (C{LatLon}).
581 @arg circle: Radius of the circle (C{meter}, conventionally) or a point on
582 the circle (C{LatLon}, as B{C{center}}).
583 @arg point: A point of the geodesic (C{LatLon}, as B{C{center}}).
584 @arg other: An other point of the geodesic (C{LatLon}, as B{C{center}}) or
585 the (forward) bearing at the B{C{point}} (compass C{degrees}).
586 @kwarg exact_height_wrap_tol: Optional keyword arguments C{B{exact}=False},
587 C{B{height}=None}, C{B{wrap}=False} and C{B{tol}}, see method
588 L{intersecant2<LatLonEllipsoidalBaseDI.intersecant2>}.
590 @raise NotImplementedError: Method C{intersecant2} not available.
592 @raise TypeError: If B{C{center}}, B{C{point}} or B{C{circle}} or B{C{other}}
593 points not ellipsoidal or not compatible with B{C{center}}.
595 @see: Method C{LatLon.intersecant2} of class L{ellipsoidalExact.LatLon},
596 L{ellipsoidalKarney.LatLon} or L{ellipsoidalVincenty.LatLon}.
597 '''
598 if not isLatLon(center, ellipsoidal=True): # isinstance(center, LatLonEllipsoidalBase)
599 raise _IsnotError(_ellipsoidal_, center=center)
600 return center.intersecant2(circle, point, other, **exact_height_wrap_tol)
603def _intersect3(s1, end1, s2, end2, height=None, wrap=False, # MCCABE 16 was=True
604 equidistant=None, tol=_TOL_M, LatLon=None, **LatLon_kwds):
605 '''(INTERNAL) Intersect two (ellipsoidal) lines, see ellipsoidal method
606 L{intersection3}, separated to allow callers to embellish any exceptions.
607 '''
608 _LLS = _MODS.sphericalTrigonometry.LatLon
609 _si = _MODS.sphericalTrigonometry._intersect
610 _vi3 = _MODS.vector3d._intersect3d3
612 def _b_d(s, e, w, t, h=_0_0):
613 # compute opposing and distance
614 t = s.classof(t.lat, t.lon, height=h, name=t.name)
615 t = s.distanceTo3(t, wrap=w) # Distance3Tuple
616 b = opposing(e, t.initial) # "before" start
617 return b, t.distance
619 def _b_e(s, e, w, t):
620 # compute an end point along the initial bearing about
621 # 1.5 times the distance to the gu-/estimate, at least
622 # 1/8 and at most 3/8 of the earth perimeter like the
623 # radians in .sphericalTrigonometry._int3d2 and bearing
624 # comparison in .sphericaltrigonometry._intb
625 b, d = _b_d(s, e, w, t, h=t.height)
626 m = s.ellipsoid().R2x * PI_4 # authalic exact
627 d = min(max(d * _1_5, m), m * _3_0)
628 e = s.destination(d, e)
629 return b, (_unrollon(s, e) if w else e)
631 def _e_ll(s, e, w, **end):
632 # return 2-tuple (end, False if bearing else True)
633 ll = not _isDegrees(e)
634 if ll:
635 e = s.others(**end)
636 if w: # unroll180 == .karney._unroll2
637 e = _unrollon(s, e)
638 return e, ll
640 def _o(o, b, n, s, t, e):
641 # determine C{o}utside before, on or after start point
642 if not o: # intersection may be on start
643 if _isequalTo(s, t, eps=e.degrees):
644 return o
645 return -n if b else n
647 E = s1.ellipsoids(s2)
649 e1, ll1 = _e_ll(s1, end1, wrap, end1=end1)
650 e2, ll2 = _e_ll(s2, end2, wrap, end2=end2)
652 e = _Tol(tol, E, s1.lat, (e1.lat if ll1 else s1.lat),
653 s2.lat, (e2.lat if ll2 else s2.lat))
655 # get the azimuthal equidistant projection
656 A = _Equidistant00(equidistant, s1)
658 # gu-/estimate initial intersection, spherically ...
659 t = _si(_LLS(s1), (_LLS(e1) if ll1 else e1),
660 _LLS(s2), (_LLS(e2) if ll2 else e2),
661 height=height, wrap=False, LatLon=_LLS) # unrolled already
662 h, n = t.height, t.name
664 if not ll1:
665 b1, e1 = _b_e(s1, e1, wrap, t)
666 if not ll2:
667 b2, e2 = _b_e(s2, e2, wrap, t)
669 # ... and iterate as Karney describes, for references
670 # @see: Function L{ellipsoidalKarney.intersection3}.
671 for i in range(1, _TRIPS):
672 A.reset(t.lat, t.lon) # gu-/estimate as origin
673 # convert start and end points to projection
674 # space and compute an intersection there
675 v, o1, o2 = _vi3(*A._forwards(s1, e1, s2, e2),
676 eps=e.meter, useZ=False)
677 # convert intersection back to geodetic
678 t, d = A._reverse2(v)
679 if e.done(d): # below tol or unchanged?
680 break
681 else:
682 raise e.degError(Error=IntersectionError)
684 # like .sphericalTrigonometry._intersect, if this intersection
685 # is "before" the first point, use the antipodal intersection
686 if not (ll1 or ll2): # end1 and end2 are an initial bearing
687 b1, _ = _b_d(s1, end1, wrap, t)
688 if b1:
689 t = t.antipodal()
690 b1 = not b1
691 b2, _ = _b_d(s2, end2, wrap, t)
693 r = _LL4Tuple(t.lat, t.lon, h, t.datum, LatLon, LatLon_kwds, inst=s1,
694 iteration=i, name=n)
695 return Intersection3Tuple(r, (o1 if ll1 else _o(o1, b1, 1, s1, t, e)),
696 (o2 if ll2 else _o(o2, b2, 2, s2, t, e)))
699def _intersection3(start1, end1, start2, end2, height=None, wrap=False, # was=True
700 **equidistant_tol_LatLon_and_kwds):
701 '''(INTERNAL) Iteratively compute the intersection point of two lines,
702 each defined by two (ellipsoidal) points or an (ellipsoidal) start
703 point and an initial bearing from North.
704 '''
705 s1 = _xellipsoidal(start1=start1)
706 s2 = s1.others(start2=start2)
707 try:
708 return _intersect3(s1, end1, s2, end2, height=height, wrap=wrap,
709 **equidistant_tol_LatLon_and_kwds)
710 except (TypeError, ValueError) as x:
711 raise _xError(x, start1=start1, end1=end1, start2=start2, end2=end2)
714def _intersections2(center1, radius1, center2, radius2, height=None, wrap=False, # was=True
715 **equidistant_tol_LatLon_and_kwds):
716 '''(INTERNAL) Iteratively compute the intersection points of two circles,
717 each defined by an (ellipsoidal) center point and a radius.
718 '''
719 c1 = _xellipsoidal(center1=center1)
720 c2 = c1.others(center2=center2)
721 try:
722 return _intersects2(c1, radius1, c2, radius2, height=height, wrap=wrap,
723 **equidistant_tol_LatLon_and_kwds)
724 except (TypeError, ValueError) as x:
725 raise _xError(x, center1=center1, radius1=radius1,
726 center2=center2, radius2=radius2)
729def _intersects2(c1, radius1, c2, radius2, height=None, wrap=False, # MCCABE 16 was=True
730 equidistant=None, tol=_TOL_M, LatLon=None, **LatLon_kwds):
731 '''(INTERNAL) Intersect two (ellipsoidal) circles, see L{_intersections2}
732 above, separated to allow callers to embellish any exceptions.
733 '''
734 _LLS = _MODS.sphericalTrigonometry.LatLon
735 _si2 = _MODS.sphericalTrigonometry._intersects2
736 _vi2 = _MODS.vector3d._intersects2
738 def _ll4(t, h, n, c):
739 return _LL4Tuple(t.lat, t.lon, h, t.datum, LatLon, LatLon_kwds, inst=c,
740 iteration=t.iteration, name=n)
742 r1 = Radius_(radius1=radius1)
743 r2 = Radius_(radius2=radius2)
745 E = c1.ellipsoids(c2)
746 # get the azimuthal equidistant projection
747 A = _Equidistant00(equidistant, c1)
749 if r1 < r2:
750 c1, c2 = c2, c1
751 r1, r2 = r2, r1
753 if r1 > (min(E.b, E.a) * PI):
754 raise _ValueError(_exceed_PI_radians_)
756 if wrap: # unroll180 == .karney._unroll2
757 c2 = _unrollon(c1, c2)
759 # distance between centers and radii are
760 # measured along the ellipsoid's surface
761 m = c1.distanceTo(c2, wrap=False) # meter
762 if m < max(r1 - r2, EPS):
763 raise IntersectionError(_near_(_concentric_)) # XXX ConcentricError?
764 if fsumf_(r1, r2, -m) < 0:
765 raise IntersectionError(_too_(Fmt.distant(m)))
767 f = _radical2(m, r1, r2).ratio # "radical fraction"
768 e = _Tol(tol, E, favg(c1.lat, c2.lat, f=f))
770 # gu-/estimate initial intersections, spherically ...
771 t1, t2 = _si2(_LLS(c1), r1, _LLS(c2), r2, radius=e.radius,
772 height=height, too_d=m, wrap=False) # unrolled already
773 h, n = t1.height, t1.name
775 # ... and iterate as Karney describes, for references
776 # @see: Function L{ellipsoidalKarney.intersections2}.
777 ts, ta = [], None
778 for t in ((t1,) if t1 is t2 else (t1, t2)):
779 for i in range(1, _TRIPS):
780 A.reset(t.lat, t.lon) # gu-/estimate as origin
781 # convert centers to projection space and
782 # compute the intersections there
783 t1, t2 = A._forwards(c1, c2)
784 v1, v2 = _vi2(t1, r1, # XXX * t1.scale?,
785 t2, r2, # XXX * t2.scale?,
786 sphere=False, too_d=m)
787 # convert intersections back to geodetic
788 if v1 is v2: # abutting
789 t, d = A._reverse2(v1)
790 else: # consider the closer intersection
791 t1, d1 = A._reverse2(v1)
792 t2, d2 = A._reverse2(v2)
793 t, d = (t1, d1) if d1 < d2 else (t2, d2)
794 if e.done(d): # below tol or unchanged?
795 t._iteration = i # _NamedTuple._iteration
796 ts.append(t)
797 if v1 is v2: # abutting
798 ta = t
799 break
800 else:
801 raise e.degError(Error=IntersectionError)
802 e.reset()
804 if ta: # abutting circles
805 pass # PYCHOK no cover
806 elif len(ts) == 2:
807 return (_ll4(ts[0], h, n, c1),
808 _ll4(ts[1], h, n, c2))
809 elif len(ts) == 1: # PYCHOK no cover
810 ta = ts[0] # assume abutting
811 else: # PYCHOK no cover
812 raise _AssertionError(ts=ts)
813 r = _ll4(ta, h, n, c1)
814 return r, r
817def _nearestOn2(p, point1, point2, within=True, height=None, wrap=False, # was=True
818 equidistant=None, tol=_TOL_M, **LatLon_and_kwds):
819 '''(INTERNAL) Closest point and fraction, like L{_intersects2} above,
820 separated to allow callers to embellish any exceptions.
821 '''
822 p1 = p.others(point1=point1)
823 p2 = p.others(point2=point2)
825 _ = p.ellipsoids(p1)
826# E = p.ellipsoids(p2) # done in _nearestOn3
828 # get the azimuthal equidistant projection
829 A = _Equidistant00(equidistant, p)
831 p1, p2, _ = _unrollon3(p, p1, p2, wrap) # XXX don't unroll?
832 r, f, _ = _nearestOn3(p, p1, p2, A, within=within, height=height,
833 tol=tol, **LatLon_and_kwds)
834 return NearestOn2Tuple(r, f)
837def _nearestOn3(p, p1, p2, A, within=True, height=None, tol=_TOL_M,
838 LatLon=None, **LatLon_kwds):
839 # Only in function C{_nearestOn2} and method C{nearestOn8} above
840 _LLS = _MODS.sphericalNvector.LatLon
841 _V3d = _MODS.vector3d.Vector3d
842 _vn2 = _MODS.vector3d._nearestOn2
844 E = p.ellipsoids(p2)
845 e = _Tol(tol, E, p.lat, p1.lat, p2.lat)
847 # gu-/estimate initial nearestOn, spherically ... wrap=False, only!
848 # using sphericalNvector.LatLon.nearestOn for within=False support
849 t = _LLS(p).nearestOn(_LLS(p1), _LLS(p2), within=within,
850 height=height)
851 n, h = t.name, t.height
852 if height is None:
853 h1 = p1.height # use heights as pseudo-Z in projection space
854 h2 = p2.height # to be included in the closest function
855 h0 = favg(h1, h2)
856 else: # ignore heights in distances, Z=0
857 h0 = h1 = h2 = _0_0
859 # ... and iterate to find the closest (to the origin with .z
860 # to interpolate height) as Karney describes, for references
861 # @see: Function L{ellipsoidalKarney.nearestOn}.
862 vp, f = _V3d(_0_0, _0_0, h0), None
863 for i in range(1, _TRIPS):
864 A.reset(t.lat, t.lon) # gu-/estimate as origin
865 # convert points to projection space and compute
866 # the nearest one (and its height) there
867 s, t = A._forwards(p1, p2)
868 v, f = _vn2(vp, _V3d(s.x, s.y, h1),
869 _V3d(t.x, t.y, h2), within=within)
870 # convert nearest one back to geodetic
871 t, d = A._reverse2(v)
872 if e.done(d): # below tol or unchanged?
873 break
874 else:
875 raise e.degError()
877 if height is None:
878 h = v.z # nearest
879 elif _isHeight(height):
880 h = height
881 r = _LL4Tuple(t.lat, t.lon, h, t.datum, LatLon, LatLon_kwds, inst=p,
882 iteration=i, name=n)
883 return r, f, e # fraction or None
886__all__ += _ALL_DOCS(LatLonEllipsoidalBaseDI, intersecant2)
887del _1_0, _0_01
889# **) MIT License
890#
891# Copyright (C) 2016-2025 -- mrJean1 at Gmail -- All Rights Reserved.
892#
893# Permission is hereby granted, free of charge, to any person obtaining a
894# copy of this software and associated documentation files (the "Software"),
895# to deal in the Software without restriction, including without limitation
896# the rights to use, copy, modify, merge, publish, distribute, sublicense,
897# and/or sell copies of the Software, and to permit persons to whom the
898# Software is furnished to do so, subject to the following conditions:
899#
900# The above copyright notice and this permission notice shall be included
901# in all copies or substantial portions of the Software.
902#
903# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
904# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
905# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
906# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
907# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
908# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
909# OTHER DEALINGS IN THE SOFTWARE.