Coverage for pygeodesy/sphericalTrigonometry.py: 93%
387 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'''Spherical, C{trigonometry}-based geodesy.
6Trigonometric classes geodetic (lat-/longitude) L{LatLon} and
7geocentric (ECEF) L{Cartesian} and functions L{areaOf}, L{intersection},
8L{intersections2}, L{isPoleEnclosedBy}, L{meanOf}, L{nearestOn3} and
9L{perimeterOf}, I{all spherical}.
11Pure Python implementation of geodetic (lat-/longitude) methods using
12spherical trigonometry, transcoded from JavaScript originals by
13I{(C) Chris Veness 2011-2024} published under the same MIT Licence**, see
14U{Latitude/Longitude<https://www.Movable-Type.co.UK/scripts/latlong.html>}.
15'''
16# make sure int/int division yields float quotient, see .basics
17from __future__ import division as _; del _ # PYCHOK semicolon
19from pygeodesy.basics import copysign0, _isin, map1, signOf, typename
20from pygeodesy.constants import EPS, EPS1, EPS4, PI, PI2, PI_2, PI_4, R_M, \
21 isnear0, isnear1, isnon0, _0_0, _0_5, \
22 _1_0, _2_0, _90_0
23from pygeodesy.datums import _ellipsoidal_datum, _mean_radius
24from pygeodesy.errors import _AssertionError, CrossError, crosserrors, \
25 _TypeError, _ValueError, IntersectionError, \
26 _xError, _xkwds, _xkwds_get, _xkwds_pop2
27from pygeodesy.fmath import favg, fdot, fdot_, fmean, hypot
28from pygeodesy.fsums import Fsum, fsum, fsumf_
29from pygeodesy.formy import antipode_, bearing_, _bearingTo2, excessAbc_, \
30 excessGirard_, excessLHuilier_, opposing_, _radical2, \
31 vincentys_
32# from pygeodesy.internals import typename # from .basics
33from pygeodesy.interns import _1_, _2_, _coincident_, _composite_, _colinear_, \
34 _concentric_, _convex_, _end_, _infinite_, \
35 _invalid_, _line_, _near_, _null_, _parallel_, \
36 _point_, _SPACE_, _too_
37from pygeodesy.lazily import _ALL_LAZY, _ALL_MODS as _MODS, _ALL_OTHER
38# from pygeodesy.nvectorBase import NvectorBase, sumOf # _MODE
39from pygeodesy.namedTuples import LatLon2Tuple, LatLon3Tuple, NearestOn3Tuple, \
40 Triangle7Tuple, Triangle8Tuple
41from pygeodesy.points import ispolar, nearestOn5 as _nearestOn5, \
42 Fmt as _Fmt # XXX shadowed
43from pygeodesy.props import deprecated_function, deprecated_method
44from pygeodesy.sphericalBase import _m2radians, CartesianSphericalBase, \
45 _intersecant2, LatLonSphericalBase, \
46 _rads3, _radians2m, _trilaterate5
47# from pygeodesy.streprs import Fmt as _Fmt # from .points XXX shadowed
48from pygeodesy.units import Bearing_, Height, _isDegrees, _isRadius, Lamd, \
49 Phid, Radius_, Scalar
50from pygeodesy.utily import acos1, asin1, atan1d, atan2, atan2d, degrees90, \
51 degrees180, degrees2m, m2radians, radiansPI2, \
52 sincos2_, tan_2, unrollPI, _unrollon, _unrollon3, \
53 wrap180, wrapPI, _Wrap
54from pygeodesy.vector3d import sumOf, Vector3d
56from math import asin, cos, degrees, fabs, radians, sin
58__all__ = _ALL_LAZY.sphericalTrigonometry
59__version__ = '25.04.14'
61_PI_EPS4 = PI - EPS4
62if _PI_EPS4 >= PI:
63 raise _AssertionError(EPS4=EPS4, PI=PI, PI_EPS4=_PI_EPS4)
66class Cartesian(CartesianSphericalBase):
67 '''Extended to convert geocentric, L{Cartesian} points to
68 spherical, geodetic L{LatLon}.
69 '''
71 def toLatLon(self, **LatLon_and_kwds): # PYCHOK LatLon=LatLon
72 '''Convert this cartesian point to a C{spherical} geodetic point.
74 @kwarg LatLon_and_kwds: Optional L{LatLon} and L{LatLon} keyword
75 arguments. Use C{B{LatLon}=...} to override
76 this L{LatLon} class or specify C{B{LatLon}=None}.
78 @return: The geodetic point (L{LatLon}) or if C{B{LatLon} is None},
79 an L{Ecef9Tuple}C{(x, y, z, lat, lon, height, C, M, datum)}
80 with C{C} and C{M} if available.
82 @raise TypeError: Invalid B{C{LatLon_and_kwds}} argument.
83 '''
84 kwds = _xkwds(LatLon_and_kwds, LatLon=LatLon, datum=self.datum)
85 return CartesianSphericalBase.toLatLon(self, **kwds)
88class LatLon(LatLonSphericalBase):
89 '''New point on a spherical earth model, based on trigonometry formulae.
90 '''
92 def _ab1_ab2_db5(self, other, wrap):
93 '''(INTERNAL) Helper for several methods.
94 '''
95 a1, b1 = self.philam
96 a2, b2 = self.others(other, up=2).philam
97 if wrap:
98 a2, b2 = _Wrap.philam(a2, b2)
99 db, b2 = unrollPI(b1, b2, wrap=wrap)
100 else: # unrollPI shortcut
101 db = b2 - b1
102 return a1, b1, a2, b2, db
104 def alongTrackDistanceTo(self, start, end, radius=R_M, wrap=False):
105 '''Compute the (signed) distance from the start to the closest
106 point on the great circle line defined by a start and an
107 end point.
109 That is, if a perpendicular is drawn from this point to the
110 great circle line, the along-track distance is the distance
111 from the start point to the point where the perpendicular
112 crosses the line.
114 @arg start: Start point of the great circle line (L{LatLon}).
115 @arg end: End point of the great circle line (L{LatLon}).
116 @kwarg radius: Mean earth radius (C{meter}) or C{None}.
117 @kwarg wrap: If C{True}, wrap or I{normalize} and unroll
118 the B{C{start}} and B{C{end}} point (C{bool}).
120 @return: Distance along the great circle line (C{radians}
121 if C{B{radius} is None} or C{meter}, same units
122 as B{C{radius}}), positive if I{after} the
123 B{C{start}} toward the B{C{end}} point of the
124 line, I{negative} if before or C{0} if at the
125 B{C{start}} point.
127 @raise TypeError: Invalid B{C{start}} or B{C{end}} point.
129 @raise ValueError: Invalid B{C{radius}}.
130 '''
131 r, x, b = self._a_x_b3(start, end, radius, wrap)
132 cx = cos(x)
133 return _0_0 if isnear0(cx) else \
134 _radians2m(copysign0(acos1(cos(r) / cx), cos(b)), radius)
136 def _a_x_b3(self, start, end, radius, wrap):
137 '''(INTERNAL) Helper for .along-/crossTrackDistanceTo.
138 '''
139 s = self.others(start=start)
140 e = self.others(end=end)
141 s, e, w = _unrollon3(self, s, e, wrap)
143 r = Radius_(radius)
144 r = s.distanceTo(self, r, wrap=w) / r
146 b = radians(s.initialBearingTo(self, wrap=w)
147 - s.initialBearingTo(e, wrap=w))
148 x = asin(sin(r) * sin(b))
149 return r, x, -b
151 @deprecated_method
152 def bearingTo(self, other, wrap=False, raiser=False): # PYCHOK no cover
153 '''DEPRECATED, use method L{initialBearingTo}.
154 '''
155 return self.initialBearingTo(other, wrap=wrap, raiser=raiser)
157 def crossingParallels(self, other, lat, wrap=False):
158 '''Return the pair of meridians at which a great circle defined
159 by this and an other point crosses the given latitude.
161 @arg other: The other point defining great circle (L{LatLon}).
162 @arg lat: Latitude at the crossing (C{degrees}).
163 @kwarg wrap: If C{True}, wrap or I{normalize} and unroll the
164 B{C{other}} point (C{bool}).
166 @return: 2-Tuple C{(lon1, lon2)}, both in C{degrees180} or
167 C{None} if the great circle doesn't reach B{C{lat}}.
168 '''
169 a1, b1, a2, b2, db = self._ab1_ab2_db5(other, wrap)
170 sa, ca, sa1, ca1, \
171 sa2, ca2, sdb, cdb = sincos2_(radians(lat), a1, a2, db)
172 sa1 *= ca2 * ca
174 x = sa1 * sdb
175 y = sa1 * cdb - ca1 * sa2 * ca
176 z = ca1 * sdb * ca2 * sa
178 h = hypot(x, y)
179 if h < EPS or fabs(z) > h: # PYCHOK no cover
180 return None # great circle doesn't reach latitude
182 m = atan2(-y, x) + b1 # longitude at max latitude
183 d = acos1(z / h) # delta longitude to intersections
184 return degrees180(m - d), degrees180(m + d)
186 def crossTrackDistanceTo(self, start, end, radius=R_M, wrap=False):
187 '''Compute the (signed) distance from this point to a great
188 circle from a start to an end point.
190 @arg start: Start point of the great circle line (L{LatLon}).
191 @arg end: End point of the great circle line (L{LatLon}).
192 @kwarg radius: Mean earth radius (C{meter}) or C{None}.
193 @kwarg wrap: If C{True}, wrap or I{normalize} and unroll
194 the B{C{start}} and B{C{end}} point (C{bool}).
196 @return: Distance to the great circle (C{radians} if
197 B{C{radius}} or C{meter}, same units as
198 B{C{radius}}), I{negative} if to the left or
199 I{positive} if to the right of the line.
201 @raise TypeError: If B{C{start}} or B{C{end}} is not L{LatLon}.
203 @raise ValueError: Invalid B{C{radius}}.
204 '''
205 _, x, _ = self._a_x_b3(start, end, radius, wrap)
206 return _radians2m(x, radius)
208 def destination(self, distance, bearing, radius=R_M, height=None):
209 '''Locate the destination from this point after having
210 travelled the given distance on a bearing from North.
212 @arg distance: Distance travelled (C{meter}, same units as
213 B{C{radius}}).
214 @arg bearing: Bearing from this point (compass C{degrees360}).
215 @kwarg radius: Mean earth radius (C{meter}).
216 @kwarg height: Optional height at destination (C{meter}, same
217 units a B{C{radius}}).
219 @return: Destination point (L{LatLon}).
221 @raise ValueError: Invalid B{C{distance}}, B{C{bearing}},
222 B{C{radius}} or B{C{height}}.
223 '''
224 a, b = self.philam
225 r, t = _m2radians(distance, radius, low=None), Bearing_(bearing)
227 a, b = _destination2(a, b, r, t)
228 h = self._heigHt(height)
229 return self.classof(degrees90(a), degrees180(b), height=h)
231 def distanceTo(self, other, radius=R_M, wrap=False):
232 '''Compute the (angular) distance from this to an other point.
234 @arg other: The other point (L{LatLon}).
235 @kwarg radius: Mean earth radius (C{meter}) or C{None}.
236 @kwarg wrap: If C{True}, wrap or I{normalize} and unroll
237 the B{C{other}} point (C{bool}).
239 @return: Distance between this and the B{C{other}} point
240 (C{meter}, same units as B{C{radius}} or
241 C{radians} if C{B{radius} is None}).
243 @raise TypeError: The B{C{other}} point is not L{LatLon}.
245 @raise ValueError: Invalid B{C{radius}}.
246 '''
247 a1, _, a2, _, db = self._ab1_ab2_db5(other, wrap)
248 return _radians2m(vincentys_(a2, a1, db), radius)
250# @Property_RO
251# def Ecef(self):
252# '''Get the ECEF I{class} (L{EcefVeness}), I{lazily}.
253# '''
254# return _MODS.ecef.EcefKarney
256 def greatCircle(self, bearing, Vector=Vector3d, **Vector_kwds):
257 '''Compute the vector normal to great circle obtained by heading
258 from this point on the bearing from North.
260 Direction of vector is such that initial bearing vector
261 b = c × n, where n is an n-vector representing this point.
263 @arg bearing: Bearing from this point (compass C{degrees360}).
264 @kwarg Vector: Vector class to return the great circle,
265 overriding the default L{Vector3d}.
266 @kwarg Vector_kwds: Optional, additional keyword argunents
267 for B{C{Vector}}.
269 @return: Vector representing great circle (C{Vector}).
271 @raise ValueError: Invalid B{C{bearing}}.
272 '''
273 a, b = self.philam
274 sa, ca, sb, cb, st, ct = sincos2_(a, b, Bearing_(bearing))
276 sa *= st
277 return Vector(fdot_(sb, ct, -cb, sa),
278 -fdot_(cb, ct, sb, sa),
279 ca * st, **Vector_kwds) # XXX .unit()?
281 def initialBearingTo(self, other, wrap=False, raiser=False):
282 '''Compute the initial bearing (forward azimuth) from this
283 to an other point.
285 @arg other: The other point (spherical L{LatLon}).
286 @kwarg wrap: If C{True}, wrap or I{normalize} and unroll
287 the B{C{other}} point (C{bool}).
288 @kwarg raiser: Optionally, raise L{CrossError} (C{bool}),
289 use C{B{raiser}=True} for behavior like
290 C{sphericalNvector.LatLon.initialBearingTo}.
292 @return: Initial bearing (compass C{degrees360}).
294 @raise CrossError: If this and the B{C{other}} point coincide
295 and if B{C{raiser}} and L{crosserrors
296 <pygeodesy.crosserrors>} are both C{True}.
298 @raise TypeError: The B{C{other}} point is not L{LatLon}.
299 '''
300 a1, b1, a2, b2, db = self._ab1_ab2_db5(other, wrap)
301 # XXX behavior like sphericalNvector.LatLon.initialBearingTo
302 if raiser and crosserrors() and max(fabs(a2 - a1), fabs(db)) < EPS:
303 raise CrossError(_point_, self, other=other, wrap=wrap, txt=_coincident_)
305 return degrees(bearing_(a1, b1, a2, b2, final=False))
307 def intermediateTo(self, other, fraction, height=None, wrap=False):
308 '''Locate the point at given fraction between (or along) this
309 and an other point.
311 @arg other: The other point (L{LatLon}).
312 @arg fraction: Fraction between both points (C{scalar},
313 0.0 at this and 1.0 at the other point).
314 @kwarg height: Optional height, overriding the intermediate
315 height (C{meter}).
316 @kwarg wrap: If C{True}, wrap or I{normalize} and unroll the
317 B{C{other}} point (C{bool}).
319 @return: Intermediate point (L{LatLon}).
321 @raise TypeError: The B{C{other}} point is not L{LatLon}.
323 @raise ValueError: Invalid B{C{fraction}} or B{C{height}}.
325 @see: Methods C{midpointTo} and C{rhumbMidpointTo}.
326 '''
327 p = self
328 f = Scalar(fraction=fraction)
329 if not isnear0(f):
330 p = p.others(other)
331 if wrap:
332 p = _Wrap.point(p)
333 if not isnear1(f): # and not near0
334 a1, b1 = self.philam
335 a2, b2 = p.philam
336 db, b2 = unrollPI(b1, b2, wrap=wrap)
337 r = vincentys_(a2, a1, db)
338 sr = sin(r)
339 if isnon0(sr):
340 sa1, ca1, sa2, ca2, \
341 sb1, cb1, sb2, cb2 = sincos2_(a1, a2, b1, b2)
343 t = f * r
344 a = sin(r - t) # / sr superflous
345 b = sin( t) # / sr superflous
347 x = fdot_(a, ca1 * cb1, b, ca2 * cb2)
348 y = fdot_(a, ca1 * sb1, b, ca2 * sb2)
349 z = fdot_(a, sa1, b, sa2)
351 a = atan1d(z, hypot(x, y))
352 b = atan2d(y, x)
354 else: # PYCHOK no cover
355 a = degrees90( favg(a1, a2, f=f)) # coincident
356 b = degrees180(favg(b1, b2, f=f))
358 h = self._havg(other, f=f, h=height)
359 p = self.classof(a, b, height=h)
360 return p
362 def intersection(self, end1, other, end2, height=None, wrap=False):
363 '''Compute the intersection point of two lines, each defined by
364 two points or a start point and a bearing from North.
366 @arg end1: End point of this line (L{LatLon}) or the initial
367 bearing at this point (compass C{degrees360}).
368 @arg other: Start point of the other line (L{LatLon}).
369 @arg end2: End point of the other line (L{LatLon}) or the
370 initial bearing at the B{C{other}} point (compass
371 C{degrees360}).
372 @kwarg height: Optional height for intersection point,
373 overriding the mean height (C{meter}).
374 @kwarg wrap: If C{True}, wrap or I{normalize} and unroll
375 B{C{start2}} and both B{C{end*}} points (C{bool}).
377 @return: The intersection point (L{LatLon}). An alternate
378 intersection point might be the L{antipode} to
379 the returned result.
381 @raise IntersectionError: Ambiguous or infinite intersection
382 or colinear, parallel or otherwise
383 non-intersecting lines.
385 @raise TypeError: If B{C{other}} is not L{LatLon} or B{C{end1}}
386 or B{C{end2}} not C{scalar} nor L{LatLon}.
388 @raise ValueError: Invalid B{C{height}} or C{null} line.
389 '''
390 try:
391 s2 = self.others(other)
392 return _intersect(self, end1, s2, end2, height=height, wrap=wrap,
393 LatLon=self.classof)
394 except (TypeError, ValueError) as x:
395 raise _xError(x, start1=self, end1=end1,
396 other=other, end2=end2, wrap=wrap)
398 def intersections2(self, rad1, other, rad2, radius=R_M, eps=_0_0,
399 height=None, wrap=True):
400 '''Compute the intersection points of two circles, each defined
401 by a center point and a radius.
403 @arg rad1: Radius of the this circle (C{meter} or C{radians},
404 see B{C{radius}}).
405 @arg other: Center point of the other circle (L{LatLon}).
406 @arg rad2: Radius of the other circle (C{meter} or C{radians},
407 see B{C{radius}}).
408 @kwarg radius: Mean earth radius (C{meter} or C{None} if B{C{rad1}},
409 B{C{rad2}} and B{C{eps}} are given in C{radians}).
410 @kwarg eps: Required overlap (C{meter} or C{radians}, see
411 B{C{radius}}).
412 @kwarg height: Optional height for the intersection points (C{meter},
413 conventionally) or C{None} for the I{"radical height"}
414 at the I{radical line} between both centers.
415 @kwarg wrap: If C{True}, wrap or I{normalize} and unroll the
416 B{C{other}} point (C{bool}).
418 @return: 2-Tuple of the intersection points, each a L{LatLon}
419 instance. For abutting circles, both intersection
420 points are the same instance, aka the I{radical center}.
422 @raise IntersectionError: Concentric, antipodal, invalid or
423 non-intersecting circles.
425 @raise TypeError: If B{C{other}} is not L{LatLon}.
427 @raise ValueError: Invalid B{C{rad1}}, B{C{rad2}}, B{C{radius}},
428 B{C{eps}} or B{C{height}}.
429 '''
430 try:
431 c2 = self.others(other)
432 return _intersects2(self, rad1, c2, rad2, radius=radius, eps=eps,
433 height=height, wrap=wrap,
434 LatLon=self.classof)
435 except (TypeError, ValueError) as x:
436 raise _xError(x, center=self, rad1=rad1,
437 other=other, rad2=rad2, wrap=wrap)
439 @deprecated_method
440 def isEnclosedBy(self, points): # PYCHOK no cover
441 '''DEPRECATED, use method C{isenclosedBy}.'''
442 return self.isenclosedBy(points)
444 def isenclosedBy(self, points, wrap=False):
445 '''Check whether a (convex) polygon or composite encloses this point.
447 @arg points: The polygon points or composite (L{LatLon}[],
448 L{BooleanFHP} or L{BooleanGH}).
449 @kwarg wrap: If C{True}, wrap or I{normalize} and unroll the
450 B{C{points}} (C{bool}).
452 @return: C{True} if this point is inside the polygon or
453 composite, C{False} otherwise.
455 @raise PointsError: Insufficient number of B{C{points}}.
457 @raise TypeError: Some B{C{points}} are not L{LatLon}.
459 @raise ValueError: Invalid B{C{points}}, non-convex polygon.
461 @see: Functions L{pygeodesy.isconvex}, L{pygeodesy.isenclosedBy}
462 and L{pygeodesy.ispolar} especially if the B{C{points}} may
463 enclose a pole or wrap around the earth I{longitudinally}.
464 '''
465 if _MODS.booleans.isBoolean(points):
466 return points._encloses(self.lat, self.lon, wrap=wrap)
468 Ps = self.PointsIter(points, loop=2, dedup=True, wrap=wrap)
469 n0 = self._N_vector
471 v2 = Ps[0]._N_vector
472 p1 = Ps[1]
473 v1 = p1._N_vector
474 # check whether this point on same side of all
475 # polygon edges (to the left or right depending
476 # on the anti-/clockwise polygon direction)
477 gc1 = v2.cross(v1)
478 t0 = gc1.angleTo(n0) > PI_2
479 s0 = None
480 # get great-circle vector for each edge
481 for i, p2 in Ps.enumerate(closed=True):
482 if wrap and not Ps.looped:
483 p2 = _unrollon(p1, p2)
484 p1 = p2
485 v2 = p2._N_vector
486 gc = v1.cross(v2)
487 t = gc.angleTo(n0) > PI_2
488 if t != t0: # different sides of edge i
489 return False # outside
491 # check for convex polygon: angle between
492 # gc vectors, signed by direction of n0
493 # (otherwise the test above is not reliable)
494 s = signOf(gc1.angleTo(gc, vSign=n0))
495 if s != s0:
496 if s0 is None:
497 s0 = s
498 else:
499 t = _Fmt.SQUARE(points=i)
500 raise _ValueError(t, p2, wrap=wrap, txt_not_=_convex_)
501 gc1, v1 = gc, v2
503 return True # inside
505 def midpointTo(self, other, height=None, fraction=_0_5, wrap=False):
506 '''Find the midpoint between this and an other point.
508 @arg other: The other point (L{LatLon}).
509 @kwarg height: Optional height for midpoint, overriding
510 the mean height (C{meter}).
511 @kwarg fraction: Midpoint location from this point (C{scalar}),
512 may be negative or greater than 1.0.
513 @kwarg wrap: If C{True}, wrap or I{normalize} and unroll the
514 B{C{other}} point (C{bool}).
516 @return: Midpoint (L{LatLon}).
518 @raise TypeError: The B{C{other}} point is not L{LatLon}.
520 @raise ValueError: Invalid B{C{height}}.
522 @see: Methods C{intermediateTo} and C{rhumbMidpointTo}.
523 '''
524 if fraction is _0_5:
525 # see <https://MathForum.org/library/drmath/view/51822.html>
526 a1, b, a2, _, db = self._ab1_ab2_db5(other, wrap)
527 sa1, ca1, sa2, ca2, sdb, cdb = sincos2_(a1, a2, db)
529 x = ca2 * cdb + ca1
530 y = ca2 * sdb
532 a = atan1d(sa1 + sa2, hypot(x, y))
533 b = degrees180(b + atan2(y, x))
535 h = self._havg(other, h=height)
536 r = self.classof(a, b, height=h)
537 else:
538 r = self.intermediateTo(other, fraction, height=height, wrap=wrap)
539 return r
541 def nearestOn(self, point1, point2, radius=R_M, **wrap_adjust_limit):
542 '''Locate the point between two other points closest to this point.
544 Distances are approximated by function L{pygeodesy.equirectangular4},
545 subject to the supplied B{C{options}}.
547 @arg point1: Start point (L{LatLon}).
548 @arg point2: End point (L{LatLon}).
549 @kwarg radius: Mean earth radius (C{meter}).
550 @kwarg wrap_adjust_limit: Optional keyword arguments for functions
551 L{sphericalTrigonometry.nearestOn3} and
552 L{pygeodesy.equirectangular4},
554 @return: Closest point on the great circle line (L{LatLon}).
556 @raise LimitError: Lat- and/or longitudinal delta exceeds B{C{limit}},
557 see function L{pygeodesy.equirectangular4}.
559 @raise NotImplementedError: Keyword argument C{B{within}=False}
560 is not (yet) supported.
562 @raise TypeError: Invalid B{C{point1}} or B{C{point2}}.
564 @raise ValueError: Invalid B{C{radius}} or B{C{options}}.
566 @see: Functions L{pygeodesy.equirectangular4} and L{pygeodesy.nearestOn5}
567 and method L{sphericalTrigonometry.LatLon.nearestOn3}.
568 '''
569 # remove kwarg B{C{within}} if present
570 w, kwds = _xkwds_pop2(wrap_adjust_limit, within=True)
571 if not w:
572 self._notImplemented(within=w)
574# # UNTESTED - handle C{B{within}=False} and C{B{within}=True}
575# wrap = _xkwds_get(options, wrap=False)
576# a = self.alongTrackDistanceTo(point1, point2, radius=radius, wrap=wrap)
577# if fabs(a) < EPS or (within and a < EPS):
578# return point1
579# d = point1.distanceTo(point2, radius=radius, wrap=wrap)
580# if isnear0(d):
581# return point1 # or point2
582# elif fabs(d - a) < EPS or (a + EPS) > d:
583# return point2
584# f = a / d
585# if within:
586# if f > EPS1:
587# return point2
588# elif f < EPS:
589# return point1
590# return point1.intermediateTo(point2, f, wrap=wrap)
592 # without kwarg B{C{within}}, use backward compatible .nearestOn3
593 return self.nearestOn3([point1, point2], closed=False, radius=radius,
594 **kwds)[0]
596 @deprecated_method
597 def nearestOn2(self, points, closed=False, radius=R_M, **options): # PYCHOK no cover
598 '''DEPRECATED, use method L{sphericalTrigonometry.LatLon.nearestOn3}.
600 @return: ... 2-Tuple C{(closest, distance)} of the closest
601 point (L{LatLon}) on the polygon and the distance
602 to that point from this point in C{meter}, same
603 units of B{C{radius}}.
604 '''
605 r = self.nearestOn3(points, closed=closed, radius=radius, **options)
606 return r.closest, r.distance
608 def nearestOn3(self, points, closed=False, radius=R_M, **wrap_adjust_limit):
609 '''Locate the point on a polygon closest to this point.
611 Distances are approximated by function L{pygeodesy.equirectangular4},
612 subject to the supplied B{C{options}}.
614 @arg points: The polygon points (L{LatLon}[]).
615 @kwarg closed: Optionally, close the polygon (C{bool}).
616 @kwarg radius: Mean earth radius (C{meter}).
617 @kwarg wrap_adjust_limit: Optional keyword arguments for function
618 L{sphericalTrigonometry.nearestOn3} and
619 L{pygeodesy.equirectangular4},
621 @return: A L{NearestOn3Tuple}C{(closest, distance, angle)} of the
622 C{closest} point (L{LatLon}), the L{pygeodesy.equirectangular4}
623 C{distance} between this and the C{closest} point converted to
624 C{meter}, same units as B{C{radius}}. The C{angle} from this
625 to the C{closest} point is in compass C{degrees360}, like
626 function L{pygeodesy.compassAngle}.
628 @raise LimitError: Lat- and/or longitudinal delta exceeds B{C{limit}},
629 see function L{pygeodesy.equirectangular4}.
631 @raise PointsError: Insufficient number of B{C{points}}.
633 @raise TypeError: Some B{C{points}} are not C{LatLon}.
635 @raise ValueError: Invalid B{C{radius}} or B{C{options}}.
637 @see: Functions L{pygeodesy.compassAngle}, L{pygeodesy.equirectangular4}
638 and L{pygeodesy.nearestOn5}.
639 '''
640 return nearestOn3(self, points, closed=closed, radius=radius,
641 LatLon=self.classof, **wrap_adjust_limit)
643 def toCartesian(self, **Cartesian_datum_kwds): # PYCHOK Cartesian=Cartesian, datum=None
644 '''Convert this point to C{Karney}-based cartesian (ECEF) coordinates.
646 @kwarg Cartesian_datum_kwds: Optional L{Cartesian}, B{C{datum}} and other
647 keyword arguments, ignored if C{B{Cartesian} is
648 None}. Use C{B{Cartesian}=...} to override this
649 L{Cartesian} class or specify C{B{Cartesian}=None}.
651 @return: The cartesian point (L{Cartesian}) or if C{B{Cartesian} is None},
652 an L{Ecef9Tuple}C{(x, y, z, lat, lon, height, C, M, datum)} with C{C}
653 and C{M} if available.
655 @raise TypeError: Invalid B{C{Cartesian_datum_kwds}} argument.
656 '''
657 kwds = _xkwds(Cartesian_datum_kwds, Cartesian=Cartesian, datum=self.datum)
658 return LatLonSphericalBase.toCartesian(self, **kwds)
660 def triangle7(self, otherB, otherC, radius=R_M, wrap=False):
661 '''Compute the angles, sides and area of a spherical triangle.
663 @arg otherB: Second triangle point (C{LatLon}).
664 @arg otherC: Third triangle point (C{LatLon}).
665 @kwarg radius: Mean earth radius, ellipsoid or datum (C{meter}, L{Ellipsoid},
666 L{Ellipsoid2}, L{Datum} or L{a_f2Tuple}) or C{None}.
667 @kwarg wrap: If C{True}, wrap or I{normalize} and unroll points B{C{otherB}}
668 and B{C{otherC}} (C{bool}).
670 @return: L{Triangle7Tuple}C{(A, a, B, b, C, c, area)} or if B{C{radius} is
671 None}, a L{Triangle8Tuple}C{(A, a, B, b, C, c, D, E)}.
673 @see: Function L{triangle7} and U{Spherical trigonometry
674 <https://WikiPedia.org/wiki/Spherical_trigonometry>}.
675 '''
676 B = self.others(otherB=otherB)
677 C = self.others(otherC=otherC)
678 B, C, _ = _unrollon3(self, B, C, wrap)
680 r = self.philam + B.philam + C.philam
681 t = triangle8_(*r, wrap=wrap)
682 return self._xnamed(_t7Tuple(t, radius))
684 def triangulate(self, bearing1, other, bearing2, **height_wrap):
685 '''Locate a point given this, an other point and a bearing from
686 North at both points.
688 @arg bearing1: Bearing at this point (compass C{degrees360}).
689 @arg other: The other point (C{LatLon}).
690 @arg bearing2: Bearing at the other point (compass C{degrees360}).
691 @kwarg height_wrap_tol: Optional keyword arguments C{B{height}=None},
692 C{B{wrap}=False}, see method L{intersection}.
694 @return: Triangulated point (C{LatLon}).
696 @see: Method L{intersection} for further details.
697 '''
698 if _isDegrees(bearing1) and _isDegrees(bearing2):
699 return self.intersection(bearing1, other, bearing2, **height_wrap)
700 raise _TypeError(bearing1=bearing1, bearing2=bearing2, **height_wrap)
702 def trilaterate5(self, distance1, point2, distance2, point3, distance3,
703 area=True, eps=EPS1, radius=R_M, wrap=False):
704 '''Trilaterate three points by I{area overlap} or I{perimeter intersection}
705 of three corresponding circles.
707 @arg distance1: Distance to this point (C{meter}, same units as B{C{radius}}).
708 @arg point2: Second center point (C{LatLon}).
709 @arg distance2: Distance to point2 (C{meter}, same units as B{C{radius}}).
710 @arg point3: Third center point (C{LatLon}).
711 @arg distance3: Distance to point3 (C{meter}, same units as B{C{radius}}).
712 @kwarg area: If C{True}, compute the area overlap, otherwise the perimeter
713 intersection of the circles (C{bool}).
714 @kwarg eps: The required I{minimal overlap} for C{B{area}=True} or the
715 I{intersection margin} if C{B{area}=False} (C{meter}, same
716 units as B{C{radius}}).
717 @kwarg radius: Mean earth radius (C{meter}, conventionally).
718 @kwarg wrap: If C{True}, wrap or I{normalize} and unroll B{C{point2}} and
719 B{C{point3}} (C{bool}).
721 @return: A L{Trilaterate5Tuple}C{(min, minPoint, max, maxPoint, n)} with
722 C{min} and C{max} in C{meter}, same units as B{C{eps}}, the
723 corresponding trilaterated points C{minPoint} and C{maxPoint}
724 as I{spherical} C{LatLon} and C{n}, the number of trilatered
725 points found for the given B{C{eps}}.
727 If only a single trilaterated point is found, C{min I{is} max},
728 C{minPoint I{is} maxPoint} and C{n = 1}.
730 For C{B{area}=True}, C{min} and C{max} are the smallest respectively
731 largest I{radial} overlap found.
733 For C{B{area}=False}, C{min} and C{max} represent the nearest
734 respectively farthest intersection margin.
736 If C{B{area}=True} and all 3 circles are concentric, C{n=0} and
737 C{minPoint} and C{maxPoint} are both the B{C{point#}} with the
738 smallest B{C{distance#}} C{min} and C{max} the largest B{C{distance#}}.
740 @raise IntersectionError: Trilateration failed for the given B{C{eps}},
741 insufficient overlap for C{B{area}=True} or
742 no intersection or all (near-)concentric if
743 C{B{area}=False}.
745 @raise TypeError: Invalid B{C{point2}} or B{C{point3}}.
747 @raise ValueError: Coincident B{C{point2}} or B{C{point3}} or invalid
748 B{C{distance1}}, B{C{distance2}}, B{C{distance3}}
749 or B{C{radius}}.
750 '''
751 return _trilaterate5(self, distance1,
752 self.others(point2=point2), distance2,
753 self.others(point3=point3), distance3,
754 area=area, radius=radius, eps=eps, wrap=wrap)
757_T00 = LatLon(0, 0, name='T00') # reference instance (L{LatLon})
760def areaOf(points, radius=R_M, wrap=False): # was=True
761 '''Calculate the area of a (spherical) polygon or composite (with the
762 points joined by great circle arcs).
764 @arg points: The polygon points or clips (L{LatLon}[], L{BooleanFHP}
765 or L{BooleanGH}).
766 @kwarg radius: Mean earth radius, ellipsoid or datum (C{meter},
767 L{Ellipsoid}, L{Ellipsoid2}, L{Datum} or L{a_f2Tuple})
768 or C{None}.
769 @kwarg wrap: If C{True}, wrap or I{normalize} and unroll the B{C{points}}
770 (C{bool}).
772 @return: Polygon area (C{meter} I{quared}, same units as B{C{radius}}
773 or C{radians} if C{B{radius} is None}).
775 @raise PointsError: Insufficient number of B{C{points}}.
777 @raise TypeError: Some B{C{points}} are not L{LatLon}.
779 @raise ValueError: Invalid B{C{radius}} or semi-circular polygon edge.
781 @note: The area is based on I{Karney}'s U{'Area of a spherical
782 polygon'<https://MathOverflow.net/questions/97711/
783 the-area-of-spherical-polygons>}, 3rd Answer.
785 @see: Functions L{pygeodesy.areaOf}, L{sphericalNvector.areaOf},
786 L{pygeodesy.excessKarney}, L{ellipsoidalExact.areaOf} and
787 L{ellipsoidalKarney.areaOf}.
788 '''
789 if _MODS.booleans.isBoolean(points):
790 return points._sum2(LatLon, areaOf, radius=radius, wrap=wrap)
792 _at2, _t_2 = atan2, tan_2
793 _un, _w180 = unrollPI, wrap180
795 Ps = _T00.PointsIter(points, loop=1, wrap=wrap)
796 p1 = p2 = Ps[0]
797 a1, b1 = p1.philam
798 ta1, z1 = _t_2(a1), None
800 A = Fsum() # mean phi
801 R = Fsum() # see L{pygeodesy.excessKarney_}
802 # ispolar: Summation of course deltas around pole is 0° rather than normally ±360°
803 # <https://blog.Element84.com/determining-if-a-spherical-polygon-contains-a-pole.html>
804 # XXX duplicate of function C{points.ispolar} to avoid copying all iterated points
805 D = Fsum()
806 for i, p2 in Ps.enumerate(closed=True):
807 a2, b2 = p2.philam
808 db, b2 = _un(b1, b2, wrap=wrap and not Ps.looped)
809 A += a2
810 ta2 = _t_2(a2)
811 tdb = _t_2(db, points=i)
812 R += _at2(tdb * (ta1 + ta2),
813 _1_0 + ta1 * ta2)
814 ta1, b1 = ta2, b2
816 if not p2.isequalTo(p1, eps=EPS):
817 z, z2 = _bearingTo2(p1, p2, wrap=wrap)
818 if z1 is not None:
819 D += _w180(z - z1) # (z - z1 + 540) ...
820 D += _w180(z2 - z) # (z2 - z + 540) % 360 - 180
821 p1, z1 = p2, z2
823 R = abs(R * _2_0)
824 if abs(D) < _90_0: # ispolar(points)
825 R = abs(R - PI2)
826 if radius:
827 a = degrees(A.fover(len(A))) # mean lat
828 R *= _mean_radius(radius, a)**2
829 return float(R)
832def _destination2(a, b, r, t):
833 '''(INTERNAL) Destination lat- and longitude in C{radians}.
835 @arg a: Latitude (C{radians}).
836 @arg b: Longitude (C{radians}).
837 @arg r: Angular distance (C{radians}).
838 @arg t: Bearing (compass C{radians}).
840 @return: 2-Tuple (phi, lam) of (C{radians}, C{radiansPI}).
841 '''
842 # see <https://www.EdWilliams.org/avform.htm#LL>
843 sa, ca, sr, cr, st, ct = sincos2_(a, r, t)
844 ca *= sr
846 a = asin1(ct * ca + cr * sa)
847 d = atan2(st * ca, cr - sa * sin(a))
848 # note, in EdWilliams.org/avform.htm W is + and E is -
849 return a, (b + d) # (mod(b + d + PI, PI2) - PI)
852def _int3d2(s, end, wrap, _i_, Vector, hs):
853 # see <https://www.EdWilliams.org/intersect.htm> (5) ff
854 # and similar logic in .ellipsoidalBaseDI._intersect3
855 a1, b1 = s.philam
857 if _isDegrees(end): # bearing, get pseudo-end point
858 a2, b2 = _destination2(a1, b1, PI_4, radians(end))
859 else: # must be a point
860 s.others(end, name=_end_ + _i_)
861 hs.append(end.height)
862 a2, b2 = end.philam
863 if wrap:
864 a2, b2 = _Wrap.philam(a2, b2)
866 db, b2 = unrollPI(b1, b2, wrap=wrap)
867 if max(fabs(db), fabs(a2 - a1)) < EPS:
868 raise _ValueError(_SPACE_(_line_ + _i_, _null_))
869 # note, in EdWilliams.org/avform.htm W is + and E is -
870 sb21, cb21, sb12, cb12 = sincos2_(db * _0_5,
871 -(b1 + b2) * _0_5)
872 cb21 *= sin(a1 - a2) # sa21
873 sb21 *= sin(a1 + a2) # sa12
874 x = Vector(fdot_(sb12, cb21, -cb12, sb21),
875 fdot_(cb12, cb21, sb12, sb21),
876 cos(a1) * cos(a2) * sin(db)) # ll=start
877 return x.unit(), (db, (a2 - a1)) # negated d
880def _intdot(ds, a1, b1, a, b, wrap):
881 # compute dot product ds . (-b + b1, a - a1)
882 db, _ = unrollPI(b1, b, wrap=wrap)
883 return fdot(ds, db, a - a1)
886def intersecant2(center, circle, point, other, **radius_exact_height_wrap):
887 '''Compute the intersections of a circle and a (great circle) line given as
888 two points or as a point and a bearing from North.
890 @arg center: Center of the circle (L{LatLon}).
891 @arg circle: Radius of the circle (C{meter}, same units as the earth
892 B{C{radius}}) or a point on the circle (L{LatLon}).
893 @arg point: A point on the (great circle) line (L{LatLon}).
894 @arg other: An other point on the (great circle) line (L{LatLon}) or
895 the bearing at the B{C{point}} (compass C{degrees360}).
896 @kwarg radius_exact_height_wrap: Optional keyword arguments, see method
897 L{intersecant2<pygeodesy.sphericalBase.LatLonSphericalBase.
898 intersecant2>} for further details.
900 @return: 2-Tuple of the intersection points (representing a chord), each
901 an instance of the B{C{point}} class. Both points are the same
902 instance if the (great circle) line is tangent to the circle.
904 @raise IntersectionError: The circle and line do not intersect.
906 @raise TypeError: If B{C{center}}, B{C{point}}, B{C{circle}} or B{C{other}}
907 not L{LatLon}.
909 @raise UnitError: Invalid B{C{circle}}, B{C{other}}, B{C{radius}},
910 B{C{exact}}, B{C{height}} or B{C{napieradius}}.
911 '''
912 c = _T00.others(center=center)
913 p = _T00.others(point=point)
914 try:
915 return _intersecant2(c, circle, p, other, **radius_exact_height_wrap)
916 except (TypeError, ValueError) as x:
917 raise _xError(x, center=center, circle=circle, point=point, other=other,
918 **radius_exact_height_wrap)
921def _intersect(start1, end1, start2, end2, height=None, wrap=False, # in.ellipsoidalBaseDI._intersect3
922 LatLon=LatLon, **LatLon_kwds):
923 # (INTERNAL) Intersect two (spherical) lines, see L{intersection}
924 # above, separated to allow callers to embellish any exceptions
926 s1, s2 = start1, start2
927 if wrap:
928 s2 = _Wrap.point(s2)
929 hs = [s1.height, s2.height]
931 a1, b1 = s1.philam
932 a2, b2 = s2.philam
933 db, b2 = unrollPI(b1, b2, wrap=wrap)
934 r12 = vincentys_(a2, a1, db)
935 if fabs(r12) < EPS: # [nearly] coincident points
936 a, b = favg(a1, a2), favg(b1, b2)
938 # see <https://www.EdWilliams.org/avform.htm#Intersection>
939 elif _isDegrees(end1) and _isDegrees(end2): # both bearings
940 sa1, ca1, sa2, ca2, sr12, cr12 = sincos2_(a1, a2, r12)
942 x1, x2 = (sr12 * ca1), (sr12 * ca2)
943 if isnear0(x1) or isnear0(x2):
944 raise IntersectionError(_parallel_)
945 # handle domain error for equivalent longitudes,
946 # see also functions asin_safe and acos_safe at
947 # <https://www.EdWilliams.org/avform.htm#Math>
948 t12, t13 = acos1((sa2 - sa1 * cr12) / x1), radiansPI2(end1)
949 t21, t23 = acos1((sa1 - sa2 * cr12) / x2), radiansPI2(end2)
950 if sin(db) > 0:
951 t21 = PI2 - t21
952 else:
953 t12 = PI2 - t12
954 sx1, cx1, sx2, cx2 = sincos2_(wrapPI(t13 - t12), # angle 2-1-3
955 wrapPI(t21 - t23)) # angle 1-2-3)
956 if isnear0(sx1) and isnear0(sx2):
957 raise IntersectionError(_infinite_)
958 sx3 = sx1 * sx2
959# XXX if sx3 < 0:
960# XXX raise ValueError(_ambiguous_)
961 x3 = acos1(cr12 * sx3 - cx2 * cx1)
962 r13 = atan2(sr12 * sx3, cx2 + cx1 * cos(x3))
964 a, b = _destination2(a1, b1, r13, t13)
965 # like .ellipsoidalBaseDI,_intersect3, if this intersection
966 # is "before" the first point, use the antipodal intersection
967 if opposing_(t13, bearing_(a1, b1, a, b, wrap=wrap)):
968 a, b = antipode_(a, b) # PYCHOK PhiLam2Tuple
970 else: # end point(s) or bearing(s)
971 _N_vector_ = _MODS.nvectorBase._N_vector_
973 x1, d1 = _int3d2(s1, end1, wrap, _1_, _N_vector_, hs)
974 x2, d2 = _int3d2(s2, end2, wrap, _2_, _N_vector_, hs)
975 x = x1.cross(x2)
976 if x.length < EPS: # [nearly] colinear or parallel lines
977 raise IntersectionError(_colinear_)
978 a, b = x.philam
979 # choose intersection similar to sphericalNvector
980 if not (_intdot(d1, a1, b1, a, b, wrap) *
981 _intdot(d2, a2, b2, a, b, wrap)) > 0:
982 a, b = antipode_(a, b) # PYCHOK PhiLam2Tuple
984 h = fmean(hs) if height is None else Height(height)
985 return _LL3Tuple(degrees90(a), degrees180(b), h,
986 intersection, LatLon, LatLon_kwds)
989def intersection(start1, end1, start2, end2, height=None, wrap=False,
990 **LatLon_and_kwds):
991 '''Compute the intersection point of two lines, each defined by
992 two points or by a start point and a bearing from North.
994 @arg start1: Start point of the first line (L{LatLon}).
995 @arg end1: End point of the first line (L{LatLon}) or the bearing
996 at the first start point (compass C{degrees360}).
997 @arg start2: Start point of the second line (L{LatLon}).
998 @arg end2: End point of the second line (L{LatLon}) or the bearing
999 at the second start point (compass C{degrees360}).
1000 @kwarg height: Optional height for the intersection point,
1001 overriding the mean height (C{meter}).
1002 @kwarg wrap: If C{True}, wrap or I{normalize} and unroll B{C{start2}}
1003 and both B{C{end*}} points (C{bool}).
1004 @kwarg LatLon_and_kwds: Optional class C{B{LatLon}=}L{LatLon} to use
1005 for the intersection point and optionally additional
1006 B{C{LatLon}} keyword arguments, ignored if C{B{LatLon}
1007 is None}.
1009 @return: The intersection point as a (B{C{LatLon}}) or if C{B{LatLon}
1010 is None} a L{LatLon3Tuple}C{(lat, lon, height)}. An alternate
1011 intersection point might be the L{antipode} to the returned result.
1013 @raise IntersectionError: Ambiguous or infinite intersection or colinear,
1014 parallel or otherwise non-intersecting lines.
1016 @raise TypeError: A B{C{start1}}, B{C{end1}}, B{C{start2}} or B{C{end2}}
1017 point not L{LatLon}.
1019 @raise ValueError: Invalid B{C{height}} or C{null} line.
1020 '''
1021 s1 = _T00.others(start1=start1)
1022 s2 = _T00.others(start2=start2)
1023 try:
1024 return _intersect(s1, end1, s2, end2, height=height, wrap=wrap, **LatLon_and_kwds)
1025 except (TypeError, ValueError) as x:
1026 raise _xError(x, start1=start1, end1=end1, start2=start2, end2=end2)
1029def intersections2(center1, rad1, center2, rad2, radius=R_M, eps=_0_0,
1030 height=None, wrap=False, # was=True
1031 **LatLon_and_kwds):
1032 '''Compute the intersection points of two circles each defined by a
1033 center point and a radius.
1035 @arg center1: Center of the first circle (L{LatLon}).
1036 @arg rad1: Radius of the first circle (C{meter} or C{radians}, see
1037 B{C{radius}}).
1038 @arg center2: Center of the second circle (L{LatLon}).
1039 @arg rad2: Radius of the second circle (C{meter} or C{radians}, see
1040 B{C{radius}}).
1041 @kwarg radius: Mean earth radius (C{meter} or C{None} if B{C{rad1}},
1042 B{C{rad2}} and B{C{eps}} are given in C{radians}).
1043 @kwarg eps: Required overlap (C{meter} or C{radians}, see B{C{radius}}).
1044 @kwarg height: Optional height for the intersection points (C{meter},
1045 conventionally) or C{None} for the I{"radical height"}
1046 at the I{radical line} between both centers.
1047 @kwarg wrap: If C{True}, wrap or I{normalize} and unroll B{C{center2}}
1048 (C{bool}).
1049 @kwarg LatLon_and_kwds: Optional class C{B{LatLon}=}L{LatLon} to use for
1050 the intersection points and optionally additional B{C{LatLon}}
1051 keyword arguments, ignored if C{B{LatLon} is None}.
1053 @return: 2-Tuple of the intersection points, each a B{C{LatLon}}
1054 instance or if C{B{LatLon} is None} a L{LatLon3Tuple}C{(lat,
1055 lon, height)}. For abutting circles, both intersection
1056 points are the same instance, aka the I{radical center}.
1058 @raise IntersectionError: Concentric, antipodal, invalid or
1059 non-intersecting circles.
1061 @raise TypeError: If B{C{center1}} or B{C{center2}} not L{LatLon}.
1063 @raise ValueError: Invalid B{C{rad1}}, B{C{rad2}}, B{C{radius}},
1064 B{C{eps}} or B{C{height}}.
1066 @note: Courtesy of U{Samuel Čavoj<https://GitHub.com/mrJean1/PyGeodesy/issues/41>}.
1068 @see: This U{Answer<https://StackOverflow.com/questions/53324667/
1069 find-intersection-coordinates-of-two-circles-on-earth/53331953>}.
1070 '''
1071 c1 = _T00.others(center1=center1)
1072 c2 = _T00.others(center2=center2)
1073 try:
1074 return _intersects2(c1, rad1, c2, rad2, radius=radius, eps=eps,
1075 height=height, wrap=wrap,
1076 **LatLon_and_kwds)
1077 except (TypeError, ValueError) as x:
1078 raise _xError(x, center1=center1, rad1=rad1,
1079 center2=center2, rad2=rad2, wrap=wrap)
1082def _intersects2(c1, rad1, c2, rad2, radius=R_M, eps=_0_0, # in .ellipsoidalBaseDI._intersects2
1083 height=None, too_d=None, wrap=False, # was=True
1084 LatLon=LatLon, **LatLon_kwds):
1085 # (INTERNAL) Intersect two spherical circles, see L{intersections2}
1086 # above, separated to allow callers to embellish any exceptions
1088 def _dest3(bearing, h):
1089 a, b = _destination2(a1, b1, r1, bearing)
1090 return _LL3Tuple(degrees90(a), degrees180(b), h,
1091 intersections2, LatLon, LatLon_kwds)
1093 a1, b1 = c1.philam
1094 a2, b2 = c2.philam
1095 if wrap:
1096 a2, b2 = _Wrap.philam(a2, b2)
1098 r1, r2, f = _rads3(rad1, rad2, radius)
1099 if f: # swapped radii, swap centers
1100 a1, a2 = a2, a1 # PYCHOK swap!
1101 b1, b2 = b2, b1 # PYCHOK swap!
1103 db, b2 = unrollPI(b1, b2, wrap=wrap)
1104 d = vincentys_(a2, a1, db) # radians
1105 if d < max(r1 - r2, EPS):
1106 raise IntersectionError(_near_(_concentric_)) # XXX ConcentricError?
1108 r = eps if radius is None else (m2radians(
1109 eps, radius=radius) if eps else _0_0)
1110 if r < _0_0:
1111 raise _ValueError(eps=r)
1113 x = fsumf_(r1, r2, -d) # overlap
1114 if x > max(r, EPS):
1115 sd, cd, sr1, cr1, _, cr2 = sincos2_(d, r1, r2)
1116 x = sd * sr1
1117 if isnear0(x):
1118 raise _ValueError(_invalid_)
1119 x = acos1((cr2 - cd * cr1) / x) # 0 <= x <= PI
1121 elif x < r: # PYCHOK no cover
1122 t = (d * radius) if too_d is None else too_d
1123 raise IntersectionError(_too_(_Fmt.distant(t)))
1125 if height is None: # "radical height"
1126 f = _radical2(d, r1, r2).ratio
1127 h = Height(favg(c1.height, c2.height, f=f))
1128 else:
1129 h = Height(height)
1131 b = bearing_(a1, b1, a2, b2, final=False, wrap=wrap)
1132 if x < EPS4: # externally ...
1133 r = _dest3(b, h)
1134 elif x > _PI_EPS4: # internally ...
1135 r = _dest3(b + PI, h)
1136 else:
1137 return _dest3(b + x, h), _dest3(b - x, h)
1138 return r, r # ... abutting circles
1141@deprecated_function
1142def isPoleEnclosedBy(points, wrap=False): # PYCHOK no cover
1143 '''DEPRECATED, use function L{pygeodesy.ispolar}.
1144 '''
1145 return ispolar(points, wrap=wrap)
1148def _LL3Tuple(lat, lon, height, where, LatLon, LatLon_kwds):
1149 '''(INTERNAL) Helper for L{intersection}, L{intersections2} and L{meanOf}.
1150 '''
1151 n = typename(where)
1152 if LatLon is None:
1153 r = LatLon3Tuple(lat, lon, height, name=n)
1154 else:
1155 kwds = _xkwds(LatLon_kwds, height=height, name=n)
1156 r = LatLon(lat, lon, **kwds)
1157 return r
1160def meanOf(points, height=None, wrap=False, LatLon=LatLon, **LatLon_kwds):
1161 '''Compute the I{geographic} mean of several points.
1163 @arg points: Points to be averaged (L{LatLon}[]).
1164 @kwarg height: Optional height at mean point, overriding the mean height
1165 (C{meter}).
1166 @kwarg wrap: If C{True}, wrap or I{normalize} the B{C{points}} (C{bool}).
1167 @kwarg LatLon: Optional class to return the mean point (L{LatLon}) or C{None}.
1168 @kwarg LatLon_kwds: Optional, additional B{C{LatLon}} keyword arguments,
1169 ignored if C{B{LatLon} is None}.
1171 @return: The geographic mean and height (B{C{LatLon}}) or if C{B{LatLon}
1172 is None}, a L{LatLon3Tuple}C{(lat, lon, height)}.
1174 @raise TypeError: Some B{C{points}} are not L{LatLon}.
1176 @raise ValueError: No B{C{points}} or invalid B{C{height}}.
1177 '''
1178 def _N_vs(ps, w):
1179 Ps = _T00.PointsIter(ps, wrap=w)
1180 for p in Ps.iterate(closed=False):
1181 yield p._N_vector
1183 m = _MODS.nvectorBase
1184 # geographic, vectorial mean
1185 n = m.sumOf(_N_vs(points, wrap), h=height, Vector=m.NvectorBase)
1186 lat, lon, h = n.latlonheight
1187 return _LL3Tuple(lat, lon, h, meanOf, LatLon, LatLon_kwds)
1190@deprecated_function
1191def nearestOn2(point, points, **closed_radius_LatLon_options): # PYCHOK no cover
1192 '''DEPRECATED, use function L{sphericalTrigonometry.nearestOn3}.
1194 @return: ... 2-tuple C{(closest, distance)} of the C{closest}
1195 point (L{LatLon}) on the polygon and the C{distance}
1196 between the C{closest} and the given B{C{point}}. The
1197 C{closest} is a B{C{LatLon}} or a L{LatLon2Tuple}C{(lat,
1198 lon)} if C{B{LatLon} is None} ...
1199 '''
1200 ll, d, _ = nearestOn3(point, points, **closed_radius_LatLon_options) # PYCHOK 3-tuple
1201 if _xkwds_get(closed_radius_LatLon_options, LatLon=LatLon) is None:
1202 ll = LatLon2Tuple(ll.lat, ll.lon)
1203 return ll, d
1206def nearestOn3(point, points, closed=False, radius=R_M, wrap=False, adjust=True,
1207 limit=9, **LatLon_and_kwds):
1208 '''Locate the point on a path or polygon closest to a reference point.
1210 Distances are I{approximated} using function L{equirectangular4
1211 <pygeodesy.equirectangular4>}, subject to the supplied B{C{options}}.
1213 @arg point: The reference point (L{LatLon}).
1214 @arg points: The path or polygon points (L{LatLon}[]).
1215 @kwarg closed: Optionally, close the polygon (C{bool}).
1216 @kwarg radius: Mean earth radius (C{meter}).
1217 @kwarg wrap: If C{True}, wrap or I{normalize} and unroll the
1218 B{C{points}} (C{bool}).
1219 @kwarg adjust: See function L{equirectangular4<pygeodesy.equirectangular4>}
1220 (C{bool}).
1221 @kwarg limit: See function L{equirectangular4<pygeodesy.equirectangular4>}
1222 (C{degrees}), default C{9 degrees} is about C{1,000 Km} (for
1223 (mean spherical earth radius L{R_KM}).
1224 @kwarg LatLon_and_kwds: Optional class C{B{LatLon}=L{LatLon}} to return the
1225 closest point and optionally additional C{B{LatLon}} keyword
1226 arguments or specify C{B{LatLon}=None}.
1228 @return: A L{NearestOn3Tuple}C{(closest, distance, angle)} with the
1229 C{closest} point as B{C{LatLon}} or L{LatLon3Tuple}C{(lat,
1230 lon, height)} if C{B{LatLon} is None}. The C{distance} is
1231 the L{equirectangular4<pygeodesy.equirectangular4>} distance
1232 between the C{closest} and the given B{C{point}} converted to
1233 C{meter}, same units as B{C{radius}}. The C{angle} from the
1234 given B{C{point}} to the C{closest} is in compass C{degrees360},
1235 like function L{compassAngle<pygeodesy.compassAngle>}. The
1236 C{height} is the (interpolated) height at the C{closest} point.
1238 @raise LimitError: Lat- and/or longitudinal delta exceeds the B{C{limit}},
1239 see function L{equirectangular4<pygeodesy.equirectangular4>}.
1241 @raise PointsError: Insufficient number of B{C{points}}.
1243 @raise TypeError: Some B{C{points}} are not C{LatLon}.
1245 @raise ValueError: Invalid B{C{radius}}.
1247 @see: Functions L{equirectangular4<pygeodesy.equirectangular4>} and
1248 L{nearestOn5<pygeodesy.nearestOn5>}.
1249 '''
1250 t = _nearestOn5(point, points, closed=closed, wrap=wrap,
1251 adjust=adjust, limit=limit)
1252 d = degrees2m(t.distance, radius=radius)
1253 h = t.height
1254 n = typename(nearestOn3)
1256 LL, kwds = _xkwds_pop2(LatLon_and_kwds, LatLon=LatLon)
1257 r = LatLon3Tuple(t.lat, t.lon, h, name=n) if LL is None else \
1258 LL(t.lat, t.lon, **_xkwds(kwds, height=h, name=n))
1259 return NearestOn3Tuple(r, d, t.angle, name=n)
1262def perimeterOf(points, closed=False, radius=R_M, wrap=True):
1263 '''Compute the perimeter of a (spherical) polygon or composite
1264 (with great circle arcs joining the points).
1266 @arg points: The polygon points or clips (L{LatLon}[], L{BooleanFHP}
1267 or L{BooleanGH}).
1268 @kwarg closed: Optionally, close the polygon (C{bool}).
1269 @kwarg radius: Mean earth radius (C{meter}) or C{None}.
1270 @kwarg wrap: If C{True}, wrap or I{normalize} and unroll the
1271 B{C{points}} (C{bool}).
1273 @return: Polygon perimeter (C{meter}, same units as B{C{radius}}
1274 or C{radians} if C{B{radius} is None}).
1276 @raise PointsError: Insufficient number of B{C{points}}.
1278 @raise TypeError: Some B{C{points}} are not L{LatLon}.
1280 @raise ValueError: Invalid B{C{radius}} or C{B{closed}=False} with
1281 C{B{points}} a composite.
1283 @note: Distances are based on function L{vincentys_<pygeodesy.vincentys_>}.
1285 @see: Functions L{perimeterOf<pygeodesy.perimeterOf>},
1286 L{sphericalNvector.perimeterOf} and L{ellipsoidalKarney.perimeterOf}.
1287 '''
1288 def _rads(ps, c, w): # angular edge lengths in radians
1289 Ps = _T00.PointsIter(ps, loop=1, wrap=w)
1290 a1, b1 = Ps[0].philam
1291 for p in Ps.iterate(closed=c):
1292 a2, b2 = p.philam
1293 db, b2 = unrollPI(b1, b2, wrap=w and not (c and Ps.looped))
1294 yield vincentys_(a2, a1, db)
1295 a1, b1 = a2, b2
1297 if _MODS.booleans.isBoolean(points):
1298 if not closed:
1299 raise _ValueError(closed=closed, points=_composite_)
1300 r = points._sum2(LatLon, perimeterOf, closed=True, radius=radius, wrap=wrap)
1301 else:
1302 r = fsum(_rads(points, closed, wrap))
1303 return _radians2m(r, radius)
1306def triangle7(latA, lonA, latB, lonB, latC, lonC, radius=R_M,
1307 excess=excessAbc_,
1308 wrap=False):
1309 '''Compute the angles, sides, and area of a (spherical) triangle.
1311 @arg latA: First corner latitude (C{degrees}).
1312 @arg lonA: First corner longitude (C{degrees}).
1313 @arg latB: Second corner latitude (C{degrees}).
1314 @arg lonB: Second corner longitude (C{degrees}).
1315 @arg latC: Third corner latitude (C{degrees}).
1316 @arg lonC: Third corner longitude (C{degrees}).
1317 @kwarg radius: Mean earth radius, ellipsoid or datum (C{meter},
1318 L{Ellipsoid}, L{Ellipsoid2}, L{Datum} or L{a_f2Tuple})
1319 or C{None}.
1320 @kwarg excess: I{Spherical excess} callable (L{excessAbc_},
1321 L{excessGirard_} or L{excessLHuilier_}).
1322 @kwarg wrap: If C{True}, wrap and L{unroll180<pygeodesy.unroll180>}
1323 longitudes (C{bool}).
1325 @return: A L{Triangle7Tuple}C{(A, a, B, b, C, c, area)} with
1326 spherical angles C{A}, C{B} and C{C}, angular sides
1327 C{a}, C{b} and C{c} all in C{degrees} and C{area}
1328 in I{square} C{meter} or same units as B{C{radius}}
1329 I{squared} or if C{B{radius}=0} or C{None}, a
1330 L{Triangle8Tuple}C{(A, a, B, b, C, c, D, E)} with
1331 I{spherical deficit} C{D} and I{spherical excess}
1332 C{E} as the C{unit area}, all in C{radians}.
1333 '''
1334 t = triangle8_(Phid(latA=latA), Lamd(lonA=lonA),
1335 Phid(latB=latB), Lamd(lonB=lonB),
1336 Phid(latC=latC), Lamd(lonC=lonC),
1337 excess=excess, wrap=wrap)
1338 return _t7Tuple(t, radius)
1341def triangle8_(phiA, lamA, phiB, lamB, phiC, lamC, excess=excessAbc_,
1342 wrap=False):
1343 '''Compute the angles, sides, I{spherical deficit} and I{spherical
1344 excess} of a (spherical) triangle.
1346 @arg phiA: First corner latitude (C{radians}).
1347 @arg lamA: First corner longitude (C{radians}).
1348 @arg phiB: Second corner latitude (C{radians}).
1349 @arg lamB: Second corner longitude (C{radians}).
1350 @arg phiC: Third corner latitude (C{radians}).
1351 @arg lamC: Third corner longitude (C{radians}).
1352 @kwarg excess: I{Spherical excess} callable (L{excessAbc_},
1353 L{excessGirard_} or L{excessLHuilier_}).
1354 @kwarg wrap: If C{True}, L{unrollPI<pygeodesy.unrollPI>} the
1355 longitudinal deltas (C{bool}).
1357 @return: A L{Triangle8Tuple}C{(A, a, B, b, C, c, D, E)} with
1358 spherical angles C{A}, C{B} and C{C}, angular sides
1359 C{a}, C{b} and C{c}, I{spherical deficit} C{D} and
1360 I{spherical excess} C{E}, all in C{radians}.
1361 '''
1362 def _a_r(w, phiA, lamA, phiB, lamB, phiC, lamC):
1363 d, _ = unrollPI(lamB, lamC, wrap=w)
1364 a = vincentys_(phiC, phiB, d)
1365 return a, (phiB, lamB, phiC, lamC, phiA, lamA) # rotate A, B, C
1367 def _A_r(a, sa, ca, sb, cb, sc, cc):
1368 s = sb * sc
1369 A = acos1((ca - cb * cc) / s) if isnon0(s) else a
1370 return A, (sb, cb, sc, cc, sa, ca) # rotate sincos2_'s
1372 # notation: side C{a} is oposite to corner C{A}, etc.
1373 a, r = _a_r(wrap, phiA, lamA, phiB, lamB, phiC, lamC)
1374 b, r = _a_r(wrap, *r)
1375 c, _ = _a_r(wrap, *r)
1377 A, r = _A_r(a, *sincos2_(a, b, c))
1378 B, r = _A_r(b, *r)
1379 C, _ = _A_r(c, *r)
1381 D = fsumf_(PI2, -a, -b, -c) # deficit aka defect
1382 E = excessGirard_(A, B, C) if _isin(excess, excessGirard_, True) else (
1383 excessLHuilier_(a, b, c) if _isin(excess, excessLHuilier_, False) else
1384 excessAbc_(*max((A, b, c), (B, c, a), (C, a, b))))
1386 return Triangle8Tuple(A, a, B, b, C, c, D, E)
1389def _t7Tuple(t, radius):
1390 '''(INTERNAL) Convert a L{Triangle8Tuple} to L{Triangle7Tuple}.
1391 '''
1392 if radius: # not _isin(radius, None, _0_0)
1393 r = radius if _isRadius(radius) else \
1394 _ellipsoidal_datum(radius).ellipsoid.Rmean
1395 A, B, C = map1(degrees, t.A, t.B, t.C)
1396 t = Triangle7Tuple(A, (r * t.a),
1397 B, (r * t.b),
1398 C, (r * t.c), t.E * r**2)
1399 return t
1402__all__ += _ALL_OTHER(Cartesian, LatLon, # classes
1403 areaOf, # functions
1404 intersecant2, intersection, intersections2, ispolar,
1405 isPoleEnclosedBy, # DEPRECATED, use ispolar
1406 meanOf,
1407 nearestOn2, nearestOn3,
1408 perimeterOf,
1409 sumOf, # XXX == vector3d.sumOf
1410 triangle7, triangle8_)
1412# **) MIT License
1413#
1414# Copyright (C) 2016-2025 -- mrJean1 at Gmail -- All Rights Reserved.
1415#
1416# Permission is hereby granted, free of charge, to any person obtaining a
1417# copy of this software and associated documentation files (the "Software"),
1418# to deal in the Software without restriction, including without limitation
1419# the rights to use, copy, modify, merge, publish, distribute, sublicense,
1420# and/or sell copies of the Software, and to permit persons to whom the
1421# Software is furnished to do so, subject to the following conditions:
1422#
1423# The above copyright notice and this permission notice shall be included
1424# in all copies or substantial portions of the Software.
1425#
1426# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
1427# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
1428# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
1429# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
1430# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
1431# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
1432# OTHER DEALINGS IN THE SOFTWARE.