Coverage for pygeodesy/ellipsoidalBaseDI.py: 91%
330 statements
« prev ^ index » next coverage.py v7.6.1, created at 2025-05-04 12:01 -0400
« prev ^ index » next coverage.py v7.6.1, created at 2025-05-04 12:01 -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 _ # PYCHOK semicolon
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.04.21'
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):
119 '''(INTERNAL) Helper for C{._Direct} result L{Destination2Tuple}.
120 '''
121 h = self._heigHt(height)
122 d = _xkwds_not(None, datum=self.datum, name=self.name,
123 epoch=self.epoch, reframe=self.reframe)
124 d = LL(*_Wrap.latlon(r.lat, r.lon), height=h, **d)
125 return Destination2Tuple(d, wrap360(r.final), name=self.name)
127 def distanceTo(self, other, wrap=False, **unused): # radius=R_M
128 '''Compute the distance between this and an other point along
129 a geodesic. See method L{distanceTo3} for more details.
131 @arg other: The other point (this C{LatLon}).
132 @kwarg wrap: If C{True}, wrap or I{normalize} and unroll the
133 B{C{other}} point (C{bool}).
135 @return: Distance (C{meter}).
137 @raise TypeError: If B{C{other}} not this C{LatLon} class.
139 @raise ValueError: This and the B{C{other}} point's L{Datum}
140 ellipsoids are incompatible.
141 '''
142 return self._Inverse(other, wrap, azis=False).distance
144 def distanceTo3(self, other, wrap=False):
145 '''Compute the distance, the initial and final bearing along
146 a geodesic between this and an other point, using this
147 C{Inverse} method.
149 The distance is in the same units as this point's datum's
150 ellipsoid's axes, conventionally meter. The distance is
151 measured on the surface of the ellipsoid, ignoring this
152 point's height.
154 The initial and final bearing (forward and reverse azimuth)
155 are in compass C{degrees360}, clockwise from North.
157 @arg other: Destination point (C{LatLon}).
158 @kwarg wrap: If C{True}, wrap or I{normalize} and unroll the
159 B{C{other}} point (C{bool}).
161 @return: A L{Distance3Tuple}C{(distance, initial, final)}.
163 @raise TypeError: If B{C{other}} not this C{LatLon} class.
165 @raise ValueError: This and the B{C{other}} point's L{Datum}
166 ellipsoids are not compatible.
167 '''
168 return self._xnamed(self._Inverse(other, wrap))
170 def finalBearingOn(self, distance, bearing):
171 '''Compute the final bearing (reverse azimuth) after having
172 travelled for the given distance along a geodesic given
173 by an initial bearing from this point. See method
174 L{destination2} for more details.
176 @arg distance: Distance (C{meter}).
177 @arg bearing: Initial bearing (compass C{degrees360}).
179 @return: Final bearing (compass C{degrees360}).
180 '''
181 return self._Direct(distance, bearing, None, None).final
183 def finalBearingTo(self, other, wrap=False):
184 '''Compute the final bearing (reverse azimuth) after having
185 travelled along a geodesic from this point to an other
186 point. See method L{distanceTo3} for more details.
188 @arg other: The other point (C{LatLon}).
189 @kwarg wrap: If C{True}, wrap or I{normalize} and unroll
190 the B{C{other}} point (C{bool}).
192 @return: Final bearing (compass C{degrees360}).
194 @raise TypeError: If B{C{other}} not this C{LatLon} class.
196 @raise ValueError: This and the B{C{other}} point's L{Datum}
197 ellipsoids are incompatible.
198 '''
199 return self._Inverse(other, wrap).final
201 @property_RO
202 def geodesic(self): # overloaded by I{Karney}'s, N/A for I{Vincenty}
203 '''N/A, invalid (C{None} I{always}).
204 '''
205 return None # PYCHOK no cover
207 def _g_gl_p3(self, start, end, exact, wrap):
208 '''(INTERNAL) Helper for methods C{.intersecant2} and C{.plumbTo}.
209 '''
210 p = _unrollon(self, self.others(start=start), wrap=wrap)
211 g = self.datum.ellipsoid.geodesic_(exact=exact)
212 gl = g._DirectLine( p, end) if _isDegrees(end) else \
213 g._InverseLine(p, self.others(end=end), wrap)
214 return g, gl, p
216 def initialBearingTo(self, other, wrap=False):
217 '''Compute the initial bearing (forward azimuth) to travel
218 along a geodesic from this point to an other point. See
219 method L{distanceTo3} for more details.
221 @arg other: The other point (this C{LatLon}).
222 @kwarg wrap: If C{True}, wrap or I{normalize} and unroll
223 the B{C{other}} point (C{bool}).
225 @return: Initial bearing (compass C{degrees360}).
227 @raise TypeError: If B{C{other}} not this C{LatLon} class.
229 @raise ValueError: If this and the B{C{other}} point's L{Datum}
230 ellipsoids are incompatible.
231 '''
232 return self._Inverse(other, wrap).initial
234 def intermediateTo(self, other, fraction, height=None, wrap=False):
235 '''Return the point at given fraction along the geodesic between
236 this and an other point.
238 @arg other: The other point (this C{LatLon}).
239 @arg fraction: Fraction between both points (C{scalar}, 0.0
240 at this and 1.0 at the other point.
241 @kwarg height: Optional height, overriding the fractional
242 height (C{meter}).
243 @kwarg wrap: If C{True}, wrap or I{normalize} and unroll the
244 B{C{other}} point (C{bool}).
246 @return: Intermediate point (C{LatLon}).
248 @raise TypeError: If B{C{other}} not this C{LatLon} class.
250 @raise UnitError: Invalid B{C{fraction}} or B{C{height}}.
252 @raise ValueError: This and the B{C{other}} point's L{Datum}
253 ellipsoids are incompatible.
255 @see: Methods L{distanceTo3}, L{destination}, C{midpointTo} and
256 C{rhumbMidpointTo}.
257 '''
258 f = Scalar(fraction=fraction)
259 if isnear0(f):
260 r = self
261 elif isnear1(f) and not wrap:
262 r = self.others(other)
263 else: # negative fraction OK
264 t = self.distanceTo3(other, wrap=wrap)
265 h = self._havg(other, f=f, h=height)
266 r = self.destination(t.distance * f, t.initial, height=h)
267 return r
269 def intersecant2(self, circle, start, end, exact=False, height=None, # PYCHOK signature
270 wrap=False, tol=_TOL):
271 '''Compute the intersections of a circle and a geodesic (line) given as two
272 points or as a point and a bearing from North.
274 @arg circle: Radius of the circle centered at this location (C{meter},
275 conventionally) or a point on the circle (this C{LatLon}).
276 @arg start: Start point of the geodesic (line) (this C{LatLon}).
277 @arg end: End point of the geodesic (line) (this C{LatLon}) or the initial
278 bearing at the B{C{start}} point (compass C{degrees360}).
279 @kwarg exact: Exact C{geodesic...} to use (C{bool} or C{Geodesic...}), see
280 method L{geodesic_<Ellipsoid.geodesic_>}.
281 @kwarg height: Optional height for the intersection points (C{meter},
282 conventionally) or C{None} for interpolated heights.
283 @kwarg wrap: If C{True}, wrap or I{normalize} and unroll the B{C{circle}},
284 B{C{start}} and/or B{C{end}} (C{bool}).
285 @kwarg tol: Convergence tolerance (C{scalar}).
287 @return: 2-Tuple of the intersection points (representing a geodesic chord),
288 each an instance of this C{LatLon} class. Both points are the same
289 instance if the geodesic (line) is tangential to the circle.
291 @raise IntersectionError: The circle and geodesic do not intersect.
293 @raise TypeError: Invalid B{C{circle}}, B{C{start}} or B{C{end}}.
295 @raise UnitError: Invalid B{C{circle}}, B{C{end}}, B{C{exact}} or B{C{height}}.
297 @see: Method L{rhumbIntersecant2<LatLonBase.rhumbIntersecant2>}.
298 '''
299 try:
300 g, gl, p = self._g_gl_p3(start, end, exact, wrap)
301 r = Radius_(circle=circle) if _isRadius(circle) else \
302 g._Inverse(self, self.others(circle=circle), wrap).s12
304 P, Q = _MODS.geodesicw._Intersecant2(gl, self.lat, self.lon, r, tol=tol,
305 form=_MODS.dms.F_DMS)
306 return self._intersecend2(p, end, wrap, height, g, P, Q,
307 self.intersecant2)
308 except (TypeError, ValueError) as x:
309 raise _xError(x, center=self, circle=circle, start=start, end=end,
310 exact=exact, wrap=wrap)
312 def _Inverse(self, other, wrap, **unused): # azis=False, overloaded by I{Vincenty}
313 '''(INTERNAL) I{Karney}'s C{Inverse} method.
315 @return: A L{Distance3Tuple}C{(distance, initial, final)}.
316 '''
317 _ = self.ellipsoids(other)
318 g = self.geodesic
319 _, lon = unroll180(self.lon, other.lon, wrap=wrap)
320 return g.Inverse3(self.lat, self.lon, other.lat, lon)
322 def nearestOn8(self, points, closed=False, height=None, wrap=False,
323 equidistant=None, tol=_TOL_M):
324 '''I{Iteratively} locate the point on a path or polygon closest
325 to this point.
327 @arg points: The path or polygon points (C{LatLon}[]).
328 @kwarg closed: Optionally, close the polygon (C{bool}).
329 @kwarg height: Optional height, overriding the height of this and all
330 other points (C{meter}, conventionally). If C{B{height}
331 is None}, each point's height is taken into account to
332 compute distances.
333 @kwarg wrap: If C{True}, wrap or I{normalize} and unroll the B{C{points}}
334 (C{bool}).
335 @kwarg equidistant: An azimuthal equidistant projection (I{class} or function
336 L{pygeodesy.equidistant}) or C{None} for the preferred
337 L{Equidistant<pygeodesy.ellipsoidalBase.Equidistant>}.
338 @kwarg tol: Convergence tolerance (C{meter}, conventionally).
340 @return: A L{NearestOn8Tuple}C{(closest, distance, fi, j, start, end,
341 initial, final)} with C{distance} in C{meter}, conventionally
342 and with the C{closest}, the C{start} the C{end} point each
343 an instance of this C{LatLon} class.
345 @raise PointsError: Insufficient number of B{C{points}}.
347 @raise TypeError: Some B{C{points}} or B{C{equidistant}} invalid.
349 @raise ValueError: Some B{C{points}}' datum or ellipsoid incompatible
350 or no convergence for the given B{C{tol}}.
352 @see: Function L{pygeodesy.nearestOn6} and method C{nearestOn6}.
353 '''
354 _d3 = self.distanceTo3 # Distance3Tuple
355 _n3 = _nearestOn3
356 try:
357 Ps = self.PointsIter(points, loop=1, wrap=wrap)
358 p1 = c = s = e = Ps[0]
359 _ = self.ellipsoids(p1)
360 c3 = _d3(c, wrap=wrap) # XXX wrap=False?
362 except (TypeError, ValueError) as x:
363 raise _xError(x, Fmt.INDEX(points=0), p1, this=self, tol=tol,
364 closed=closed, height=height, wrap=wrap)
366 # get the azimuthal equidistant projection, once
367 A = _Equidistant00(equidistant, c)
368 b = _Box(c, c3.distance)
369 m = f = i = 0 # p1..p2 == points[i]..[j]
371 kwds = dict(within=True, height=height, tol=tol,
372 LatLon=self.classof, # this LatLon
373 datum=self.datum, epoch=self.epoch, reframe=self.reframe)
374 try:
375 for j, p2 in Ps.enumerate(closed=closed):
376 if wrap and j != 0: # not Ps.looped
377 p2 = _unrollon(p1, p2)
378 # skip edge if no overlap with box around closest
379 if j < 4 or b.overlaps(p1.lat, p1.lon, p2.lat, p2.lon):
380 p, t, _ = _n3(self, p1, p2, A, **kwds)
381 d3 = _d3(p, wrap=False) # already unrolled
382 if d3.distance < c3.distance:
383 c3, c, s, e, f = d3, p, p1, p2, (i + t)
384 b = _Box(c, c3.distance)
385 m = max(m, c.iteration)
386 p1, i = p2, j
388 except (TypeError, ValueError) as x:
389 raise _xError(x, Fmt.INDEX(points=i), p1,
390 Fmt.INDEX(points=j), p2, this=self, tol=tol,
391 closed=closed, height=height, wrap=wrap)
393 f, j = _fi_j2(f, len(Ps)) # like .vector3d.nearestOn6
395 n = typename(self)
396 c.rename(n)
397 if s is not c:
398 s = s.copy(name=n)
399 if e is not c:
400 e = e.copy(name=n) # name__=self.nearestOn8
401 return NearestOn8Tuple(c, c3.distance, f, j, s, e, c3.initial, c3.final,
402 iteration=m) # ._iteration for tests
404 def plumbTo(self, start, end, exact=False, height=None, # PYCHOK signature
405 wrap=False, tol=_TOL):
406 '''Compute the intersection of a geodesic from this point I{perpendicular} to
407 a geodesic (line) given as two points or as a point and a bearing from North.
409 @arg start: Start point of the geodesic (line) (this C{LatLon}).
410 @arg end: End point of the geodesic (line) (this C{LatLon}) or the initial
411 bearing at the B{C{start}} point (compass C{degrees360}).
412 @kwarg exact: Exact C{geodesic...} to use (C{bool} or C{Geodesic...}),
413 see method L{Ellipsoid.geodesic_}.
414 @kwarg height: Optional height for the intersection point (C{meter},
415 conventionally) or C{None} for an interpolated height.
416 @kwarg wrap: If C{True}, wrap or I{normalize} and unroll the B{C{start}}
417 and/or B{C{end}} point (C{bool}).
418 @kwarg tol: Convergence tolerance (C{meter}).
420 @return: The intersection point, an instance of this C{LatLon} class.
422 @raise TypeError: If B{C{start}} or B{C{end}} not this C{LatLon} class.
424 @raise UnitError: Invalid B{C{end}}, B{C{exact}} or B{C{height}}.
425 '''
426 try:
427 g, gl, p = self._g_gl_p3(start, end, exact, wrap)
429 P = _MODS.geodesicw._PlumbTo(gl, self.lat, self.lon, tol=tol)
430 h = self._havg(p, h=height)
431 p = self.classof(P.lat2, P.lon2, datum=self.datum, height=h) # name=n
432 p._iteration = P.iteration
433 except (TypeError, ValueError) as x:
434 raise _xError(x, plumb=self, start=start, end=end,
435 exact=exact, wrap=wrap)
436 return p
439class _Box(object):
440 '''Bounding box around a C{LatLon} point.
442 @see: Function C{_box4} in .clipy.py.
443 '''
444 _1_01 = _1_0 + _0_01 # ~1% margin
446 def __init__(self, center, distance):
447 '''New L{_Box} around point.
449 @arg center: The center point (C{LatLon}).
450 @arg distance: Radius, half-size of the box
451 (C{meter}, conventionally)
452 '''
453 m = Radius_(distance=distance)
454 E = center.ellipsoid()
455 d = E.m2degrees(m) * self._1_01
456 self._N = center.lat + d
457 self._S = center.lat - d
458 self._E = center.lon + d
459 self._W = center.lon - d
461 def overlaps(self, lat1, lon1, lat2, lon2):
462 '''Check whether this box overlaps an other box.
464 @arg lat1: Latitude of a box corner (C{degrees}).
465 @arg lon1: Longitude of a box corner (C{degrees}).
466 @arg lat2: Latitude of the opposing corner (C{degrees}).
467 @arg lon2: Longitude of the opposing corner (C{degrees}).
469 @return: C{True} if there is some overlap, C{False}
470 otherwise (C{bool}).
471 '''
472 if lat1 > lat2:
473 lat1, lat2 = lat2, lat1
474 if lat1 > self._N or lat2 < self._S:
475 return False
476 if lon1 > lon2:
477 lon1, lon2 = lon2, lon1
478 if lon1 > self._E or lon2 < self._W:
479 return False
480 return True
483class _Tol(object):
484 '''Handle a tolerance in C{meter} as C{degrees} and C{meter}.
485 '''
486 _deg = 0 # tol in degrees
487 _lat = 0
488 _m = 0 # tol in meter
489 _min = MAX # degrees
490 _prev = None
491 _r = 0
493 def __init__(self, tol_m, E, lat, *lats):
494 '''New L{_Tol}.
496 @arg tol_m: Tolerance (C{meter}, only).
497 @arg E: Earth ellipsoid (L{Ellipsoid}).
498 @arg lat: Latitude (C{degrees}).
499 @arg lats: Additional latitudes (C{degrees}).
500 '''
501 self._lat = fmean_(lat, *lats) if lats else lat
502 self._r = max(EPS, E.rocMean(self._lat))
503 self._m = max(EPS, tol_m)
504 self._deg = max(EPS, degrees(self._m / self._r)) # NOT E.m2degrees!
506 @property_RO
507 def degrees(self):
508 '''Get this tolerance in C{degrees}.
509 '''
510 return self._deg
512 def degrees2m(self, deg):
513 '''Convert B{C{deg}} to meter at the same C{lat} and earth radius.
514 '''
515 return self.radius * radians(deg) / PI2 # NOT E.degrees2m!
517 def degError(self, Error=_ValueError):
518 '''Compose an error with the C{deg}rees minimum.
519 '''
520 return self.mError(self.degrees2m(self._min), Error=Error)
522 def done(self, deg):
523 '''Check C{deg} vs tolerance and previous value.
524 '''
525 if deg < self._deg or deg == self._prev:
526 return True
527 self._min = min(self._min, deg)
528 self._prev = deg
529 return False
531 @property_RO
532 def lat(self):
533 '''Get the mean latitude in C{degrees}.
534 '''
535 return self._lat
537 def mError(self, m, Error=_ValueError):
538 '''Compose an error with B{C{m}}eter minimum.
539 '''
540 t = _SPACE_(Fmt.tolerance(self.meter), _too_(_low_))
541 if m2km(m) > self.meter:
542 t = _or(t, _antipodal_, _near_(_polar__))
543 return Error(Fmt.no_convergence(m), txt=t)
545 @property_RO
546 def meter(self):
547 '''Get this tolerance in C{meter}.
548 '''
549 return self._m
551 @property_RO
552 def radius(self):
553 '''Get the earth radius in C{meter}.
554 '''
555 return self._r
557 def reset(self):
558 '''Reset tolerances.
559 '''
560 self._min = MAX # delattrof()
561 self._prev = None # delattrof()
564def _Equidistant00(equidistant, p1):
565 '''(INTERNAL) Get an C{Equidistant*(0, 0, ...)} instance.
566 '''
567 if equidistant is None or not callable(equidistant):
568 equidistant = p1.Equidistant
569 else:
570 _xsubclassof(*_MODS.azimuthal._Equidistants,
571 equidistant=equidistant)
572 return equidistant(0, 0, p1.datum)
575def intersecant2(center, circle, point, other, **exact_height_wrap_tol):
576 '''Compute the intersections of a circle and a geodesic given as two points
577 or as a point and (forward) bearing.
579 @arg center: Center of the circle (C{LatLon}).
580 @arg circle: Radius of the circle (C{meter}, conventionally) or a point on
581 the circle (C{LatLon}, as B{C{center}}).
582 @arg point: A point of the geodesic (C{LatLon}, as B{C{center}}).
583 @arg other: An other point of the geodesic (C{LatLon}, as B{C{center}}) or
584 the (forward) bearing at the B{C{point}} (compass C{degrees}).
585 @kwarg exact_height_wrap_tol: Optional keyword arguments C{B{exact}=False},
586 C{B{height}=None}, C{B{wrap}=False} and C{B{tol}}, see method
587 L{intersecant2<LatLonEllipsoidalBaseDI.intersecant2>}.
589 @raise NotImplementedError: Method C{intersecant2} not available.
591 @raise TypeError: If B{C{center}}, B{C{point}} or B{C{circle}} or B{C{other}}
592 points not ellipsoidal or not compatible with B{C{center}}.
594 @see: Method C{LatLon.intersecant2} of class L{ellipsoidalExact.LatLon},
595 L{ellipsoidalKarney.LatLon} or L{ellipsoidalVincenty.LatLon}.
596 '''
597 if not isLatLon(center, ellipsoidal=True): # isinstance(center, LatLonEllipsoidalBase)
598 raise _IsnotError(_ellipsoidal_, center=center)
599 return center.intersecant2(circle, point, other, **exact_height_wrap_tol)
602def _intersect3(s1, end1, s2, end2, height=None, wrap=False, # MCCABE 16 was=True
603 equidistant=None, tol=_TOL_M, LatLon=None, **LatLon_kwds):
604 '''(INTERNAL) Intersect two (ellipsoidal) lines, see ellipsoidal method
605 L{intersection3}, separated to allow callers to embellish any exceptions.
606 '''
607 _LLS = _MODS.sphericalTrigonometry.LatLon
608 _si = _MODS.sphericalTrigonometry._intersect
609 _vi3 = _MODS.vector3d._intersect3d3
611 def _b_d(s, e, w, t, h=_0_0):
612 # compute opposing and distance
613 t = s.classof(t.lat, t.lon, height=h, name=t.name)
614 t = s.distanceTo3(t, wrap=w) # Distance3Tuple
615 b = opposing(e, t.initial) # "before" start
616 return b, t.distance
618 def _b_e(s, e, w, t):
619 # compute an end point along the initial bearing about
620 # 1.5 times the distance to the gu-/estimate, at least
621 # 1/8 and at most 3/8 of the earth perimeter like the
622 # radians in .sphericalTrigonometry._int3d2 and bearing
623 # comparison in .sphericaltrigonometry._intb
624 b, d = _b_d(s, e, w, t, h=t.height)
625 m = s.ellipsoid().R2x * PI_4 # authalic exact
626 d = min(max(d * _1_5, m), m * _3_0)
627 e = s.destination(d, e)
628 return b, (_unrollon(s, e) if w else e)
630 def _e_ll(s, e, w, **end):
631 # return 2-tuple (end, False if bearing else True)
632 ll = not _isDegrees(e)
633 if ll:
634 e = s.others(**end)
635 if w: # unroll180 == .karney._unroll2
636 e = _unrollon(s, e)
637 return e, ll
639 def _o(o, b, n, s, t, e):
640 # determine C{o}utside before, on or after start point
641 if not o: # intersection may be on start
642 if _isequalTo(s, t, eps=e.degrees):
643 return o
644 return -n if b else n
646 E = s1.ellipsoids(s2)
648 e1, ll1 = _e_ll(s1, end1, wrap, end1=end1)
649 e2, ll2 = _e_ll(s2, end2, wrap, end2=end2)
651 e = _Tol(tol, E, s1.lat, (e1.lat if ll1 else s1.lat),
652 s2.lat, (e2.lat if ll2 else s2.lat))
654 # get the azimuthal equidistant projection
655 A = _Equidistant00(equidistant, s1)
657 # gu-/estimate initial intersection, spherically ...
658 t = _si(_LLS(s1), (_LLS(e1) if ll1 else e1),
659 _LLS(s2), (_LLS(e2) if ll2 else e2),
660 height=height, wrap=False, LatLon=_LLS) # unrolled already
661 h, n = t.height, t.name
663 if not ll1:
664 b1, e1 = _b_e(s1, e1, wrap, t)
665 if not ll2:
666 b2, e2 = _b_e(s2, e2, wrap, t)
668 # ... and iterate as Karney describes, for references
669 # @see: Function L{ellipsoidalKarney.intersection3}.
670 for i in range(1, _TRIPS):
671 A.reset(t.lat, t.lon) # gu-/estimate as origin
672 # convert start and end points to projection
673 # space and compute an intersection there
674 v, o1, o2 = _vi3(*A._forwards(s1, e1, s2, e2),
675 eps=e.meter, useZ=False)
676 # convert intersection back to geodetic
677 t, d = A._reverse2(v)
678 if e.done(d): # below tol or unchanged?
679 break
680 else:
681 raise e.degError(Error=IntersectionError)
683 # like .sphericalTrigonometry._intersect, if this intersection
684 # is "before" the first point, use the antipodal intersection
685 if not (ll1 or ll2): # end1 and end2 are an initial bearing
686 b1, _ = _b_d(s1, end1, wrap, t)
687 if b1:
688 t = t.antipodal()
689 b1 = not b1
690 b2, _ = _b_d(s2, end2, wrap, t)
692 r = _LL4Tuple(t.lat, t.lon, h, t.datum, LatLon, LatLon_kwds, inst=s1,
693 iteration=i, name=n)
694 return Intersection3Tuple(r, (o1 if ll1 else _o(o1, b1, 1, s1, t, e)),
695 (o2 if ll2 else _o(o2, b2, 2, s2, t, e)))
698def _intersection3(start1, end1, start2, end2, height=None, wrap=False, # was=True
699 **equidistant_tol_LatLon_and_kwds):
700 '''(INTERNAL) Iteratively compute the intersection point of two lines,
701 each defined by two (ellipsoidal) points or an (ellipsoidal) start
702 point and an initial bearing from North.
703 '''
704 s1 = _xellipsoidal(start1=start1)
705 s2 = s1.others(start2=start2)
706 try:
707 return _intersect3(s1, end1, s2, end2, height=height, wrap=wrap,
708 **equidistant_tol_LatLon_and_kwds)
709 except (TypeError, ValueError) as x:
710 raise _xError(x, start1=start1, end1=end1, start2=start2, end2=end2)
713def _intersections2(center1, radius1, center2, radius2, height=None, wrap=False, # was=True
714 **equidistant_tol_LatLon_and_kwds):
715 '''(INTERNAL) Iteratively compute the intersection points of two circles,
716 each defined by an (ellipsoidal) center point and a radius.
717 '''
718 c1 = _xellipsoidal(center1=center1)
719 c2 = c1.others(center2=center2)
720 try:
721 return _intersects2(c1, radius1, c2, radius2, height=height, wrap=wrap,
722 **equidistant_tol_LatLon_and_kwds)
723 except (TypeError, ValueError) as x:
724 raise _xError(x, center1=center1, radius1=radius1,
725 center2=center2, radius2=radius2)
728def _intersects2(c1, radius1, c2, radius2, height=None, wrap=False, # MCCABE 16 was=True
729 equidistant=None, tol=_TOL_M, LatLon=None, **LatLon_kwds):
730 '''(INTERNAL) Intersect two (ellipsoidal) circles, see L{_intersections2}
731 above, separated to allow callers to embellish any exceptions.
732 '''
733 _LLS = _MODS.sphericalTrigonometry.LatLon
734 _si2 = _MODS.sphericalTrigonometry._intersects2
735 _vi2 = _MODS.vector3d._intersects2
737 def _ll4(t, h, n, c):
738 return _LL4Tuple(t.lat, t.lon, h, t.datum, LatLon, LatLon_kwds, inst=c,
739 iteration=t.iteration, name=n)
741 r1 = Radius_(radius1=radius1)
742 r2 = Radius_(radius2=radius2)
744 E = c1.ellipsoids(c2)
745 # get the azimuthal equidistant projection
746 A = _Equidistant00(equidistant, c1)
748 if r1 < r2:
749 c1, c2 = c2, c1
750 r1, r2 = r2, r1
752 if r1 > (min(E.b, E.a) * PI):
753 raise _ValueError(_exceed_PI_radians_)
755 if wrap: # unroll180 == .karney._unroll2
756 c2 = _unrollon(c1, c2)
758 # distance between centers and radii are
759 # measured along the ellipsoid's surface
760 m = c1.distanceTo(c2, wrap=False) # meter
761 if m < max(r1 - r2, EPS):
762 raise IntersectionError(_near_(_concentric_)) # XXX ConcentricError?
763 if fsumf_(r1, r2, -m) < 0:
764 raise IntersectionError(_too_(Fmt.distant(m)))
766 f = _radical2(m, r1, r2).ratio # "radical fraction"
767 e = _Tol(tol, E, favg(c1.lat, c2.lat, f=f))
769 # gu-/estimate initial intersections, spherically ...
770 t1, t2 = _si2(_LLS(c1), r1, _LLS(c2), r2, radius=e.radius,
771 height=height, too_d=m, wrap=False) # unrolled already
772 h, n = t1.height, t1.name
774 # ... and iterate as Karney describes, for references
775 # @see: Function L{ellipsoidalKarney.intersections2}.
776 ts, ta = [], None
777 for t in ((t1,) if t1 is t2 else (t1, t2)):
778 for i in range(1, _TRIPS):
779 A.reset(t.lat, t.lon) # gu-/estimate as origin
780 # convert centers to projection space and
781 # compute the intersections there
782 t1, t2 = A._forwards(c1, c2)
783 v1, v2 = _vi2(t1, r1, # XXX * t1.scale?,
784 t2, r2, # XXX * t2.scale?,
785 sphere=False, too_d=m)
786 # convert intersections back to geodetic
787 if v1 is v2: # abutting
788 t, d = A._reverse2(v1)
789 else: # consider the closer intersection
790 t1, d1 = A._reverse2(v1)
791 t2, d2 = A._reverse2(v2)
792 t, d = (t1, d1) if d1 < d2 else (t2, d2)
793 if e.done(d): # below tol or unchanged?
794 t._iteration = i # _NamedTuple._iteration
795 ts.append(t)
796 if v1 is v2: # abutting
797 ta = t
798 break
799 else:
800 raise e.degError(Error=IntersectionError)
801 e.reset()
803 if ta: # abutting circles
804 pass # PYCHOK no cover
805 elif len(ts) == 2:
806 return (_ll4(ts[0], h, n, c1),
807 _ll4(ts[1], h, n, c2))
808 elif len(ts) == 1: # PYCHOK no cover
809 ta = ts[0] # assume abutting
810 else: # PYCHOK no cover
811 raise _AssertionError(ts=ts)
812 r = _ll4(ta, h, n, c1)
813 return r, r
816def _nearestOn2(p, point1, point2, within=True, height=None, wrap=False, # was=True
817 equidistant=None, tol=_TOL_M, **LatLon_and_kwds):
818 '''(INTERNAL) Closest point and fraction, like L{_intersects2} above,
819 separated to allow callers to embellish any exceptions.
820 '''
821 p1 = p.others(point1=point1)
822 p2 = p.others(point2=point2)
824 _ = p.ellipsoids(p1)
825# E = p.ellipsoids(p2) # done in _nearestOn3
827 # get the azimuthal equidistant projection
828 A = _Equidistant00(equidistant, p)
830 p1, p2, _ = _unrollon3(p, p1, p2, wrap) # XXX don't unroll?
831 r, f, _ = _nearestOn3(p, p1, p2, A, within=within, height=height,
832 tol=tol, **LatLon_and_kwds)
833 return NearestOn2Tuple(r, f)
836def _nearestOn3(p, p1, p2, A, within=True, height=None, tol=_TOL_M,
837 LatLon=None, **LatLon_kwds):
838 # Only in function C{_nearestOn2} and method C{nearestOn8} above
839 _LLS = _MODS.sphericalNvector.LatLon
840 _V3d = _MODS.vector3d.Vector3d
841 _vn2 = _MODS.vector3d._nearestOn2
843 E = p.ellipsoids(p2)
844 e = _Tol(tol, E, p.lat, p1.lat, p2.lat)
846 # gu-/estimate initial nearestOn, spherically ... wrap=False, only!
847 # using sphericalNvector.LatLon.nearestOn for within=False support
848 t = _LLS(p).nearestOn(_LLS(p1), _LLS(p2), within=within,
849 height=height)
850 n, h = t.name, t.height
851 if height is None:
852 h1 = p1.height # use heights as pseudo-Z in projection space
853 h2 = p2.height # to be included in the closest function
854 h0 = favg(h1, h2)
855 else: # ignore heights in distances, Z=0
856 h0 = h1 = h2 = _0_0
858 # ... and iterate to find the closest (to the origin with .z
859 # to interpolate height) as Karney describes, for references
860 # @see: Function L{ellipsoidalKarney.nearestOn}.
861 vp, f = _V3d(_0_0, _0_0, h0), None
862 for i in range(1, _TRIPS):
863 A.reset(t.lat, t.lon) # gu-/estimate as origin
864 # convert points to projection space and compute
865 # the nearest one (and its height) there
866 s, t = A._forwards(p1, p2)
867 v, f = _vn2(vp, _V3d(s.x, s.y, h1),
868 _V3d(t.x, t.y, h2), within=within)
869 # convert nearest one back to geodetic
870 t, d = A._reverse2(v)
871 if e.done(d): # below tol or unchanged?
872 break
873 else:
874 raise e.degError()
876 if height is None:
877 h = v.z # nearest
878 elif _isHeight(height):
879 h = height
880 r = _LL4Tuple(t.lat, t.lon, h, t.datum, LatLon, LatLon_kwds, inst=p,
881 iteration=i, name=n)
882 return r, f, e # fraction or None
885__all__ += _ALL_DOCS(LatLonEllipsoidalBaseDI, intersecant2)
886del _1_0, _0_01
888# **) MIT License
889#
890# Copyright (C) 2016-2025 -- mrJean1 at Gmail -- All Rights Reserved.
891#
892# Permission is hereby granted, free of charge, to any person obtaining a
893# copy of this software and associated documentation files (the "Software"),
894# to deal in the Software without restriction, including without limitation
895# the rights to use, copy, modify, merge, publish, distribute, sublicense,
896# and/or sell copies of the Software, and to permit persons to whom the
897# Software is furnished to do so, subject to the following conditions:
898#
899# The above copyright notice and this permission notice shall be included
900# in all copies or substantial portions of the Software.
901#
902# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
903# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
904# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
905# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
906# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
907# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
908# OTHER DEALINGS IN THE SOFTWARE.