Coverage for pygeodesy/sphericalTrigonometry.py: 93%
388 statements
« prev ^ index » next coverage.py v7.6.1, created at 2025-05-23 16:31 -0400
« prev ^ index » next coverage.py v7.6.1, created at 2025-05-23 16:31 -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 _ # noqa: E702 ;
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.latlonBase import _trilaterate5
38from pygeodesy.lazily import _ALL_LAZY, _ALL_MODS as _MODS, _ALL_OTHER
39# from pygeodesy.nvectorBase import NvectorBase, sumOf # _MODS
40from pygeodesy.namedTuples import LatLon2Tuple, LatLon3Tuple, NearestOn3Tuple, \
41 Triangle7Tuple, Triangle8Tuple
42from pygeodesy.points import ispolar, nearestOn5 as _nearestOn5, \
43 Fmt as _Fmt # XXX shadowed
44from pygeodesy.props import deprecated_function, deprecated_method
45from pygeodesy.sphericalBase import _m2radians, CartesianSphericalBase, \
46 _intersecant2, LatLonSphericalBase, \
47 _rads3, _radians2m
48# from pygeodesy.streprs import Fmt as _Fmt # from .points XXX shadowed
49from pygeodesy.units import Bearing_, Height, _isDegrees, _isRadius, Lamd, \
50 Phid, Radius_, Scalar
51from pygeodesy.utily import acos1, asin1, atan1d, atan2, atan2d, degrees90, \
52 degrees180, degrees2m, m2radians, radiansPI2, \
53 sincos2_, tan_2, unrollPI, _unrollon, _unrollon3, \
54 wrap180, wrapPI, _Wrap
55from pygeodesy.vector3d import sumOf, Vector3d
57from math import asin, cos, degrees, fabs, radians, sin
59__all__ = _ALL_LAZY.sphericalTrigonometry
60__version__ = '25.05.12'
62_PI_EPS4 = PI - EPS4
63if _PI_EPS4 >= PI:
64 raise _AssertionError(EPS4=EPS4, PI=PI, PI_EPS4=_PI_EPS4)
67class Cartesian(CartesianSphericalBase):
68 '''Extended to convert geocentric, L{Cartesian} points to
69 spherical, geodetic L{LatLon}.
70 '''
72 def toLatLon(self, **LatLon_and_kwds): # PYCHOK LatLon=LatLon
73 '''Convert this cartesian point to a C{spherical} geodetic point.
75 @kwarg LatLon_and_kwds: Optional L{LatLon} and L{LatLon} keyword
76 arguments. Use C{B{LatLon}=...} to override
77 this L{LatLon} class or specify C{B{LatLon}=None}.
79 @return: The geodetic point (L{LatLon}) or if C{B{LatLon} is None},
80 an L{Ecef9Tuple}C{(x, y, z, lat, lon, height, C, M, datum)}
81 with C{C} and C{M} if available.
83 @raise TypeError: Invalid B{C{LatLon_and_kwds}} argument.
84 '''
85 kwds = _xkwds(LatLon_and_kwds, LatLon=LatLon, datum=self.datum)
86 return CartesianSphericalBase.toLatLon(self, **kwds)
89class LatLon(LatLonSphericalBase):
90 '''New point on a spherical earth model, based on trigonometry formulae.
91 '''
93 def _ab1_ab2_db5(self, other, wrap):
94 '''(INTERNAL) Helper for several methods.
95 '''
96 a1, b1 = self.philam
97 a2, b2 = self.others(other, up=2).philam
98 if wrap:
99 a2, b2 = _Wrap.philam(a2, b2)
100 db, b2 = unrollPI(b1, b2, wrap=wrap)
101 else: # unrollPI shortcut
102 db = b2 - b1
103 return a1, b1, a2, b2, db
105 def alongTrackDistanceTo(self, start, end, radius=R_M, wrap=False):
106 '''Compute the (signed) distance from the start to the closest
107 point on the great circle line defined by a start and an
108 end point.
110 That is, if a perpendicular is drawn from this point to the
111 great circle line, the along-track distance is the distance
112 from the start point to the point where the perpendicular
113 crosses the line.
115 @arg start: Start point of the great circle line (L{LatLon}).
116 @arg end: End point of the great circle line (L{LatLon}).
117 @kwarg radius: Mean earth radius (C{meter}) or C{None}.
118 @kwarg wrap: If C{True}, wrap or I{normalize} and unroll
119 the B{C{start}} and B{C{end}} point (C{bool}).
121 @return: Distance along the great circle line (C{radians}
122 if C{B{radius} is None} or C{meter}, same units
123 as B{C{radius}}), positive if I{after} the
124 B{C{start}} toward the B{C{end}} point of the
125 line, I{negative} if before or C{0} if at the
126 B{C{start}} point.
128 @raise TypeError: Invalid B{C{start}} or B{C{end}} point.
130 @raise ValueError: Invalid B{C{radius}}.
131 '''
132 r, x, b = self._a_x_b3(start, end, radius, wrap)
133 cx = cos(x)
134 return _0_0 if isnear0(cx) else \
135 _radians2m(copysign0(acos1(cos(r) / cx), cos(b)), radius)
137 def _a_x_b3(self, start, end, radius, wrap):
138 '''(INTERNAL) Helper for .along-/crossTrackDistanceTo.
139 '''
140 s = self.others(start=start)
141 e = self.others(end=end)
142 s, e, w = _unrollon3(self, s, e, wrap)
144 r = Radius_(radius)
145 r = s.distanceTo(self, r, wrap=w) / r
147 b = radians(s.initialBearingTo(self, wrap=w)
148 - s.initialBearingTo(e, wrap=w))
149 x = asin(sin(r) * sin(b))
150 return r, x, -b
152 @deprecated_method
153 def bearingTo(self, other, wrap=False, raiser=False): # PYCHOK no cover
154 '''DEPRECATED, use method L{initialBearingTo}.
155 '''
156 return self.initialBearingTo(other, wrap=wrap, raiser=raiser)
158 def crossingParallels(self, other, lat, wrap=False):
159 '''Return the pair of meridians at which a great circle defined
160 by this and an other point crosses the given latitude.
162 @arg other: The other point defining great circle (L{LatLon}).
163 @arg lat: Latitude at the crossing (C{degrees}).
164 @kwarg wrap: If C{True}, wrap or I{normalize} and unroll the
165 B{C{other}} point (C{bool}).
167 @return: 2-Tuple C{(lon1, lon2)}, both in C{degrees180} or
168 C{None} if the great circle doesn't reach B{C{lat}}.
169 '''
170 a1, b1, a2, b2, db = self._ab1_ab2_db5(other, wrap)
171 sa, ca, sa1, ca1, \
172 sa2, ca2, sdb, cdb = sincos2_(radians(lat), a1, a2, db)
173 sa1 *= ca2 * ca
175 x = sa1 * sdb
176 y = sa1 * cdb - ca1 * sa2 * ca
177 z = ca1 * sdb * ca2 * sa
179 h = hypot(x, y)
180 if h < EPS or fabs(z) > h: # PYCHOK no cover
181 return None # great circle doesn't reach latitude
183 m = atan2(-y, x) + b1 # longitude at max latitude
184 d = acos1(z / h) # delta longitude to intersections
185 return degrees180(m - d), degrees180(m + d)
187 def crossTrackDistanceTo(self, start, end, radius=R_M, wrap=False):
188 '''Compute the (signed) distance from this point to a great
189 circle from a start to an end point.
191 @arg start: Start point of the great circle line (L{LatLon}).
192 @arg end: End point of the great circle line (L{LatLon}).
193 @kwarg radius: Mean earth radius (C{meter}) or C{None}.
194 @kwarg wrap: If C{True}, wrap or I{normalize} and unroll
195 the B{C{start}} and B{C{end}} point (C{bool}).
197 @return: Distance to the great circle (C{radians} if
198 B{C{radius}} or C{meter}, same units as
199 B{C{radius}}), I{negative} if to the left or
200 I{positive} if to the right of the line.
202 @raise TypeError: If B{C{start}} or B{C{end}} is not L{LatLon}.
204 @raise ValueError: Invalid B{C{radius}}.
205 '''
206 _, x, _ = self._a_x_b3(start, end, radius, wrap)
207 return _radians2m(x, radius)
209 def destination(self, distance, bearing, radius=R_M, height=None):
210 '''Locate the destination from this point after having
211 travelled the given distance on a bearing from North.
213 @arg distance: Distance travelled (C{meter}, same units as
214 B{C{radius}}).
215 @arg bearing: Bearing from this point (compass C{degrees360}).
216 @kwarg radius: Mean earth radius (C{meter}).
217 @kwarg height: Optional height at destination (C{meter}, same
218 units a B{C{radius}}).
220 @return: Destination point (L{LatLon}).
222 @raise ValueError: Invalid B{C{distance}}, B{C{bearing}},
223 B{C{radius}} or B{C{height}}.
224 '''
225 a, b = self.philam
226 r, t = _m2radians(distance, radius, low=None), Bearing_(bearing)
228 a, b = _destination2(a, b, r, t)
229 h = self._heigHt(height)
230 return self.classof(degrees90(a), degrees180(b), height=h)
232 def distanceTo(self, other, radius=R_M, wrap=False):
233 '''Compute the (angular) distance from this to an other point.
235 @arg other: The other point (L{LatLon}).
236 @kwarg radius: Mean earth radius (C{meter}) or C{None}.
237 @kwarg wrap: If C{True}, wrap or I{normalize} and unroll
238 the B{C{other}} point (C{bool}).
240 @return: Distance between this and the B{C{other}} point
241 (C{meter}, same units as B{C{radius}} or
242 C{radians} if C{B{radius} is None}).
244 @raise TypeError: The B{C{other}} point is not L{LatLon}.
246 @raise ValueError: Invalid B{C{radius}}.
247 '''
248 a1, _, a2, _, db = self._ab1_ab2_db5(other, wrap)
249 return _radians2m(vincentys_(a2, a1, db), radius)
251# @Property_RO
252# def Ecef(self):
253# '''Get the ECEF I{class} (L{EcefVeness}), I{lazily}.
254# '''
255# return _MODS.ecef.EcefKarney
257 def greatCircle(self, bearing, Vector=Vector3d, **Vector_kwds):
258 '''Compute the vector normal to great circle obtained by heading
259 from this point on the bearing from North.
261 Direction of vector is such that initial bearing vector
262 b = c × n, where n is an n-vector representing this point.
264 @arg bearing: Bearing from this point (compass C{degrees360}).
265 @kwarg Vector: Vector class to return the great circle,
266 overriding the default L{Vector3d}.
267 @kwarg Vector_kwds: Optional, additional keyword argunents
268 for B{C{Vector}}.
270 @return: Vector representing great circle (C{Vector}).
272 @raise ValueError: Invalid B{C{bearing}}.
273 '''
274 a, b = self.philam
275 sa, ca, sb, cb, st, ct = sincos2_(a, b, Bearing_(bearing))
277 sa *= st
278 return Vector(fdot_(sb, ct, -cb, sa),
279 -fdot_(cb, ct, sb, sa),
280 ca * st, **Vector_kwds) # XXX .unit()?
282 def initialBearingTo(self, other, wrap=False, raiser=False):
283 '''Compute the initial bearing (forward azimuth) from this
284 to an other point.
286 @arg other: The other point (spherical L{LatLon}).
287 @kwarg wrap: If C{True}, wrap or I{normalize} and unroll
288 the B{C{other}} point (C{bool}).
289 @kwarg raiser: Optionally, raise L{CrossError} (C{bool}),
290 use C{B{raiser}=True} for behavior like
291 C{sphericalNvector.LatLon.initialBearingTo}.
293 @return: Initial bearing (compass C{degrees360}).
295 @raise CrossError: If this and the B{C{other}} point coincide
296 and if B{C{raiser}} and L{crosserrors
297 <pygeodesy.crosserrors>} are both C{True}.
299 @raise TypeError: The B{C{other}} point is not L{LatLon}.
300 '''
301 a1, b1, a2, b2, db = self._ab1_ab2_db5(other, wrap)
302 # XXX behavior like sphericalNvector.LatLon.initialBearingTo
303 if raiser and crosserrors() and max(fabs(a2 - a1), fabs(db)) < EPS:
304 raise CrossError(_point_, self, other=other, wrap=wrap, txt=_coincident_)
306 return degrees(bearing_(a1, b1, a2, b2, final=False))
308 def intermediateTo(self, other, fraction, height=None, wrap=False):
309 '''Locate the point at given fraction between (or along) this
310 and an other point.
312 @arg other: The other point (L{LatLon}).
313 @arg fraction: Fraction between both points (C{scalar},
314 0.0 at this and 1.0 at the other point).
315 @kwarg height: Optional height, overriding the intermediate
316 height (C{meter}).
317 @kwarg wrap: If C{True}, wrap or I{normalize} and unroll the
318 B{C{other}} point (C{bool}).
320 @return: Intermediate point (L{LatLon}).
322 @raise TypeError: The B{C{other}} point is not L{LatLon}.
324 @raise ValueError: Invalid B{C{fraction}} or B{C{height}}.
326 @see: Methods C{midpointTo} and C{rhumbMidpointTo}.
327 '''
328 p = self
329 f = Scalar(fraction=fraction)
330 if not isnear0(f):
331 p = p.others(other)
332 if wrap:
333 p = _Wrap.point(p)
334 if not isnear1(f): # and not near0
335 a1, b1 = self.philam
336 a2, b2 = p.philam
337 db, b2 = unrollPI(b1, b2, wrap=wrap)
338 r = vincentys_(a2, a1, db)
339 sr = sin(r)
340 if isnon0(sr):
341 sa1, ca1, sa2, ca2, \
342 sb1, cb1, sb2, cb2 = sincos2_(a1, a2, b1, b2)
344 t = f * r
345 a = sin(r - t) # / sr superflous
346 b = sin( t) # / sr superflous
348 x = fdot_(a, ca1 * cb1, b, ca2 * cb2)
349 y = fdot_(a, ca1 * sb1, b, ca2 * sb2)
350 z = fdot_(a, sa1, b, sa2)
352 a = atan1d(z, hypot(x, y))
353 b = atan2d(y, x)
355 else: # PYCHOK no cover
356 a = degrees90( favg(a1, a2, f=f)) # coincident
357 b = degrees180(favg(b1, b2, f=f))
359 h = self._havg(other, f=f, h=height)
360 p = self.classof(a, b, height=h)
361 return p
363 def intersection(self, end1, other, end2, height=None, wrap=False):
364 '''Compute the intersection point of two lines, each defined by
365 two points or a start point and a bearing from North.
367 @arg end1: End point of this line (L{LatLon}) or the initial
368 bearing at this point (compass C{degrees360}).
369 @arg other: Start point of the other line (L{LatLon}).
370 @arg end2: End point of the other line (L{LatLon}) or the
371 initial bearing at the B{C{other}} point (compass
372 C{degrees360}).
373 @kwarg height: Optional height for intersection point,
374 overriding the mean height (C{meter}).
375 @kwarg wrap: If C{True}, wrap or I{normalize} and unroll
376 B{C{start2}} and both B{C{end*}} points (C{bool}).
378 @return: The intersection point (L{LatLon}). An alternate
379 intersection point might be the L{antipode} to
380 the returned result.
382 @raise IntersectionError: Ambiguous or infinite intersection
383 or colinear, parallel or otherwise
384 non-intersecting lines.
386 @raise TypeError: If B{C{other}} is not L{LatLon} or B{C{end1}}
387 or B{C{end2}} not C{scalar} nor L{LatLon}.
389 @raise ValueError: Invalid B{C{height}} or C{null} line.
390 '''
391 try:
392 s2 = self.others(other)
393 return _intersect(self, end1, s2, end2, height=height, wrap=wrap,
394 LatLon=self.classof)
395 except (TypeError, ValueError) as x:
396 raise _xError(x, start1=self, end1=end1,
397 other=other, end2=end2, wrap=wrap)
399 def intersections2(self, rad1, other, rad2, radius=R_M, eps=_0_0,
400 height=None, wrap=True):
401 '''Compute the intersection points of two circles, each defined
402 by a center point and a radius.
404 @arg rad1: Radius of the this circle (C{meter} or C{radians},
405 see B{C{radius}}).
406 @arg other: Center point of the other circle (L{LatLon}).
407 @arg rad2: Radius of the other circle (C{meter} or C{radians},
408 see B{C{radius}}).
409 @kwarg radius: Mean earth radius (C{meter} or C{None} if B{C{rad1}},
410 B{C{rad2}} and B{C{eps}} are given in C{radians}).
411 @kwarg eps: Required overlap (C{meter} or C{radians}, see
412 B{C{radius}}).
413 @kwarg height: Optional height for the intersection points (C{meter},
414 conventionally) or C{None} for the I{"radical height"}
415 at the I{radical line} between both centers.
416 @kwarg wrap: If C{True}, wrap or I{normalize} and unroll the
417 B{C{other}} point (C{bool}).
419 @return: 2-Tuple of the intersection points, each a L{LatLon}
420 instance. For abutting circles, both intersection
421 points are the same instance, aka the I{radical center}.
423 @raise IntersectionError: Concentric, antipodal, invalid or
424 non-intersecting circles.
426 @raise TypeError: If B{C{other}} is not L{LatLon}.
428 @raise ValueError: Invalid B{C{rad1}}, B{C{rad2}}, B{C{radius}},
429 B{C{eps}} or B{C{height}}.
430 '''
431 try:
432 c2 = self.others(other)
433 return _intersects2(self, rad1, c2, rad2, radius=radius, eps=eps,
434 height=height, wrap=wrap,
435 LatLon=self.classof)
436 except (TypeError, ValueError) as x:
437 raise _xError(x, center=self, rad1=rad1,
438 other=other, rad2=rad2, wrap=wrap)
440 @deprecated_method
441 def isEnclosedBy(self, points): # PYCHOK no cover
442 '''DEPRECATED, use method C{isenclosedBy}.'''
443 return self.isenclosedBy(points)
445 def isenclosedBy(self, points, wrap=False):
446 '''Check whether a (convex) polygon or composite encloses this point.
448 @arg points: The polygon points or composite (L{LatLon}[],
449 L{BooleanFHP} or L{BooleanGH}).
450 @kwarg wrap: If C{True}, wrap or I{normalize} and unroll the
451 B{C{points}} (C{bool}).
453 @return: C{True} if this point is inside the polygon or
454 composite, C{False} otherwise.
456 @raise PointsError: Insufficient number of B{C{points}}.
458 @raise TypeError: Some B{C{points}} are not L{LatLon}.
460 @raise ValueError: Invalid B{C{points}}, non-convex polygon.
462 @see: Functions L{pygeodesy.isconvex}, L{pygeodesy.isenclosedBy}
463 and L{pygeodesy.ispolar} especially if the B{C{points}} may
464 enclose a pole or wrap around the earth I{longitudinally}.
465 '''
466 if _MODS.booleans.isBoolean(points):
467 return points._encloses(self.lat, self.lon, wrap=wrap)
469 Ps = self.PointsIter(points, loop=2, dedup=True, wrap=wrap)
470 n0 = self._N_vector
472 v2 = Ps[0]._N_vector
473 p1 = Ps[1]
474 v1 = p1._N_vector
475 # check whether this point on same side of all
476 # polygon edges (to the left or right depending
477 # on the anti-/clockwise polygon direction)
478 gc1 = v2.cross(v1)
479 t0 = gc1.angleTo(n0) > PI_2
480 s0 = None
481 # get great-circle vector for each edge
482 for i, p2 in Ps.enumerate(closed=True):
483 if wrap and not Ps.looped:
484 p2 = _unrollon(p1, p2)
485 p1 = p2
486 v2 = p2._N_vector
487 gc = v1.cross(v2)
488 t = gc.angleTo(n0) > PI_2
489 if t != t0: # different sides of edge i
490 return False # outside
492 # check for convex polygon: angle between
493 # gc vectors, signed by direction of n0
494 # (otherwise the test above is not reliable)
495 s = signOf(gc1.angleTo(gc, vSign=n0))
496 if s != s0:
497 if s0 is None:
498 s0 = s
499 else:
500 t = _Fmt.SQUARE(points=i)
501 raise _ValueError(t, p2, wrap=wrap, txt_not_=_convex_)
502 gc1, v1 = gc, v2
504 return True # inside
506 def midpointTo(self, other, height=None, fraction=_0_5, wrap=False):
507 '''Find the midpoint between this and an other point.
509 @arg other: The other point (L{LatLon}).
510 @kwarg height: Optional height for midpoint, overriding
511 the mean height (C{meter}).
512 @kwarg fraction: Midpoint location from this point (C{scalar}),
513 may be negative or greater than 1.0.
514 @kwarg wrap: If C{True}, wrap or I{normalize} and unroll the
515 B{C{other}} point (C{bool}).
517 @return: Midpoint (L{LatLon}).
519 @raise TypeError: The B{C{other}} point is not L{LatLon}.
521 @raise ValueError: Invalid B{C{height}}.
523 @see: Methods C{intermediateTo} and C{rhumbMidpointTo}.
524 '''
525 if fraction is _0_5:
526 # see <https://MathForum.org/library/drmath/view/51822.html>
527 a1, b, a2, _, db = self._ab1_ab2_db5(other, wrap)
528 sa1, ca1, sa2, ca2, sdb, cdb = sincos2_(a1, a2, db)
530 x = ca2 * cdb + ca1
531 y = ca2 * sdb
533 a = atan1d(sa1 + sa2, hypot(x, y))
534 b = degrees180(b + atan2(y, x))
536 h = self._havg(other, h=height)
537 r = self.classof(a, b, height=h)
538 else:
539 r = self.intermediateTo(other, fraction, height=height, wrap=wrap)
540 return r
542 def nearestOn(self, point1, point2, radius=R_M, **wrap_adjust_limit):
543 '''Locate the point between two other points closest to this point.
545 Distances are approximated by function L{pygeodesy.equirectangular4},
546 subject to the supplied B{C{options}}.
548 @arg point1: Start point (L{LatLon}).
549 @arg point2: End point (L{LatLon}).
550 @kwarg radius: Mean earth radius (C{meter}).
551 @kwarg wrap_adjust_limit: Optional keyword arguments for functions
552 L{sphericalTrigonometry.nearestOn3} and
553 L{pygeodesy.equirectangular4},
555 @return: Closest point on the great circle line (L{LatLon}).
557 @raise LimitError: Lat- and/or longitudinal delta exceeds B{C{limit}},
558 see function L{pygeodesy.equirectangular4}.
560 @raise NotImplementedError: Keyword argument C{B{within}=False}
561 is not (yet) supported.
563 @raise TypeError: Invalid B{C{point1}} or B{C{point2}}.
565 @raise ValueError: Invalid B{C{radius}} or B{C{options}}.
567 @see: Functions L{pygeodesy.equirectangular4} and L{pygeodesy.nearestOn5}
568 and method L{sphericalTrigonometry.LatLon.nearestOn3}.
569 '''
570 # remove kwarg B{C{within}} if present
571 w, kwds = _xkwds_pop2(wrap_adjust_limit, within=True)
572 if not w:
573 self._notImplemented(within=w)
575# # UNTESTED - handle C{B{within}=False} and C{B{within}=True}
576# wrap = _xkwds_get(options, wrap=False)
577# a = self.alongTrackDistanceTo(point1, point2, radius=radius, wrap=wrap)
578# if fabs(a) < EPS or (within and a < EPS):
579# return point1
580# d = point1.distanceTo(point2, radius=radius, wrap=wrap)
581# if isnear0(d):
582# return point1 # or point2
583# elif fabs(d - a) < EPS or (a + EPS) > d:
584# return point2
585# f = a / d
586# if within:
587# if f > EPS1:
588# return point2
589# elif f < EPS:
590# return point1
591# return point1.intermediateTo(point2, f, wrap=wrap)
593 # without kwarg B{C{within}}, use backward compatible .nearestOn3
594 return self.nearestOn3([point1, point2], closed=False, radius=radius,
595 **kwds)[0]
597 @deprecated_method
598 def nearestOn2(self, points, closed=False, radius=R_M, **options): # PYCHOK no cover
599 '''DEPRECATED, use method L{sphericalTrigonometry.LatLon.nearestOn3}.
601 @return: ... 2-Tuple C{(closest, distance)} of the closest
602 point (L{LatLon}) on the polygon and the distance
603 to that point from this point in C{meter}, same
604 units of B{C{radius}}.
605 '''
606 r = self.nearestOn3(points, closed=closed, radius=radius, **options)
607 return r.closest, r.distance
609 def nearestOn3(self, points, closed=False, radius=R_M, **wrap_adjust_limit):
610 '''Locate the point on a polygon closest to this point.
612 Distances are approximated by function L{pygeodesy.equirectangular4},
613 subject to the supplied B{C{options}}.
615 @arg points: The polygon points (L{LatLon}[]).
616 @kwarg closed: Optionally, close the polygon (C{bool}).
617 @kwarg radius: Mean earth radius (C{meter}).
618 @kwarg wrap_adjust_limit: Optional keyword arguments for function
619 L{sphericalTrigonometry.nearestOn3} and
620 L{pygeodesy.equirectangular4},
622 @return: A L{NearestOn3Tuple}C{(closest, distance, angle)} of the
623 C{closest} point (L{LatLon}), the L{pygeodesy.equirectangular4}
624 C{distance} between this and the C{closest} point converted to
625 C{meter}, same units as B{C{radius}}. The C{angle} from this
626 to the C{closest} point is in compass C{degrees360}, like
627 function L{pygeodesy.compassAngle}.
629 @raise LimitError: Lat- and/or longitudinal delta exceeds B{C{limit}},
630 see function L{pygeodesy.equirectangular4}.
632 @raise PointsError: Insufficient number of B{C{points}}.
634 @raise TypeError: Some B{C{points}} are not C{LatLon}.
636 @raise ValueError: Invalid B{C{radius}} or B{C{options}}.
638 @see: Functions L{pygeodesy.compassAngle}, L{pygeodesy.equirectangular4}
639 and L{pygeodesy.nearestOn5}.
640 '''
641 return nearestOn3(self, points, closed=closed, radius=radius,
642 LatLon=self.classof, **wrap_adjust_limit)
644 def toCartesian(self, **Cartesian_datum_kwds): # PYCHOK Cartesian=Cartesian, datum=None
645 '''Convert this point to C{Karney}-based cartesian (ECEF) coordinates.
647 @kwarg Cartesian_datum_kwds: Optional L{Cartesian}, B{C{datum}} and other
648 keyword arguments, ignored if C{B{Cartesian} is
649 None}. Use C{B{Cartesian}=...} to override this
650 L{Cartesian} class or specify C{B{Cartesian}=None}.
652 @return: The cartesian point (L{Cartesian}) or if C{B{Cartesian} is None},
653 an L{Ecef9Tuple}C{(x, y, z, lat, lon, height, C, M, datum)} with C{C}
654 and C{M} if available.
656 @raise TypeError: Invalid B{C{Cartesian_datum_kwds}} argument.
657 '''
658 kwds = _xkwds(Cartesian_datum_kwds, Cartesian=Cartesian, datum=self.datum)
659 return LatLonSphericalBase.toCartesian(self, **kwds)
661 def triangle7(self, otherB, otherC, radius=R_M, wrap=False):
662 '''Compute the angles, sides and area of a spherical triangle.
664 @arg otherB: Second triangle point (C{LatLon}).
665 @arg otherC: Third triangle point (C{LatLon}).
666 @kwarg radius: Mean earth radius, ellipsoid or datum (C{meter}, L{Ellipsoid},
667 L{Ellipsoid2}, L{Datum} or L{a_f2Tuple}) or C{None}.
668 @kwarg wrap: If C{True}, wrap or I{normalize} and unroll points B{C{otherB}}
669 and B{C{otherC}} (C{bool}).
671 @return: L{Triangle7Tuple}C{(A, a, B, b, C, c, area)} or if B{C{radius} is
672 None}, a L{Triangle8Tuple}C{(A, a, B, b, C, c, D, E)}.
674 @see: Function L{triangle7} and U{Spherical trigonometry
675 <https://WikiPedia.org/wiki/Spherical_trigonometry>}.
676 '''
677 B = self.others(otherB=otherB)
678 C = self.others(otherC=otherC)
679 B, C, _ = _unrollon3(self, B, C, wrap)
681 r = self.philam + B.philam + C.philam
682 t = triangle8_(*r, wrap=wrap)
683 return self._xnamed(_t7Tuple(t, radius))
685 def triangulate(self, bearing1, other, bearing2, **height_wrap):
686 '''Locate a point given this, an other point and a bearing from
687 North at both points.
689 @arg bearing1: Bearing at this point (compass C{degrees360}).
690 @arg other: The other point (C{LatLon}).
691 @arg bearing2: Bearing at the other point (compass C{degrees360}).
692 @kwarg height_wrap_tol: Optional keyword arguments C{B{height}=None},
693 C{B{wrap}=False}, see method L{intersection}.
695 @return: Triangulated point (C{LatLon}).
697 @see: Method L{intersection} for further details.
698 '''
699 if _isDegrees(bearing1) and _isDegrees(bearing2):
700 return self.intersection(bearing1, other, bearing2, **height_wrap)
701 raise _TypeError(bearing1=bearing1, bearing2=bearing2, **height_wrap)
703 def trilaterate5(self, distance1, point2, distance2, point3, distance3,
704 area=True, eps=EPS1, radius=R_M, wrap=False):
705 '''Trilaterate three points by I{area overlap} or I{perimeter intersection}
706 of three corresponding circles.
708 @arg distance1: Distance to this point (C{meter}, same units as B{C{radius}}).
709 @arg point2: Second center point (C{LatLon}).
710 @arg distance2: Distance to point2 (C{meter}, same units as B{C{radius}}).
711 @arg point3: Third center point (C{LatLon}).
712 @arg distance3: Distance to point3 (C{meter}, same units as B{C{radius}}).
713 @kwarg area: If C{True}, compute the area overlap, otherwise the perimeter
714 intersection of the circles (C{bool}).
715 @kwarg eps: The required I{minimal overlap} for C{B{area}=True} or the
716 I{intersection margin} if C{B{area}=False} (C{meter}, same
717 units as B{C{radius}}).
718 @kwarg radius: Mean earth radius (C{meter}, conventionally).
719 @kwarg wrap: If C{True}, wrap or I{normalize} and unroll B{C{point2}} and
720 B{C{point3}} (C{bool}).
722 @return: A L{Trilaterate5Tuple}C{(min, minPoint, max, maxPoint, n)} with
723 C{min} and C{max} in C{meter}, same units as B{C{eps}}, the
724 corresponding trilaterated points C{minPoint} and C{maxPoint}
725 as I{spherical} C{LatLon} and C{n}, the number of trilatered
726 points found for the given B{C{eps}}.
728 If only a single trilaterated point is found, C{min I{is} max},
729 C{minPoint I{is} maxPoint} and C{n = 1}.
731 For C{B{area}=True}, C{min} and C{max} are the smallest respectively
732 largest I{radial} overlap found.
734 For C{B{area}=False}, C{min} and C{max} represent the nearest
735 respectively farthest intersection margin.
737 If C{B{area}=True} and all 3 circles are concentric, C{n=0} and
738 C{minPoint} and C{maxPoint} are both the B{C{point#}} with the
739 smallest B{C{distance#}} C{min} and C{max} the largest B{C{distance#}}.
741 @raise IntersectionError: Trilateration failed for the given B{C{eps}},
742 insufficient overlap for C{B{area}=True} or
743 no intersection or all (near-)concentric if
744 C{B{area}=False}.
746 @raise TypeError: Invalid B{C{point2}} or B{C{point3}}.
748 @raise ValueError: Coincident B{C{point2}} or B{C{point3}} or invalid
749 B{C{distance1}}, B{C{distance2}}, B{C{distance3}}
750 or B{C{radius}}.
751 '''
752 return _trilaterate5(self, distance1,
753 self.others(point2=point2), distance2,
754 self.others(point3=point3), distance3,
755 area=area, radius=radius, eps=eps, wrap=wrap)
758_T00 = LatLon(0, 0, name='T00') # reference instance (L{LatLon})
761def areaOf(points, radius=R_M, wrap=False): # was=True
762 '''Calculate the area of a (spherical) polygon or composite (with the
763 points joined by great circle arcs).
765 @arg points: The polygon points or clips (L{LatLon}[], L{BooleanFHP}
766 or L{BooleanGH}).
767 @kwarg radius: Mean earth radius, ellipsoid or datum (C{meter},
768 L{Ellipsoid}, L{Ellipsoid2}, L{Datum} or L{a_f2Tuple})
769 or C{None}.
770 @kwarg wrap: If C{True}, wrap or I{normalize} and unroll the B{C{points}}
771 (C{bool}).
773 @return: Polygon area (C{meter} I{quared}, same units as B{C{radius}}
774 or C{radians} if C{B{radius} is None}).
776 @raise PointsError: Insufficient number of B{C{points}}.
778 @raise TypeError: Some B{C{points}} are not L{LatLon}.
780 @raise ValueError: Invalid B{C{radius}} or semi-circular polygon edge.
782 @note: The area is based on I{Karney}'s U{'Area of a spherical
783 polygon'<https://MathOverflow.net/questions/97711/
784 the-area-of-spherical-polygons>}, 3rd Answer.
786 @see: Functions L{pygeodesy.areaOf}, L{sphericalNvector.areaOf},
787 L{pygeodesy.excessKarney}, L{ellipsoidalExact.areaOf} and
788 L{ellipsoidalKarney.areaOf}.
789 '''
790 if _MODS.booleans.isBoolean(points):
791 return points._sum2(LatLon, areaOf, radius=radius, wrap=wrap)
793 _at2, _t_2 = atan2, tan_2
794 _un, _w180 = unrollPI, wrap180
796 Ps = _T00.PointsIter(points, loop=1, wrap=wrap)
797 p1 = p2 = Ps[0]
798 a1, b1 = p1.philam
799 ta1, z1 = _t_2(a1), None
801 A = Fsum() # mean phi
802 R = Fsum() # see L{pygeodesy.excessKarney_}
803 # ispolar: Summation of course deltas around pole is 0° rather than normally ±360°
804 # <https://blog.Element84.com/determining-if-a-spherical-polygon-contains-a-pole.html>
805 # XXX duplicate of function C{points.ispolar} to avoid copying all iterated points
806 D = Fsum()
807 for i, p2 in Ps.enumerate(closed=True):
808 a2, b2 = p2.philam
809 db, b2 = _un(b1, b2, wrap=wrap and not Ps.looped)
810 A += a2
811 ta2 = _t_2(a2)
812 tdb = _t_2(db, points=i)
813 R += _at2(tdb * (ta1 + ta2),
814 _1_0 + ta1 * ta2)
815 ta1, b1 = ta2, b2
817 if not p2.isequalTo(p1, eps=EPS):
818 z, z2 = _bearingTo2(p1, p2, wrap=wrap)
819 if z1 is not None:
820 D += _w180(z - z1) # (z - z1 + 540) ...
821 D += _w180(z2 - z) # (z2 - z + 540) % 360 - 180
822 p1, z1 = p2, z2
824 R = abs(R * _2_0)
825 if abs(D) < _90_0: # ispolar(points)
826 R = abs(R - PI2)
827 if radius:
828 a = degrees(A.fover(len(A))) # mean lat
829 R *= _mean_radius(radius, a)**2
830 return float(R)
833def _destination2(a, b, r, t):
834 '''(INTERNAL) Destination lat- and longitude in C{radians}.
836 @arg a: Latitude (C{radians}).
837 @arg b: Longitude (C{radians}).
838 @arg r: Angular distance (C{radians}).
839 @arg t: Bearing (compass C{radians}).
841 @return: 2-Tuple (phi, lam) of (C{radians}, C{radiansPI}).
842 '''
843 # see <https://www.EdWilliams.org/avform.htm#LL>
844 sa, ca, sr, cr, st, ct = sincos2_(a, r, t)
845 ca *= sr
847 a = asin1(ct * ca + cr * sa)
848 d = atan2(st * ca, cr - sa * sin(a))
849 # note, in EdWilliams.org/avform.htm W is + and E is -
850 return a, (b + d) # (mod(b + d + PI, PI2) - PI)
853def _int3d2(s, end, wrap, _i_, Vector, hs):
854 # see <https://www.EdWilliams.org/intersect.htm> (5) ff
855 # and similar logic in .ellipsoidalBaseDI._intersect3
856 a1, b1 = s.philam
858 if _isDegrees(end): # bearing, get pseudo-end point
859 a2, b2 = _destination2(a1, b1, PI_4, radians(end))
860 else: # must be a point
861 s.others(end, name=_end_ + _i_)
862 hs.append(end.height)
863 a2, b2 = end.philam
864 if wrap:
865 a2, b2 = _Wrap.philam(a2, b2)
867 db, b2 = unrollPI(b1, b2, wrap=wrap)
868 if max(fabs(db), fabs(a2 - a1)) < EPS:
869 raise _ValueError(_SPACE_(_line_ + _i_, _null_))
870 # note, in EdWilliams.org/avform.htm W is + and E is -
871 sb21, cb21, sb12, cb12 = sincos2_(db * _0_5,
872 -(b1 + b2) * _0_5)
873 cb21 *= sin(a1 - a2) # sa21
874 sb21 *= sin(a1 + a2) # sa12
875 x = Vector(fdot_(sb12, cb21, -cb12, sb21),
876 fdot_(cb12, cb21, sb12, sb21),
877 cos(a1) * cos(a2) * sin(db)) # ll=start
878 return x.unit(), (db, (a2 - a1)) # negated d
881def _intdot(ds, a1, b1, a, b, wrap):
882 # compute dot product ds . (-b + b1, a - a1)
883 db, _ = unrollPI(b1, b, wrap=wrap)
884 return fdot(ds, db, a - a1)
887def intersecant2(center, circle, point, other, **radius_exact_height_wrap):
888 '''Compute the intersections of a circle and a (great circle) line given as
889 two points or as a point and a bearing from North.
891 @arg center: Center of the circle (L{LatLon}).
892 @arg circle: Radius of the circle (C{meter}, same units as the earth
893 B{C{radius}}) or a point on the circle (L{LatLon}).
894 @arg point: A point on the (great circle) line (L{LatLon}).
895 @arg other: An other point on the (great circle) line (L{LatLon}) or
896 the bearing at the B{C{point}} (compass C{degrees360}).
897 @kwarg radius_exact_height_wrap: Optional keyword arguments, see method
898 L{intersecant2<pygeodesy.sphericalBase.LatLonSphericalBase.
899 intersecant2>} for further details.
901 @return: 2-Tuple of the intersection points (representing a chord), each
902 an instance of the B{C{point}} class. Both points are the same
903 instance if the (great circle) line is tangent to the circle.
905 @raise IntersectionError: The circle and line do not intersect.
907 @raise TypeError: If B{C{center}}, B{C{point}}, B{C{circle}} or B{C{other}}
908 not L{LatLon}.
910 @raise UnitError: Invalid B{C{circle}}, B{C{other}}, B{C{radius}},
911 B{C{exact}}, B{C{height}} or B{C{napieradius}}.
912 '''
913 c = _T00.others(center=center)
914 p = _T00.others(point=point)
915 try:
916 return _intersecant2(c, circle, p, other, **radius_exact_height_wrap)
917 except (TypeError, ValueError) as x:
918 raise _xError(x, center=center, circle=circle, point=point, other=other,
919 **radius_exact_height_wrap)
922def _intersect(start1, end1, start2, end2, height=None, wrap=False, # in.ellipsoidalBaseDI._intersect3
923 LatLon=LatLon, **LatLon_kwds):
924 # (INTERNAL) Intersect two (spherical) lines, see L{intersection}
925 # above, separated to allow callers to embellish any exceptions
927 s1, s2 = start1, start2
928 if wrap:
929 s2 = _Wrap.point(s2)
930 hs = [s1.height, s2.height]
932 a1, b1 = s1.philam
933 a2, b2 = s2.philam
934 db, b2 = unrollPI(b1, b2, wrap=wrap)
935 r12 = vincentys_(a2, a1, db)
936 if fabs(r12) < EPS: # [nearly] coincident points
937 a, b = favg(a1, a2), favg(b1, b2)
939 # see <https://www.EdWilliams.org/avform.htm#Intersection>
940 elif _isDegrees(end1) and _isDegrees(end2): # both bearings
941 sa1, ca1, sa2, ca2, sr12, cr12 = sincos2_(a1, a2, r12)
943 x1, x2 = (sr12 * ca1), (sr12 * ca2)
944 if isnear0(x1) or isnear0(x2):
945 raise IntersectionError(_parallel_)
946 # handle domain error for equivalent longitudes,
947 # see also functions asin_safe and acos_safe at
948 # <https://www.EdWilliams.org/avform.htm#Math>
949 t12, t13 = acos1((sa2 - sa1 * cr12) / x1), radiansPI2(end1)
950 t21, t23 = acos1((sa1 - sa2 * cr12) / x2), radiansPI2(end2)
951 if sin(db) > 0:
952 t21 = PI2 - t21
953 else:
954 t12 = PI2 - t12
955 sx1, cx1, sx2, cx2 = sincos2_(wrapPI(t13 - t12), # angle 2-1-3
956 wrapPI(t21 - t23)) # angle 1-2-3)
957 if isnear0(sx1) and isnear0(sx2):
958 raise IntersectionError(_infinite_)
959 sx3 = sx1 * sx2
960# XXX if sx3 < 0:
961# XXX raise ValueError(_ambiguous_)
962 x3 = acos1(cr12 * sx3 - cx2 * cx1)
963 r13 = atan2(sr12 * sx3, cx2 + cx1 * cos(x3))
965 a, b = _destination2(a1, b1, r13, t13)
966 # like .ellipsoidalBaseDI,_intersect3, if this intersection
967 # is "before" the first point, use the antipodal intersection
968 if opposing_(t13, bearing_(a1, b1, a, b, wrap=wrap)):
969 a, b = antipode_(a, b) # PYCHOK PhiLam2Tuple
971 else: # end point(s) or bearing(s)
972 _N_vector_ = _MODS.nvectorBase._N_vector_
974 x1, d1 = _int3d2(s1, end1, wrap, _1_, _N_vector_, hs)
975 x2, d2 = _int3d2(s2, end2, wrap, _2_, _N_vector_, hs)
976 x = x1.cross(x2)
977 if x.length < EPS: # [nearly] colinear or parallel lines
978 raise IntersectionError(_colinear_)
979 a, b = x.philam
980 # choose intersection similar to sphericalNvector
981 if not (_intdot(d1, a1, b1, a, b, wrap) *
982 _intdot(d2, a2, b2, a, b, wrap)) > 0:
983 a, b = antipode_(a, b) # PYCHOK PhiLam2Tuple
985 h = fmean(hs) if height is None else Height(height)
986 return _LL3Tuple(degrees90(a), degrees180(b), h,
987 intersection, LatLon, LatLon_kwds)
990def intersection(start1, end1, start2, end2, height=None, wrap=False,
991 **LatLon_and_kwds):
992 '''Compute the intersection point of two lines, each defined by
993 two points or by a start point and a bearing from North.
995 @arg start1: Start point of the first line (L{LatLon}).
996 @arg end1: End point of the first line (L{LatLon}) or the bearing
997 at the first start point (compass C{degrees360}).
998 @arg start2: Start point of the second line (L{LatLon}).
999 @arg end2: End point of the second line (L{LatLon}) or the bearing
1000 at the second start point (compass C{degrees360}).
1001 @kwarg height: Optional height for the intersection point,
1002 overriding the mean height (C{meter}).
1003 @kwarg wrap: If C{True}, wrap or I{normalize} and unroll B{C{start2}}
1004 and both B{C{end*}} points (C{bool}).
1005 @kwarg LatLon_and_kwds: Optional class C{B{LatLon}=}L{LatLon} to use
1006 for the intersection point and optionally additional
1007 B{C{LatLon}} keyword arguments, ignored if C{B{LatLon}
1008 is None}.
1010 @return: The intersection point as a (B{C{LatLon}}) or if C{B{LatLon}
1011 is None} a L{LatLon3Tuple}C{(lat, lon, height)}. An alternate
1012 intersection point might be the L{antipode} to the returned result.
1014 @raise IntersectionError: Ambiguous or infinite intersection or colinear,
1015 parallel or otherwise non-intersecting lines.
1017 @raise TypeError: A B{C{start1}}, B{C{end1}}, B{C{start2}} or B{C{end2}}
1018 point not L{LatLon}.
1020 @raise ValueError: Invalid B{C{height}} or C{null} line.
1021 '''
1022 s1 = _T00.others(start1=start1)
1023 s2 = _T00.others(start2=start2)
1024 try:
1025 return _intersect(s1, end1, s2, end2, height=height, wrap=wrap, **LatLon_and_kwds)
1026 except (TypeError, ValueError) as x:
1027 raise _xError(x, start1=start1, end1=end1, start2=start2, end2=end2)
1030def intersections2(center1, rad1, center2, rad2, radius=R_M, eps=_0_0,
1031 height=None, wrap=False, # was=True
1032 **LatLon_and_kwds):
1033 '''Compute the intersection points of two circles each defined by a
1034 center point and a radius.
1036 @arg center1: Center of the first circle (L{LatLon}).
1037 @arg rad1: Radius of the first circle (C{meter} or C{radians}, see
1038 B{C{radius}}).
1039 @arg center2: Center of the second circle (L{LatLon}).
1040 @arg rad2: Radius of the second circle (C{meter} or C{radians}, see
1041 B{C{radius}}).
1042 @kwarg radius: Mean earth radius (C{meter} or C{None} if B{C{rad1}},
1043 B{C{rad2}} and B{C{eps}} are given in C{radians}).
1044 @kwarg eps: Required overlap (C{meter} or C{radians}, see B{C{radius}}).
1045 @kwarg height: Optional height for the intersection points (C{meter},
1046 conventionally) or C{None} for the I{"radical height"}
1047 at the I{radical line} between both centers.
1048 @kwarg wrap: If C{True}, wrap or I{normalize} and unroll B{C{center2}}
1049 (C{bool}).
1050 @kwarg LatLon_and_kwds: Optional class C{B{LatLon}=}L{LatLon} to use for
1051 the intersection points and optionally additional B{C{LatLon}}
1052 keyword arguments, ignored if C{B{LatLon} is None}.
1054 @return: 2-Tuple of the intersection points, each a B{C{LatLon}}
1055 instance or if C{B{LatLon} is None} a L{LatLon3Tuple}C{(lat,
1056 lon, height)}. For abutting circles, both intersection
1057 points are the same instance, aka the I{radical center}.
1059 @raise IntersectionError: Concentric, antipodal, invalid or
1060 non-intersecting circles.
1062 @raise TypeError: If B{C{center1}} or B{C{center2}} not L{LatLon}.
1064 @raise ValueError: Invalid B{C{rad1}}, B{C{rad2}}, B{C{radius}},
1065 B{C{eps}} or B{C{height}}.
1067 @note: Courtesy of U{Samuel Čavoj<https://GitHub.com/mrJean1/PyGeodesy/issues/41>}.
1069 @see: This U{Answer<https://StackOverflow.com/questions/53324667/
1070 find-intersection-coordinates-of-two-circles-on-earth/53331953>}.
1071 '''
1072 c1 = _T00.others(center1=center1)
1073 c2 = _T00.others(center2=center2)
1074 try:
1075 return _intersects2(c1, rad1, c2, rad2, radius=radius, eps=eps,
1076 height=height, wrap=wrap,
1077 **LatLon_and_kwds)
1078 except (TypeError, ValueError) as x:
1079 raise _xError(x, center1=center1, rad1=rad1,
1080 center2=center2, rad2=rad2, wrap=wrap)
1083def _intersects2(c1, rad1, c2, rad2, radius=R_M, eps=_0_0, # in .ellipsoidalBaseDI._intersects2
1084 height=None, too_d=None, wrap=False, # was=True
1085 LatLon=LatLon, **LatLon_kwds):
1086 # (INTERNAL) Intersect two spherical circles, see L{intersections2}
1087 # above, separated to allow callers to embellish any exceptions
1089 def _dest3(bearing, h):
1090 a, b = _destination2(a1, b1, r1, bearing)
1091 return _LL3Tuple(degrees90(a), degrees180(b), h,
1092 intersections2, LatLon, LatLon_kwds)
1094 a1, b1 = c1.philam
1095 a2, b2 = c2.philam
1096 if wrap:
1097 a2, b2 = _Wrap.philam(a2, b2)
1099 r1, r2, f = _rads3(rad1, rad2, radius)
1100 if f: # swapped radii, swap centers
1101 a1, a2 = a2, a1 # PYCHOK swap!
1102 b1, b2 = b2, b1 # PYCHOK swap!
1104 db, b2 = unrollPI(b1, b2, wrap=wrap)
1105 d = vincentys_(a2, a1, db) # radians
1106 if d < max(r1 - r2, EPS):
1107 raise IntersectionError(_near_(_concentric_)) # XXX ConcentricError?
1109 r = eps if radius is None else (m2radians(
1110 eps, radius=radius) if eps else _0_0)
1111 if r < _0_0:
1112 raise _ValueError(eps=r)
1114 x = fsumf_(r1, r2, -d) # overlap
1115 if x > max(r, EPS):
1116 sd, cd, sr1, cr1, _, cr2 = sincos2_(d, r1, r2)
1117 x = sd * sr1
1118 if isnear0(x):
1119 raise _ValueError(_invalid_)
1120 x = acos1((cr2 - cd * cr1) / x) # 0 <= x <= PI
1122 elif x < r: # PYCHOK no cover
1123 t = (d * radius) if too_d is None else too_d
1124 raise IntersectionError(_too_(_Fmt.distant(t)))
1126 if height is None: # "radical height"
1127 f = _radical2(d, r1, r2).ratio
1128 h = Height(favg(c1.height, c2.height, f=f))
1129 else:
1130 h = Height(height)
1132 b = bearing_(a1, b1, a2, b2, final=False, wrap=wrap)
1133 if x < EPS4: # externally ...
1134 r = _dest3(b, h)
1135 elif x > _PI_EPS4: # internally ...
1136 r = _dest3(b + PI, h)
1137 else:
1138 return _dest3(b + x, h), _dest3(b - x, h)
1139 return r, r # ... abutting circles
1142@deprecated_function
1143def isPoleEnclosedBy(points, wrap=False): # PYCHOK no cover
1144 '''DEPRECATED, use function L{pygeodesy.ispolar}.
1145 '''
1146 return ispolar(points, wrap=wrap)
1149def _LL3Tuple(lat, lon, height, where, LatLon, LatLon_kwds):
1150 '''(INTERNAL) Helper for L{intersection}, L{intersections2} and L{meanOf}.
1151 '''
1152 n = typename(where)
1153 if LatLon is None:
1154 r = LatLon3Tuple(lat, lon, height, name=n)
1155 else:
1156 kwds = _xkwds(LatLon_kwds, height=height, name=n)
1157 r = LatLon(lat, lon, **kwds)
1158 return r
1161def meanOf(points, height=None, wrap=False, LatLon=LatLon, **LatLon_kwds):
1162 '''Compute the I{geographic} mean of several points.
1164 @arg points: Points to be averaged (L{LatLon}[]).
1165 @kwarg height: Optional height at mean point, overriding the mean height
1166 (C{meter}).
1167 @kwarg wrap: If C{True}, wrap or I{normalize} the B{C{points}} (C{bool}).
1168 @kwarg LatLon: Optional class to return the mean point (L{LatLon}) or C{None}.
1169 @kwarg LatLon_kwds: Optional, additional B{C{LatLon}} keyword arguments,
1170 ignored if C{B{LatLon} is None}.
1172 @return: The geographic mean and height (B{C{LatLon}}) or if C{B{LatLon}
1173 is None}, a L{LatLon3Tuple}C{(lat, lon, height)}.
1175 @raise TypeError: Some B{C{points}} are not L{LatLon}.
1177 @raise ValueError: No B{C{points}} or invalid B{C{height}}.
1178 '''
1179 def _N_vs(ps, w):
1180 Ps = _T00.PointsIter(ps, wrap=w)
1181 for p in Ps.iterate(closed=False):
1182 yield p._N_vector
1184 m = _MODS.nvectorBase
1185 # geographic, vectorial mean
1186 n = m.sumOf(_N_vs(points, wrap), h=height, Vector=m.NvectorBase)
1187 lat, lon, h = n.latlonheight
1188 return _LL3Tuple(lat, lon, h, meanOf, LatLon, LatLon_kwds)
1191@deprecated_function
1192def nearestOn2(point, points, **closed_radius_LatLon_options): # PYCHOK no cover
1193 '''DEPRECATED, use function L{sphericalTrigonometry.nearestOn3}.
1195 @return: ... 2-tuple C{(closest, distance)} of the C{closest}
1196 point (L{LatLon}) on the polygon and the C{distance}
1197 between the C{closest} and the given B{C{point}}. The
1198 C{closest} is a B{C{LatLon}} or a L{LatLon2Tuple}C{(lat,
1199 lon)} if C{B{LatLon} is None} ...
1200 '''
1201 ll, d, _ = nearestOn3(point, points, **closed_radius_LatLon_options) # PYCHOK 3-tuple
1202 if _xkwds_get(closed_radius_LatLon_options, LatLon=LatLon) is None:
1203 ll = LatLon2Tuple(ll.lat, ll.lon)
1204 return ll, d
1207def nearestOn3(point, points, closed=False, radius=R_M, wrap=False, adjust=True,
1208 limit=9, **LatLon_and_kwds):
1209 '''Locate the point on a path or polygon closest to a reference point.
1211 Distances are I{approximated} using function L{equirectangular4
1212 <pygeodesy.equirectangular4>}, subject to the supplied B{C{options}}.
1214 @arg point: The reference point (L{LatLon}).
1215 @arg points: The path or polygon points (L{LatLon}[]).
1216 @kwarg closed: Optionally, close the polygon (C{bool}).
1217 @kwarg radius: Mean earth radius (C{meter}).
1218 @kwarg wrap: If C{True}, wrap or I{normalize} and unroll the
1219 B{C{points}} (C{bool}).
1220 @kwarg adjust: See function L{equirectangular4<pygeodesy.equirectangular4>}
1221 (C{bool}).
1222 @kwarg limit: See function L{equirectangular4<pygeodesy.equirectangular4>}
1223 (C{degrees}), default C{9 degrees} is about C{1,000 Km} (for
1224 (mean spherical earth radius L{R_KM}).
1225 @kwarg LatLon_and_kwds: Optional class C{B{LatLon}=L{LatLon}} to return the
1226 closest point and optionally additional C{B{LatLon}} keyword
1227 arguments or specify C{B{LatLon}=None}.
1229 @return: A L{NearestOn3Tuple}C{(closest, distance, angle)} with the
1230 C{closest} point as B{C{LatLon}} or L{LatLon3Tuple}C{(lat,
1231 lon, height)} if C{B{LatLon} is None}. The C{distance} is
1232 the L{equirectangular4<pygeodesy.equirectangular4>} distance
1233 between the C{closest} and the given B{C{point}} converted to
1234 C{meter}, same units as B{C{radius}}. The C{angle} from the
1235 given B{C{point}} to the C{closest} is in compass C{degrees360},
1236 like function L{compassAngle<pygeodesy.compassAngle>}. The
1237 C{height} is the (interpolated) height at the C{closest} point.
1239 @raise LimitError: Lat- and/or longitudinal delta exceeds the B{C{limit}},
1240 see function L{equirectangular4<pygeodesy.equirectangular4>}.
1242 @raise PointsError: Insufficient number of B{C{points}}.
1244 @raise TypeError: Some B{C{points}} are not C{LatLon}.
1246 @raise ValueError: Invalid B{C{radius}}.
1248 @see: Functions L{equirectangular4<pygeodesy.equirectangular4>} and
1249 L{nearestOn5<pygeodesy.nearestOn5>}.
1250 '''
1251 t = _nearestOn5(point, points, closed=closed, wrap=wrap,
1252 adjust=adjust, limit=limit)
1253 d = degrees2m(t.distance, radius=radius)
1254 h = t.height
1255 n = typename(nearestOn3)
1257 LL, kwds = _xkwds_pop2(LatLon_and_kwds, LatLon=LatLon)
1258 r = LatLon3Tuple(t.lat, t.lon, h, name=n) if LL is None else \
1259 LL(t.lat, t.lon, **_xkwds(kwds, height=h, name=n))
1260 return NearestOn3Tuple(r, d, t.angle, name=n)
1263def perimeterOf(points, closed=False, radius=R_M, wrap=True):
1264 '''Compute the perimeter of a (spherical) polygon or composite
1265 (with great circle arcs joining the points).
1267 @arg points: The polygon points or clips (L{LatLon}[], L{BooleanFHP}
1268 or L{BooleanGH}).
1269 @kwarg closed: Optionally, close the polygon (C{bool}).
1270 @kwarg radius: Mean earth radius (C{meter}) or C{None}.
1271 @kwarg wrap: If C{True}, wrap or I{normalize} and unroll the
1272 B{C{points}} (C{bool}).
1274 @return: Polygon perimeter (C{meter}, same units as B{C{radius}}
1275 or C{radians} if C{B{radius} is None}).
1277 @raise PointsError: Insufficient number of B{C{points}}.
1279 @raise TypeError: Some B{C{points}} are not L{LatLon}.
1281 @raise ValueError: Invalid B{C{radius}} or C{B{closed}=False} with
1282 C{B{points}} a composite.
1284 @note: Distances are based on function L{vincentys_<pygeodesy.vincentys_>}.
1286 @see: Functions L{perimeterOf<pygeodesy.perimeterOf>},
1287 L{sphericalNvector.perimeterOf} and L{ellipsoidalKarney.perimeterOf}.
1288 '''
1289 def _rads(ps, c, w): # angular edge lengths in radians
1290 Ps = _T00.PointsIter(ps, loop=1, wrap=w)
1291 a1, b1 = Ps[0].philam
1292 for p in Ps.iterate(closed=c):
1293 a2, b2 = p.philam
1294 db, b2 = unrollPI(b1, b2, wrap=w and not (c and Ps.looped))
1295 yield vincentys_(a2, a1, db)
1296 a1, b1 = a2, b2
1298 if _MODS.booleans.isBoolean(points):
1299 if not closed:
1300 raise _ValueError(closed=closed, points=_composite_)
1301 r = points._sum2(LatLon, perimeterOf, closed=True, radius=radius, wrap=wrap)
1302 else:
1303 r = fsum(_rads(points, closed, wrap))
1304 return _radians2m(r, radius)
1307def triangle7(latA, lonA, latB, lonB, latC, lonC, radius=R_M,
1308 excess=excessAbc_,
1309 wrap=False):
1310 '''Compute the angles, sides, and area of a (spherical) triangle.
1312 @arg latA: First corner latitude (C{degrees}).
1313 @arg lonA: First corner longitude (C{degrees}).
1314 @arg latB: Second corner latitude (C{degrees}).
1315 @arg lonB: Second corner longitude (C{degrees}).
1316 @arg latC: Third corner latitude (C{degrees}).
1317 @arg lonC: Third corner longitude (C{degrees}).
1318 @kwarg radius: Mean earth radius, ellipsoid or datum (C{meter},
1319 L{Ellipsoid}, L{Ellipsoid2}, L{Datum} or L{a_f2Tuple})
1320 or C{None}.
1321 @kwarg excess: I{Spherical excess} callable (L{excessAbc_},
1322 L{excessGirard_} or L{excessLHuilier_}).
1323 @kwarg wrap: If C{True}, wrap and L{unroll180<pygeodesy.unroll180>}
1324 longitudes (C{bool}).
1326 @return: A L{Triangle7Tuple}C{(A, a, B, b, C, c, area)} with
1327 spherical angles C{A}, C{B} and C{C}, angular sides
1328 C{a}, C{b} and C{c} all in C{degrees} and C{area}
1329 in I{square} C{meter} or same units as B{C{radius}}
1330 I{squared} or if C{B{radius}=0} or C{None}, a
1331 L{Triangle8Tuple}C{(A, a, B, b, C, c, D, E)} with
1332 I{spherical deficit} C{D} and I{spherical excess}
1333 C{E} as the C{unit area}, all in C{radians}.
1334 '''
1335 t = triangle8_(Phid(latA=latA), Lamd(lonA=lonA),
1336 Phid(latB=latB), Lamd(lonB=lonB),
1337 Phid(latC=latC), Lamd(lonC=lonC),
1338 excess=excess, wrap=wrap)
1339 return _t7Tuple(t, radius)
1342def triangle8_(phiA, lamA, phiB, lamB, phiC, lamC, excess=excessAbc_,
1343 wrap=False):
1344 '''Compute the angles, sides, I{spherical deficit} and I{spherical
1345 excess} of a (spherical) triangle.
1347 @arg phiA: First corner latitude (C{radians}).
1348 @arg lamA: First corner longitude (C{radians}).
1349 @arg phiB: Second corner latitude (C{radians}).
1350 @arg lamB: Second corner longitude (C{radians}).
1351 @arg phiC: Third corner latitude (C{radians}).
1352 @arg lamC: Third corner longitude (C{radians}).
1353 @kwarg excess: I{Spherical excess} callable (L{excessAbc_},
1354 L{excessGirard_} or L{excessLHuilier_}).
1355 @kwarg wrap: If C{True}, L{unrollPI<pygeodesy.unrollPI>} the
1356 longitudinal deltas (C{bool}).
1358 @return: A L{Triangle8Tuple}C{(A, a, B, b, C, c, D, E)} with
1359 spherical angles C{A}, C{B} and C{C}, angular sides
1360 C{a}, C{b} and C{c}, I{spherical deficit} C{D} and
1361 I{spherical excess} C{E}, all in C{radians}.
1362 '''
1363 def _a_r(w, phiA, lamA, phiB, lamB, phiC, lamC):
1364 d, _ = unrollPI(lamB, lamC, wrap=w)
1365 a = vincentys_(phiC, phiB, d)
1366 return a, (phiB, lamB, phiC, lamC, phiA, lamA) # rotate A, B, C
1368 def _A_r(a, sa, ca, sb, cb, sc, cc):
1369 s = sb * sc
1370 A = acos1((ca - cb * cc) / s) if isnon0(s) else a
1371 return A, (sb, cb, sc, cc, sa, ca) # rotate sincos2_'s
1373 # notation: side C{a} is oposite to corner C{A}, etc.
1374 a, r = _a_r(wrap, phiA, lamA, phiB, lamB, phiC, lamC)
1375 b, r = _a_r(wrap, *r)
1376 c, _ = _a_r(wrap, *r)
1378 A, r = _A_r(a, *sincos2_(a, b, c))
1379 B, r = _A_r(b, *r)
1380 C, _ = _A_r(c, *r)
1382 D = fsumf_(PI2, -a, -b, -c) # deficit aka defect
1383 E = excessGirard_(A, B, C) if _isin(excess, excessGirard_, True) else (
1384 excessLHuilier_(a, b, c) if _isin(excess, excessLHuilier_, False) else
1385 excessAbc_(*max((A, b, c), (B, c, a), (C, a, b))))
1387 return Triangle8Tuple(A, a, B, b, C, c, D, E)
1390def _t7Tuple(t, radius):
1391 '''(INTERNAL) Convert a L{Triangle8Tuple} to L{Triangle7Tuple}.
1392 '''
1393 if radius: # not _isin(radius, None, _0_0)
1394 r = radius if _isRadius(radius) else \
1395 _ellipsoidal_datum(radius).ellipsoid.Rmean
1396 A, B, C = map1(degrees, t.A, t.B, t.C)
1397 t = Triangle7Tuple(A, (r * t.a),
1398 B, (r * t.b),
1399 C, (r * t.c), t.E * r**2)
1400 return t
1403__all__ += _ALL_OTHER(Cartesian, LatLon, # classes
1404 areaOf, # functions
1405 intersecant2, intersection, intersections2, ispolar,
1406 isPoleEnclosedBy, # DEPRECATED, use ispolar
1407 meanOf,
1408 nearestOn2, nearestOn3,
1409 perimeterOf,
1410 sumOf, # XXX == vector3d.sumOf
1411 triangle7, triangle8_)
1413# **) MIT License
1414#
1415# Copyright (C) 2016-2025 -- mrJean1 at Gmail -- All Rights Reserved.
1416#
1417# Permission is hereby granted, free of charge, to any person obtaining a
1418# copy of this software and associated documentation files (the "Software"),
1419# to deal in the Software without restriction, including without limitation
1420# the rights to use, copy, modify, merge, publish, distribute, sublicense,
1421# and/or sell copies of the Software, and to permit persons to whom the
1422# Software is furnished to do so, subject to the following conditions:
1423#
1424# The above copyright notice and this permission notice shall be included
1425# in all copies or substantial portions of the Software.
1426#
1427# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
1428# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
1429# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
1430# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
1431# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
1432# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
1433# OTHER DEALINGS IN THE SOFTWARE.