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