Coverage for pygeodesy/sphericalNvector.py: 91%
315 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{N-vector}-based geodesy.
6N-vector-based classes geodetic (lat-/longitude) L{LatLon}, geocentric
7(ECEF) L{Cartesian} and C{Nvector} and functions L{areaOf}, L{intersection},
8L{meanOf}, L{nearestOn3}, L{perimeterOf}, L{sumOf}, L{triangulate} and
9L{trilaterate}, I{all spherical}.
11Pure Python implementation of n-vector-based spherical geodetic (lat-/longitude)
12methods, transcoded from JavaScript originals by I{(C) Chris Veness 2011-2024},
13published under the same MIT Licence**. See U{Vector-based geodesy
14<https://www.Movable-Type.co.UK/scripts/latlong-vectors.html>} and
15U{Module latlon-nvector-spherical
16<https://www.Movable-Type.co.UK/scripts/geodesy/docs/module-latlon-nvector-spherical.html>}.
18Tools for working with points and lines on (a spherical model of) the
19earth’s surface using using n-vectors rather than the more common
20spherical trigonometry. N-vectors make many calculations much simpler,
21and easier to follow, compared with the trigonometric equivalents.
23Based on Kenneth Gade’s U{‘Non-singular Horizontal Position Representation’
24<https://www.NavLab.net/Publications/A_Nonsingular_Horizontal_Position_Representation.pdf>},
25The Journal of Navigation (2010), vol 63, nr 3, pp 395-417.
27Note that the formulations below take x => 0°N,0°E, y => 0°N,90°E and
28z => 90°N while Gade uses x => 90°N, y => 0°N,90°E, z => 0°N,0°E.
30Also note that on a spherical earth model, an n-vector is equivalent
31to a normalised version of an (ECEF) cartesian coordinate.
32'''
33# make sure int/int division yields float quosient, see .basics
34from __future__ import division as _; del _ # PYCHOK semicolon
36from pygeodesy.basics import _isin, _xinstanceof, typename
37from pygeodesy.constants import EPS, EPS0, PI, PI2, PI_2, R_M, \
38 _0_0, _0_5, _1_0
39# from pygeodesy.datums import Datums # from .sphericalBase
40from pygeodesy.errors import PointsError, VectorError, _xError, _xkwds
41from pygeodesy.fmath import fdot_, fmean, fsum
42# from pygeodesy.fsums import fsum # from .fmath
43# from pygeodesy.internals import typename # from .basics
44from pygeodesy.interns import _composite_, _end_, _Nv00_, _other_, \
45 _point_, _pole_
46from pygeodesy.lazily import _ALL_LAZY, _ALL_MODS as _MODS, _ALL_OTHER
47# from pygeodesy.named import notImplemented # from .points
48# from pygeodesy.namedTuples import NearestOn3Tuple # from .points
49from pygeodesy.nvectorBase import LatLonNvectorBase, NorthPole, _nsumOf, \
50 NvectorBase, _triangulate, _trilaterate
51from pygeodesy.points import NearestOn3Tuple, notImplemented, \
52 ispolar # PYCHOK exported
53from pygeodesy.props import deprecated_function, deprecated_method, \
54 property_RO
55from pygeodesy.sphericalBase import _m2radians, CartesianSphericalBase, \
56 _intersecant2, LatLonSphericalBase, \
57 _radians2m, Datums
58from pygeodesy.units import Bearing, Bearing_, _isDegrees, Radius, Scalar
59from pygeodesy.utily import atan2, degrees360, sincos2, sincos2_, sincos2d, \
60 _unrollon, _Wrap, fabs
62# from math import fabs # from utily
64__all__ = _ALL_LAZY.sphericalNvector
65__version__ = '25.04.14'
67_lines_ = 'lines'
70class Cartesian(CartesianSphericalBase):
71 '''Extended to convert geocentric, L{Cartesian} points to
72 C{Nvector} and n-vector-based, spherical L{LatLon}.
73 '''
75 def toLatLon(self, **LatLon_and_kwds): # PYCHOK LatLon=LatLon
76 '''Convert this cartesian to an C{Nvector}-based geodetic point.
78 @kwarg LatLon_and_kwds: Optional C{LatLon} class and C{LatLon} keyword
79 arguments, like C{datum}. Use C{B{LatLon}=...}
80 to override this L{LatLon} class or specify
81 C{B{LatLon}=None}.
83 @return: A C{LatLon} or if C{LatLon is None}, an L{Ecef9Tuple}C{(x, y, z,
84 lat, lon, height, C, M, datum)} with C{C} and C{M} if available.
86 @raise TypeError: Invalid C{LatLon} or other B{C{LatLon_and_kwds}} item.
87 '''
88 kwds = _xkwds(LatLon_and_kwds, LatLon=LatLon, datum=self.datum)
89 return CartesianSphericalBase.toLatLon(self, **kwds)
91 def toNvector(self, **Nvector_and_kwds): # PYCHOK Datums.WGS84
92 '''Convert this cartesian to C{Nvector} components, I{including height}.
94 @kwarg Nvector_and_kwds: Optional C{Nvector} class and C{Nvector} keyword
95 arguments, like C{datum}. Use C{B{Nvector}=...}
96 to override this C{Nvector} class or specify
97 C{B{Nvector}=None}.
99 @return: An C{Nvector}) or if C{Nvector is None}, a L{Vector4Tuple}C{(x, y, z, h)}.
101 @raise TypeError: Invalid C{Nvector} or other B{C{Nvector_and_kwds}} item.
102 '''
103 # ll = CartesianBase.toLatLon(self, LatLon=LatLon,
104 # datum=datum or self.datum)
105 # kwds = _xkwds(kwds, Nvector=Nvector)
106 # return ll.toNvector(**kwds)
107 kwds = _xkwds(Nvector_and_kwds, Nvector=Nvector, datum=self.datum)
108 return CartesianSphericalBase.toNvector(self, **kwds)
111class LatLon(LatLonNvectorBase, LatLonSphericalBase):
112 '''New n-vector-based point on a spherical earth model.
114 Tools for working with points, lines and paths on (a spherical
115 model of) the earth's surface using vector-based methods.
116 '''
117 _Nv = None # cached_toNvector C{Nvector})
119 def _update(self, updated, *attrs, **setters): # PYCHOK args
120 '''(INTERNAL) Zap cached attributes if updated.
121 '''
122 if updated: # reset caches
123 LatLonNvectorBase._update(self, updated, _Nv=self._Nv) # special case
124 LatLonSphericalBase._update(self, updated, *attrs, **setters)
126 def alongTrackDistanceTo(self, start, end, radius=R_M, wrap=False):
127 '''Compute the (signed) distance from the start to the closest
128 point on the great circle line defined by a start and an
129 end point.
131 That is, if a perpendicular is drawn from this point to the
132 great circle line, the along-track distance is the distance
133 from the start point to the point where the perpendicular
134 crosses the line.
136 @arg start: Start point of great circle line (L{LatLon}).
137 @arg end: End point of great circle line (L{LatLon}) or
138 initial bearing from start point (compass
139 C{degrees360}).
140 @kwarg radius: Mean earth radius (C{meter}) or C{None}.
141 @kwarg wrap: If C{True}, wrap or I{normalize} and unroll
142 the B{C{start}} and B{C{end}} points (C{bool}).
144 @return: Distance along the great circle line (C{radians}
145 if C{B{radius} is None} else C{meter}, same units
146 as B{C{radius}}), positive if "after" the start
147 toward the end point of the line or negative if
148 "before" the start point.
150 @raise TypeError: If B{C{start}} or B{C{end}} point is not L{LatLon}.
152 @raise Valuerror: Some points coincide.
153 '''
154 p = self.others(start=start)
155 n = self.toNvector()
157 gc, _, _ = self._gc3(p, end, _end_, wrap=wrap)
158 a = gc.cross(n).cross(gc) # along-track point gc × p × gc
159 return _radians2m(start.toNvector().angleTo(a, vSign=gc), radius)
161 @deprecated_method
162 def bearingTo(self, other, **unused): # PYCHOK no cover
163 '''DEPRECATED, use method L{initialBearingTo}.
164 '''
165 return self.initialBearingTo(other)
167 def crossTrackDistanceTo(self, start, end, radius=R_M, wrap=False):
168 '''Compute the (signed) distance from this point to great circle
169 defined by a start and end point.
171 @arg start: Start point of great circle line (L{LatLon}).
172 @arg end: End point of great circle line (L{LatLon}) or initial
173 bearing from start point (compass C{degrees360}).
174 @kwarg radius: Mean earth radius (C{meter}) or C{None}.
175 @kwarg wrap: If C{True}, wrap or I{normalize} and unroll the
176 B{C{start}} and B{C{end}} points (C{bool}).
178 @return: Distance to great circle (C{radians} if C{B{radius}
179 is None} else C{meter}, same units as B{C{radius}}),
180 negative if to the left or positive if to the right
181 of the line .
183 @raise TypeError: If B{C{start}} or B{C{end}} point is not L{LatLon}.
185 @raise Valuerror: Some points coincide.
186 '''
187 p = self.others(start=start)
188 n = self.toNvector()
190 gc, _, _ = self._gc3(p, end, _end_, wrap=wrap)
191 return _radians2m(gc.angleTo(n) - PI_2, radius)
193 def destination(self, distance, bearing, radius=R_M, height=None):
194 '''Locate the destination from this point after having travelled
195 the given distance on the given bearing.
197 @arg distance: Distance travelled (C{meter}, same units as
198 B{C{radius}}).
199 @arg bearing: Bearing from this point (compass C{degrees360}).
200 @kwarg radius: Mean earth radius (C{meter}).
201 @kwarg height: Optional height at destination, overriding the
202 default height (C{meter}, same units as B{C{radius}}).
204 @return: Destination point (L{LatLon}).
206 @raise Valuerror: Polar coincidence or invalid B{C{distance}},
207 B{C{bearing}}, B{C{radius}} or B{C{height}}.
208 '''
209 b = Bearing_(bearing)
210 a = _m2radians(distance, radius, low=None)
211 sa, ca, sb, cb = sincos2_(a, b)
213 n = self.toNvector()
214 e = NorthPole.cross(n, raiser=_pole_).unit() # east vector at n
215 x = n.cross(e) # north vector at n
216 d = x.times(cb).plus(e.times(sb)) # direction vector @ n
217 n = n.times(ca).plus(d.times(sa))
218 return n.toLatLon(height=height, LatLon=self.classof) # Nvector(n.x, n.y, n.z).toLatLon(...)
220 def distanceTo(self, other, radius=R_M, wrap=False):
221 '''Compute the distance from this to an other point.
223 @arg other: The other point (L{LatLon}).
224 @kwarg radius: Mean earth radius (C{meter}) or C{None}.
225 @kwarg wrap: If C{True}, wrap or I{normalize} and unroll
226 the B{C{other}} point (C{bool}).
228 @return: Distance between this and the B{C{other}} point
229 (C{meter}, same units as B{C{radius}} or C{radians}
230 if C{B{radius} is None}).
232 @raise TypeError: Invalid B{C{other}} point.
233 '''
234 p = self.others(other)
235 if wrap:
236 p = _unrollon(self, p)
237 n = p.toNvector()
238 r = fabs(self.toNvector().angleTo(n, wrap=wrap))
239 return r if radius is None else (Radius(radius) * r)
241# @Property_RO
242# def Ecef(self):
243# '''Get the ECEF I{class} (L{EcefVeness}), I{lazily}.
244# '''
245# return _ALL_MODS.ecef.EcefKarney
247 def _gc3(self, start, end, namend, raiser=_point_, wrap=False):
248 '''(INTERNAL) Return great circle, start and end Nvectors.
249 '''
250 s = start.toNvector()
251 if _isDegrees(end): # bearing
252 gc = s.greatCircle(end)
253 e = None
254 else: # point
255 p = self.others(end, name=namend)
256 if wrap:
257 p = _unrollon(start, p, wrap=wrap)
258 e = p.toNvector()
259 gc = s.cross(e, raiser=raiser) # XXX .unit()?
260 return gc, s, e
262 def greatCircle(self, bearing):
263 '''Compute the vector normal to great circle obtained by
264 heading on the given bearing from this point.
266 Direction of vector is such that initial bearing vector
267 b = c × n, where n is an n-vector representing this point.
269 @arg bearing: Bearing from this point (compass C{degrees360}).
271 @return: N-vector representing the great circle (C{Nvector}).
272 '''
273 t = Bearing_(bearing)
274 a, b = self.philam
276 sa, ca, sb, cb, st, ct = sincos2_(a, b, t)
278 sa *= st
279 return Nvector(fdot_(sb, ct, -sa, cb),
280 -fdot_(cb, ct, sa, sb),
281 ca * st, name=self.name) # XXX .unit()
283 def greatCircleTo(self, other, wrap=False):
284 '''Compute the vector normal to great circle obtained by
285 heading from this to an other point or on a given bearing.
287 Direction of vector is such that initial bearing vector
288 b = c × n, where n is an n-vector representing this point.
290 @arg other: The other point (L{LatLon}) or the bearing from
291 this point (compass C{degrees360}).
292 @kwarg wrap: If C{True}, wrap or I{normalize} and unroll
293 the B{C{other}} point (C{bool}).
295 @return: N-vector representing the great circle (C{Nvector}).
297 @raise TypeError: The B{C{other}} point is not L{LatLon}.
299 @raise Valuerror: Points coincide.
300 '''
301 gc, _, _ = self._gc3(self, other, _other_, wrap=wrap)
302 return gc.unit()
304 def initialBearingTo(self, other, wrap=False, **unused): # raiser=...
305 '''Compute the initial bearing (forward azimuth) from this
306 to an other point.
308 @arg other: The other point (L{LatLon}).
309 @kwarg wrap: If C{True}, wrap or I{normalize} and unroll the
310 B{C{other}} point (C{bool}).
312 @return: Initial bearing (compass C{degrees360}).
314 @raise Crosserror: This point coincides with the B{C{other}}
315 point or the C{NorthPole} and L{crosserrors
316 <pygeodesy.crosserrors>} is C{True}.
318 @raise TypeError: The B{C{other}} point is not L{LatLon}.
319 '''
320 n = self.toNvector()
321 p = self.others(other)
322 if wrap:
323 p = _unrollon(self, p, wrap=wrap)
324 p = p.toNvector()
325 # see <https://MathForum.org/library/drmath/view/55417.html>
326# gc1 = self.greatCircleTo(other)
327 gc1 = n.cross(p, raiser=_point_) # .unit()
328# gc2 = self.greatCircleTo(NorthPole)
329 gc2 = n.cross(NorthPole, raiser=_pole_) # .unit()
330 return degrees360(gc1.angleTo(gc2, vSign=n))
332 def intermediateChordTo(self, other, fraction, height=None, wrap=False):
333 '''Locate the point projected from the point at given fraction
334 on a straight line (chord) between this and an other point.
336 @arg other: The other point (L{LatLon}).
337 @arg fraction: Fraction between both points (float, between
338 0.0 for this and 1.0 for the other point).
339 @kwarg height: Optional height at the intermediate point,
340 overriding the fractional height (C{meter}).
341 @kwarg wrap: If C{True}, wrap or I{normalize} and unroll
342 the B{C{other}} point (C{bool}).
344 @return: Intermediate point (L{LatLon}).
346 @raise TypeError: The B{C{other}} point is not L{LatLon}.
347 '''
348 n = self.toNvector()
349 p = self.others(other)
350 if wrap:
351 p = _unrollon(self, p, wrap=wrap)
353 f = Scalar(fraction=fraction)
354 i = p.toNvector().times(f).plus(n.times(1 - f))
355# i = p.toNvector() * f + self.toNvector() * (1 - f))
357 h = self._havg(other, f=f, h=height)
358 return i.toLatLon(height=h, LatLon=self.classof) # Nvector(i.x, i.y, i.z).toLatLon(...)
360 def intermediateTo(self, other, fraction, height=None, wrap=False):
361 '''Locate the point at a given fraction between this and an
362 other point.
364 @arg other: The other point (L{LatLon}).
365 @arg fraction: Fraction between both points (C{float}, between
366 0.0 for this and 1.0 for the other point).
367 @kwarg height: Optional height at the intermediate point,
368 overriding the fractional height (C{meter}).
369 @kwarg wrap: If C{True}, wrap or I{normalize} and unroll
370 the B{C{other}} point (C{bool}).
372 @return: Intermediate point (L{LatLon}).
374 @raise TypeError: The B{C{other}} point is not L{LatLon}.
376 @raise Valuerror: Points coincide or invalid B{C{height}}.
378 @see: Methods C{midpointTo} and C{rhumbMidpointTo}.
379 '''
380 n = self.toNvector()
381 p = self.others(other)
382 if wrap:
383 p = _unrollon(self, p, wrap=wrap)
384 p = p.toNvector()
385 f = Scalar(fraction=fraction)
387 x = n.cross(p, raiser=_point_)
388 d = x.unit().cross(n) # unit(n × p) × n
389 # angular distance α, tan(α) = |n × p| / n ⋅ p
390 s, c = sincos2(atan2(x.length, n.dot(p)) * f) # interpolated
391 i = n.times(c).plus(d.times(s)) # n * cosα + d * sinα
393 h = self._havg(other, f=f, h=height)
394 return i.toLatLon(height=h, LatLon=self.classof) # Nvector(i.x, i.y, i.z).toLatLon(...)
396 def intersection(self, end1, start2, end2, height=None, wrap=False):
397 '''Locate an intersection point of two lines each defined by two
398 points or by a point and an (initial) bearing.
400 @return: The intersection point (L{LatLon}).
402 @see: Method L{intersection2<sphericalNvector.LatLon.intersection2>}
403 for further details.
404 '''
405 return intersection(self, end1, start2, end2, height=height,
406 wrap=wrap, LatLon=self.classof)
408 def intersection2(self, end1, start2, end2, height=None, wrap=False):
409 '''Locate both intersections of two (great circle) lines each defined
410 by two points or by a point and an (initial) bearing.
412 @arg end1: End point of the line starting at this point (L{LatLon})
413 or the bearing at this point (compass C{degrees360}).
414 @arg start2: Start point of the other line (L{LatLon}).
415 @arg end2: End point of the other line (L{LatLon}) or the bearing
416 at B{C{start2}} (compass C{degrees360}).
417 @kwarg height: Optional height at the intersection and antipodal
418 point, overriding the mean height (C{meter}).
419 @kwarg wrap: If C{True}, wrap or I{normalize} and unroll the
420 B{C{start2}} and both B{C{end*}} points (C{bool}).
422 @return: 2-Tuple C{(intersection, antipode)}, each a B{C{LatLon}}.
424 @raise TypeError: If B{C{start2}}, B{C{end1}} or B{C{end2}}
425 point is not L{LatLon}.
427 @raise ValueError: Intersection is ambiguous or infinite or
428 the lines are parallel, coincident or null.
430 @see: Function L{sphericalNvector.intersection2}.
431 '''
432 return intersection2(self, end1, start2, end2, height=height,
433 wrap=wrap, LatLon=self.classof)
435 def isenclosedBy(self, points, wrap=False):
436 '''Check whether a (convex) polygon or composite encloses this point.
438 @arg points: The polygon points or composite (L{LatLon}[],
439 L{BooleanFHP} or L{BooleanGH}).
440 @kwarg wrap: If C{True}, wrap or I{normalize} and unroll the
441 B{C{points}} (C{bool}).
443 @return: C{True} if this point is inside the polygon or composite,
444 C{False} otherwise.
446 @raise PointsError: Insufficient number of B{C{points}}.
448 @raise TypeError: Some B{C{points}} are not L{LatLon}.
450 @see: Functions L{pygeodesy.isconvex}, L{pygeodesy.isenclosedBy}
451 and L{pygeodesy.ispolar} especially if the B{C{points}} may
452 enclose a pole or wrap around the earth I{longitudinally}.
453 '''
454 if _MODS.booleans.isBoolean(points):
455 return points._encloses(self.lat, self.lon, wrap=wrap)
457 # sum subtended angles of each edge (using n0, the
458 # normal vector to this point for sign of α)
459 def _subt(Ps, n0, w):
460 p1 = Ps[0]
461 vs1 = n0.minus(p1.toNvector())
462 for p2 in Ps.iterate(closed=True):
463 if w and not Ps.looped:
464 p2 = _unrollon(p1, p2)
465 p1 = p2
466 vs2 = n0.minus(p2.toNvector())
467 yield vs1.angleTo(vs2, vSign=n0) # PYCHOK false
468 vs1 = vs2
470 # Note, this method uses angle summation test: on a plane,
471 # angles for an enclosed point will sum to 360°, angles for
472 # an exterior point will sum to 0°. On a sphere, enclosed
473 # point angles will sum to less than 360° (due to spherical
474 # excess), exterior point angles will be small but non-zero.
475 s = fsum(_subt(self.PointsIter(points, loop=1, wrap=wrap),
476 self.toNvector(), wrap)) # normal vector
477 # XXX are winding number optimisations equally applicable to
478 # spherical surface?
479 return fabs(s) > PI
481 @deprecated_method
482 def isEnclosedBy(self, points): # PYCHOK no cover
483 '''DEPRECATED, use method C{isenclosedBy}.'''
484 return self.isenclosedBy(points)
486 def iswithin(self, point1, point2, wrap=False):
487 '''Check whether this point is between two other points.
489 If this point is not on the great circle arc defined by
490 both points, return whether it is within the area bound
491 by perpendiculars to the great circle at each point (in
492 the same hemispere).
494 @arg point1: Start point of the arc (L{LatLon}).
495 @arg point2: End point of the arc (L{LatLon}).
496 @kwarg wrap: If C{True}, wrap or I{normalize} and unroll
497 B{C{point1}} and B{C{point2}} (C{bool}).
499 @return: C{True} if this point is within the (great circle)
500 arc, C{False} otherwise.
502 @raise TypeError: If B{C{point1}} or B{C{point2}} is not
503 L{LatLon}.
504 '''
505 p1 = self.others(point1=point1)
506 p2 = self.others(point2=point2)
507 if wrap:
508 p1 = _Wrap.point(p1)
509 p2 = _unrollon(p1, p2, wrap=wrap)
510 n, n1, n2 = (_.toNvector() for _ in (self, p1, p2))
512 # corner case, null arc
513 if n1.isequalTo(n2):
514 return n.isequalTo(n1) or n.isequalTo(n2) # PYCHOK returns
516 if n.dot(n1) < 0 or n.dot(n2) < 0: # different hemisphere
517 return False # PYCHOK returns
519 # get vectors representing d0=p0->p1 and d2=p2->p1 and the
520 # dot product d0⋅d2 tells us if p0 is on the p2 side of p1 or
521 # on the other side (similarly for d0=p0->p2 and d1=p1->p2
522 # and dot product d0⋅d1 and p0 on the p1 side of p2 or not)
523 return n.minus(n1).dot(n2.minus(n1)) >= 0 and \
524 n.minus(n2).dot(n1.minus(n2)) >= 0
526 @deprecated_method
527 def isWithin(self, point1, point2): # PYCHOK no cover
528 '''DEPRECATED, use method C{iswithin}.'''
529 return self.iswithin(point1, point2)
531 def midpointTo(self, other, height=None, fraction=_0_5, wrap=False):
532 '''Find the midpoint between this and an other point.
534 @arg other: The other point (L{LatLon}).
535 @kwarg height: Optional height at the midpoint, overriding
536 the mean height (C{meter}).
537 @kwarg fraction: Midpoint location from this point (C{scalar}),
538 may be negative or greater than 1.0.
539 @kwarg wrap: If C{True}, wrap or I{normalize} and unroll the
540 B{C{other}} point (C{bool}).
542 @return: Midpoint (L{LatLon}).
544 @raise TypeError: The B{C{other}} point is not L{LatLon}.
546 @see: Methods C{intermediateTo} and C{rhumbMidpointTo}.
547 '''
548 if fraction is _0_5:
549 p = self.others(other)
550 if wrap:
551 p = _unrollon(self, p, wrap=wrap)
552 m = self.toNvector().plus(p.toNvector())
553 h = self._havg(other, f=fraction, h=height)
554 r = m.toLatLon(height=h, LatLon=self.classof)
555 else:
556 r = self.intermediateTo(other, fraction, height=height, wrap=wrap)
557 return r
559 def nearestOn(self, point1, point2, height=None, within=True, wrap=False):
560 '''Locate the point on the great circle arc between two points
561 closest to this point.
563 @arg point1: Start point of the arc (L{LatLon}).
564 @arg point2: End point of the arc (L{LatLon}).
565 @kwarg height: Optional height, overriding the mean height for
566 the point within the arc (C{meter}), or C{None}
567 to interpolate the height.
568 @kwarg within: If C{True}, return the closest point between both
569 given points, otherwise the closest point
570 elsewhere on the great circle arc (C{bool}).
571 @kwarg wrap: If C{True}, wrap or I{normalize} and unroll
572 B{C{point1}} and B{C{point2}} (C{bool}).
574 @return: Closest point on the arc (L{LatLon}).
576 @raise NotImplementedError: Keyword argument C{B{wrap}=True}
577 not supported.
579 @raise TypeError: Invalid B{C{point1}} or B{C{point2}}.
580 '''
581 p1 = self.others(point1=point1)
582 p2 = self.others(point2=point2)
583 if wrap:
584 p1 = _Wrap.point(p1)
585 p2 = _unrollon(p1, p2, wrap=wrap)
586 p0 = self
588 if p0.iswithin(p1, p2) and not p1.isequalTo(p2, EPS):
589 # closer to arc than to its endpoints,
590 # find the closest point on the arc
591 gc1 = p1.toNvector().cross(p2.toNvector())
592 gc2 = p0.toNvector().cross(gc1)
593 n = gc1.cross(gc2)
595 elif within: # for backward compatibility, XXX unwrapped
596 return point1 if (self.distanceTo(point1) <
597 self.distanceTo(point2)) else point2
599 else: # handle beyond arc extent by .vector3d.nearestOn
600 n1 = p1.toNvector()
601 n2 = p2.toNvector()
602 n = p0.toNvector().nearestOn(n1, n2, within=False)
603 if n is n1:
604 return p1 # is point1
605 elif n is n2:
606 return p2 # is point2 if not wrap
608 p = n.toLatLon(height=height or 0, LatLon=self.classof)
609 if _isin(height, None, False): # interpolate height within extent
610 d = p1.distanceTo(p2)
611 f = (p1.distanceTo(p) / d) if d > EPS0 else _0_5
612 p.height = p1._havg(p2, f=max(_0_0, min(f, _1_0)))
613 return p
615 # @deprecated_method
616 def nearestOn2(self, points, **closed_radius_height): # PYCHOK no cover
617 '''DEPRECATED, use method L{sphericalNvector.LatLon.nearestOn3}.
619 @return: ... 2-Tuple C{(closest, distance)} of the C{closest}
620 point (L{LatLon}) on the polygon and the C{distance}
621 to that point from this point ...
622 '''
623 r = self.nearestOn3(points, **closed_radius_height)
624 return r.closest, r.distance
626 def nearestOn3(self, points, closed=False, radius=R_M, height=None, wrap=False):
627 '''Locate the point on a path or polygon (with great circle arcs
628 joining consecutive points) closest to this point.
630 The closest point is either on within the extent of any great
631 circle arc or the nearest of the arc's end points.
633 @arg points: The path or polygon points (L{LatLon}[]).
634 @kwarg closed: Optionally, close the polygon (C{bool}).
635 @kwarg radius: Mean earth radius (C{meter}) or C{None}.
636 @kwarg height: Optional height, overriding the mean height
637 for a point within the arc (C{meter}).
638 @kwarg wrap: If C{True}, wrap or I{normalize} and unroll the
639 B{C{points}} (C{bool}).
641 @return: A L{NearestOn3Tuple}C{(closest, distance, angle)} of
642 the C{closest} point (L{LatLon}), the C{distance}
643 between this and the C{closest} point in C{meter},
644 same units as B{C{radius}} (or in C{radians} if
645 C{B{radius} is None}) and the C{angle} from this to
646 the C{closest} point in compass C{degrees360}.
648 @raise TypeError: Some B{C{points}} are not C{LatLon}.
650 @raise ValueError: No B{C{points}}.
651 '''
652 Ps = self.PointsIter(points, loop=1, wrap=wrap)
653 _d = self.distanceTo
654 _n = self.nearestOn
656 c = p1 = Ps[0]
657 r = _d(c, radius=None) # radians
658 for p2 in Ps.iterate(closed=closed):
659 if wrap and not Ps.looped:
660 p2 = _unrollon(p1, p2)
661 p = _n(p1, p2, height=height)
662 d = _d(p, radius=None) # radians
663 if d < r:
664 c, r = p, d
665 p1 = p2
666 d = r if radius is None else (Radius(radius) * r)
667 return NearestOn3Tuple(c, d, degrees360(r))
669 def toCartesian(self, **Cartesian_and_kwds): # PYCHOK Cartesian=Cartesian, datum=None
670 '''Convert this point to C{Nvector}-based cartesian (ECEF) coordinates.
672 @kwarg Cartesian_and_kwds: Optional L{Cartesian} and L{Cartesian} keyword
673 arguments, like C{datum}. Use C{B{Cartesian}=...}
674 to override this L{Cartesian} class or specify
675 C{B{Cartesian}=None}.
677 @return: A L{Cartesian} or if C{B{Cartesian} is None}, an L{Ecef9Tuple}C{(x, y,
678 z, lat, lon, height, C, M, datum)} with C{C} and C{M} if available.
680 @raise TypeError: Invalid L{Cartesian} or other B{C{Cartesian_and_kwds}} item.
681 '''
682 kwds = _xkwds(Cartesian_and_kwds, Cartesian=Cartesian, datum=self.datum)
683 return LatLonSphericalBase.toCartesian(self, **kwds)
685 def toNvector(self, **Nvector_and_kwds): # PYCHOK signature
686 '''Convert this point to C{Nvector} components, I{including height}.
688 @kwarg Nvector_and_kwds: Optional C{Nvector} and C{Nvector} keyword arguments.
689 Specify C{B{Nvector}=...} to override this C{Nvector}
690 class or use C{B{Nvector}=None}.
692 @return: An C{Nvector} or if C{B{Nvector} is None}, a L{Vector4Tuple}C{(x, y, z, h)}.
694 @raise TypeError: Invalid C{Nvector} or other B{C{Nvector_and_kwds}} item.
695 '''
696 return LatLonNvectorBase.toNvector(self, **_xkwds(Nvector_and_kwds, Nvector=Nvector))
699class Nvector(NvectorBase):
700 '''An n-vector is a position representation using a (unit) vector
701 normal to the earth's surface. Unlike lat-/longitude points,
702 n-vectors have no singularities or discontinuities.
704 For many applications, n-vectors are more convenient to work
705 with than other position representations like lat-/longitude,
706 earth-centred earth-fixed (ECEF) vectors, UTM coordinates, etc.
708 On a spherical model earth, an n-vector is equivalent to an
709 earth-centred earth-fixed (ECEF) vector.
711 Note commonality with L{pygeodesy.ellipsoidalNvector.Nvector}.
712 '''
713 _datum = Datums.Sphere # default datum (L{Datum})
715 @property_RO
716 def sphericalNvector(self):
717 '''Get this C{Nvector}'s spherical class.
718 '''
719 return type(self)
721 def toCartesian(self, **Cartesian_and_kwds): # PYCHOK Cartesian=Cartesian
722 '''Convert this n-vector to C{Nvector}-based cartesian
723 (ECEF) coordinates.
725 @kwarg Cartesian_and_kwds: Optional L{Cartesian} and L{Cartesian} keyword
726 arguments, like C{h}. Use C{B{Cartesian}=...}
727 to override this L{Cartesian} class or specify
728 C{B{Cartesian}=None}.
730 @return: The cartesian point (L{Cartesian}) or if B{C{Cartesian}} is
731 set to C{None}, an L{Ecef9Tuple}C{(x, y, z, lat, lon, height,
732 C, M, datum)} with C{C} and C{M} if available.
734 @raise TypeError: Invalid B{C{Cartesian_and_kwds}} argument.
735 '''
736 kwds = _xkwds(Cartesian_and_kwds, h=self.h, Cartesian=Cartesian)
737 return NvectorBase.toCartesian(self, **kwds) # class or .classof
739 def toLatLon(self, **LatLon_and_kwds): # PYCHOK height=None, LatLon=LatLon
740 '''Convert this n-vector to an C{Nvector}-based geodetic point.
742 @kwarg LatLon_and_kwds: Optional L{LatLon} and L{LatLon} keyword
743 arguments, like C{height}. Use C{B{LatLon}=...}
744 to override this L{LatLon} class or specify
745 C{B{LatLon}=None}.
747 @return: The geodetic point (L{LatLon}) or if B{C{LatLon}} is set
748 to C{None}, an L{Ecef9Tuple}C{(x, y, z, lat, lon, height,
749 C, M, datum)} with C{C} and C{M} if available.
751 @raise TypeError: Invalid B{C{LatLon_and_kwds}} argument.
753 @raise ValueError: Invalid B{C{height}}.
754 '''
755 kwds = _xkwds(LatLon_and_kwds, height=self.h, LatLon=LatLon)
756 return NvectorBase.toLatLon(self, **kwds) # class or .classof
758 def greatCircle(self, bearing):
759 '''Compute the n-vector normal to great circle obtained by
760 heading on given (initial) bearing from this point as its
761 n-vector.
763 Direction of vector is such that initial bearing vector
764 b = c × p.
766 @arg bearing: Initial bearing (compass C{degrees360}).
768 @return: N-vector representing great circle (C{Nvector}).
770 @raise Valuerror: Polar coincidence.
771 '''
772 s, c = sincos2d(Bearing(bearing))
774 e = NorthPole.cross(self, raiser=_pole_) # easting
775 n = self.cross(e, raiser=_point_) # northing
777 e = e.times(c / e.length)
778 n = n.times(s / n.length)
779 return n.minus(e)
782_Nv00 = LatLon(_0_0, _0_0, name=_Nv00_) # reference instance (L{LatLon})
785def areaOf(points, radius=R_M, wrap=False):
786 '''Calculate the area of a (spherical) polygon or composite (with
787 great circle arcs joining consecutive points).
789 @arg points: The polygon points or clips (C{LatLon}[],
790 L{BooleanFHP} or L{BooleanGH}).
791 @kwarg radius: Mean earth radius (C{meter}) or C{None}.
792 @kwarg wrap: If C{True}, wrap or I{normalize} and unroll the
793 B{C{points}} (C{bool}).
795 @return: Polygon area (C{meter} I{squared}, same units as
796 B{C{radius}}, or C{radians} if C{B{radius} is None}).
798 @raise PointsError: Insufficient number of B{C{points}}.
800 @raise TypeError: Some B{C{points}} are not L{LatLon}.
802 @see: Functions L{pygeodesy.areaOf}, L{sphericalTrigonometry.areaOf}
803 and L{ellipsoidalKarney.areaOf}.
804 '''
805 def _interangles(ps, w): # like .karney._polygon
806 Ps = _Nv00.PointsIter(ps, loop=2, wrap=w)
807 # use vector to 1st point as plane normal for sign of α
808 n0 = Ps[0].toNvector()
810 v2 = Ps[0]._N_vector # XXX v2 == no?
811 p1 = Ps[1]
812 v1 = p1._N_vector
813 gc = v2.cross(v1)
814 for p2 in Ps.iterate(closed=True):
815 if w and not Ps.looped:
816 p2 = _unrollon(p1, p2)
817 p1 = p2
818 v2 = p2._N_vector
819 gc1 = v1.cross(v2)
820 v1 = v2
821 yield gc.angleTo(gc1, vSign=n0)
822 gc = gc1
824 if _MODS.booleans.isBoolean(points):
825 r = points._sum2(LatLon, areaOf, radius=None, wrap=wrap)
826 else:
827 # sum interior angles: depending on whether polygon is cw or ccw,
828 # angle between edges is π−α or π+α, where α is angle between
829 # great-circle vectors; so sum α, then take n·π − |Σα| (cannot
830 # use Σ(π−|α|) as concave polygons would fail)
831 s = fsum(_interangles(points, wrap))
832 # using Girard’s theorem: A = [Σθᵢ − (n−2)·π]·R²
833 # (PI2 - abs(s) == (n*PI - abs(s)) - (n-2)*PI)
834 r = fabs(PI2 - fabs(s))
835 return r if radius is None else (r * Radius(radius)**2)
838def intersecant2(center, circle, point, other, **radius_exact_height_wrap):
839 '''Compute the intersections of a circle and a (great circle) line given as
840 two points or as a point and bearing.
842 @arg center: Center of the circle (L{LatLon}).
843 @arg circle: Radius of the circle (C{meter}, same units as the earth
844 B{C{radius}}) or a point on the circle (L{LatLon}).
845 @arg point: A point on the (great circle) line (L{LatLon}).
846 @arg other: An other point on the (great circle) line (L{LatLon}) or
847 the bearing at the B{C{point}} (compass C{degrees360}).
848 @kwarg radius_exact_height_wrap: Optional keyword arguments, see method
849 L{intersecant2<pygeodesy.sphericalBase.LatLonSphericalBase.
850 intersecant2>} for further details.
852 @return: 2-Tuple of the intersection points (representing a chord), each
853 an instance of the B{C{point}} class. Both points are the same
854 instance if the (great circle) line is tangent to the circle.
856 @raise IntersectionError: The circle and line do not intersect.
858 @raise TypeError: If B{C{center}}, B{C{point}}, B{C{circle}} or B{C{other}}
859 not L{LatLon}.
861 @raise UnitError: Invalid B{C{circle}}, B{C{other}}, B{C{radius}},
862 B{C{exact}}, B{C{height}} or B{C{napieradius}}.
863 '''
864 c = _Nv00.others(center=center)
865 p = _Nv00.others(point=point)
866 try:
867 return _intersecant2(c, circle, p, other, **radius_exact_height_wrap)
868 except (TypeError, ValueError) as x:
869 raise _xError(x, center=center, circle=circle, point=point, other=other,
870 **radius_exact_height_wrap)
873def intersection(start1, end1, start2, end2, height=None, wrap=False,
874 **LatLon_and_kwds):
875 '''Locate an intersection point of two (great circle) lines each defined
876 by two points or by a point and an (initial) bearing.
878 @return: The intersection point (L{LatLon}) or if C{B{LatLon}=None},
879 a cartesian L{Ecef9Tuple}C{(x, y, z, lat, lon, height, C, M,
880 datum)} with C{C} and C{M} if available.
882 @see: Function L{intersection2<sphericalNvector.intersection2>}
883 for further details.
884 '''
885 i, _, h = _intersect3(start1, end1, start2, end2, height, wrap)
886 kwds = _xkwds(LatLon_and_kwds, height=h, LatLon=LatLon)
887 return i.toLatLon(**kwds)
890def intersection2(start1, end1, start2, end2, height=None, wrap=False,
891 **LatLon_and_kwds):
892 '''Locate both intersections of two (great circle) lines each defined
893 by two points or by a point and an (initial) bearing.
895 @arg start1: Start point of the first line (L{LatLon}).
896 @arg end1: End point of the first line (L{LatLon}) or the bearing at
897 B{C{start1}} (compass C{degrees360}).
898 @arg start2: Start point of the second line (L{LatLon}).
899 @arg end2: End point of the second line (L{LatLon}) or the bearing at
900 B{C{start2}} (compass C{degrees360}).
901 @kwarg height: Optional height at the intersection and antipodal point,
902 overriding the mean height (C{meter}).
903 @kwarg wrap: If C{True}, wrap or I{normalize} and unroll B{C{start2}} and
904 both B{C{end*}} points (C{bool}).
905 @kwarg LatLon_and_kwds: Optional class C{B{LatLon}=}L{LatLon} to return
906 the intersection points and optionally, additional B{C{LatLon}}
907 keyword arguments, ignored if C{B{LatLon} is None}.
909 @return: 2-Tuple C{(intersection, antipode)}, each a (B{C{LatLon}}) or if
910 C{B{LatLon}=None}, a cartesian L{Ecef9Tuple}C{(x, y, z, lat, lon,
911 height, C, M, datum)} with C{C} and C{M} if available.
913 @raise TypeError: If B{C{start*}} or B{C{end*}} is not L{LatLon}.
915 @raise ValueError: Intersection is ambiguous or infinite or the lines are
916 parallel, coincident or null.
918 @see: Function L{sphericalNvector.intersection}.
919 '''
920 i, a, h = _intersect3(start1, end1, start2, end2, height, wrap)
921 kwds = _xkwds(LatLon_and_kwds, height=h, LatLon=LatLon)
922 return i.toLatLon(**kwds), a.toLatLon(**kwds)
925def _intersect3(start1, end1, start2, end2, height, wrap):
926 '''(INTERNAL) Return the intersection and antipodal points for
927 functions C{intersection} and C{intersection2}.
928 '''
929 p1 = _Nv00.others(start1=start1)
930 p2 = _Nv00.others(start2=start2)
931 if wrap:
932 p2 = _unrollon(p1, p2, wrap=wrap)
933 # If gc1 and gc2 are great circles through start and end points
934 # (or defined by start point and bearing), then the candidate
935 # intersections are simply gc1 × gc2 and gc2 × gc1. Most of the
936 # work is deciding the correct intersection point to select! If
937 # bearing is given, that determines the intersection, but if both
938 # lines are defined by start/end points, take closer intersection.
939 gc1, s1, e1 = _Nv00._gc3(p1, end1, 'end1', wrap=wrap)
940 gc2, s2, e2 = _Nv00._gc3(p2, end2, 'end2', wrap=wrap)
942 hs = start1.height, start2.height
943 # there are two (antipodal) candidate intersection
944 # points ... we have to choose the one to return
945 i1 = gc1.cross(gc2, raiser=_lines_)
946 i2 = gc2.cross(gc1, raiser=_lines_)
948 # selection of intersection point depends on how
949 # lines are defined (by bearings or endpoints)
950 if e1 and e2: # endpoint+endpoint
951 d = sumOf((s1, s2, e1, e2)).dot(i1)
952 hs += end1.height, end2.height
953 elif e1 and not e2: # endpoint+bearing
954 # gc2 x v2 . i1 +ve means v2 bearing points to i1
955 d = gc2.cross(s2).dot(i1)
956 hs += end1.height,
957 elif e2 and not e1: # bearing+endpoint
958 # gc1 x v1 . i1 +ve means v1 bearing points to i1
959 d = gc1.cross(s1).dot(i1)
960 hs += end2.height,
961 else: # bearing+bearing
962 # if gc x v . i1 is +ve, initial bearing is
963 # towards i1, otherwise towards antipodal i2
964 d1 = gc1.cross(s1).dot(i1) # +ve means p1 bearing points to i1
965 d2 = gc2.cross(s2).dot(i1) # +ve means p2 bearing points to i1
966 if d1 > 0 and d2 > 0:
967 d = 1 # both point to i1
968 elif d1 < 0 and d2 < 0:
969 d = -1 # both point to i2
970 else: # d1, d2 opposite signs
971 # intersection is at further-away intersection point,
972 # take opposite intersection from mid- point of v1
973 # and v2 [is this always true?] XXX changed to always
974 # get intersection p1 bearing points to, aka being
975 # located "after" p1 along the bearing at p1, like
976 # function .sphericalTrigonometry._intersect and
977 # .ellipsoidalBaseDI._intersect3
978 d = d1 # neg(s1.plus(s2).dot(i1))
980 h = fmean(hs) if height is None else height
981 return (i1, i2, h) if d > 0 else (i2, i1, h)
984def meanOf(points, height=None, wrap=False, **LatLon_and_kwds):
985 '''Compute the I{geographic} mean of the supplied points.
987 @arg points: Array of points to be averaged (L{LatLon}[]).
988 @kwarg height: Optional height, overriding the mean height (C{meter}).
989 @kwarg wrap: If C{True}, wrap or I{normalize} B{C{points}} (C{bool}).
990 @kwarg LatLon_and_kwds: Optional class C{B{LatLon}=}L{LatLon} to return
991 the mean point and optionally, additional B{C{LatLon}}
992 keyword arguments, ignored if C{B{LatLon} is None}.
994 @return: Point at geographic mean and mean height (B{C{LatLon}}).
996 @raise PointsError: Insufficient number of B{C{points}} or some
997 B{C{points}} are not C{LatLon}.
998 '''
999 def _N_vs(ps, w):
1000 Ps = _Nv00.PointsIter(ps, wrap=w)
1001 for p in Ps.iterate(closed=False):
1002 yield p._N_vector
1004 try:
1005 # geographic mean
1006 n = _nsumOf(_N_vs(points, wrap), height, Nvector, {})
1007 except (TypeError, ValueError) as x:
1008 raise PointsError(points=points, wrap=wrap, cause=x, **LatLon_and_kwds)
1009 return n.toLatLon(**_xkwds(LatLon_and_kwds, LatLon=LatLon, height=n.h,
1010 name=typename(meanOf)))
1013@deprecated_function
1014def nearestOn2(point, points, **closed_radius_height): # PYCHOK no cover
1015 '''DEPRECATED, use method L{sphericalNvector.nearestOn3}.
1017 @return: ... 2-Tuple C{(closest, distance)} of the C{closest}
1018 point (L{LatLon}) on the polygon and the C{distance}
1019 between the C{closest} and the given B{C{point}} ...
1020 '''
1021 r = nearestOn3(point, points, **closed_radius_height)
1022 return r.closest, r.distance
1025def nearestOn3(point, points, closed=False, radius=R_M, height=None, wrap=False):
1026 '''Locate the point on a polygon (with great circle arcs joining
1027 consecutive points) closest to an other point.
1029 If the given point is between the end points of a great circle
1030 arc, the closest point is on that arc. Otherwise, the closest
1031 point is the nearest of the arc's end points.
1033 @arg point: The other, reference point (L{LatLon}).
1034 @arg points: The polygon points (L{LatLon}[]).
1035 @kwarg closed: Optionally, close the polygon (C{bool}).
1036 @kwarg radius: Mean earth radius (C{meter}) or C{None}.
1037 @kwarg height: Optional height, overriding the mean height for
1038 a point within the (great circle) arc (C{meter}).
1039 @kwarg wrap: If C{True}, wrap or I{normalize} and unroll the
1040 B{C{points}} (C{bool}).
1042 @return: A L{NearestOn3Tuple}C{(closest, distance, angle)} of
1043 the C{closest} point (L{LatLon}) on the polygon, the
1044 C{distance} and the C{angle} between the C{closest}
1045 and the given B{C{point}}. The C{distance} is in
1046 C{meter}, same units as B{C{radius}} or in C{radians}
1047 if C{B{radius} is None}, the C{angle} is in compass
1048 C{degrees360}.
1050 @raise PointsError: Insufficient number of B{C{points}}.
1052 @raise TypeError: Some B{C{points}} or B{C{point}} not C{LatLon}.
1053 '''
1054 _xinstanceof(LatLon, point=point)
1056 return point.nearestOn3(points, closed=closed, radius=radius,
1057 height=height, wrap=wrap)
1060def perimeterOf(points, closed=False, radius=R_M, wrap=False):
1061 '''Compute the perimeter of a (spherical) polygon or composite (with
1062 great circle arcs joining consecutive points).
1064 @arg points: The polygon points (L{LatLon}[]).
1065 @kwarg closed: Optionally, close the polygon (C{bool}).
1066 @kwarg radius: Mean earth radius (C{meter}) or C{None}.
1067 @kwarg wrap: If C{True}, wrap or I{normalize} and unroll the
1068 B{C{points}} (C{bool}).
1070 @return: Polygon perimeter (C{meter}, same units as B{C{radius}}
1071 or C{radians} if C{B{radius} is None}).
1073 @raise PointsError: Insufficient number of B{C{points}}.
1075 @raise TypeError: Some B{C{points}} are not L{LatLon}.
1077 @raise ValueError: Invalid B{C{radius}} or C{B{closed}=False} with
1078 C{B{points}} a composite.
1080 @see: Functions L{pygeodesy.perimeterOf}, L{ellipsoidalKarney.perimeterOf}
1081 and L{sphericalTrigonometry.perimeterOf}.
1082 '''
1083 def _rads(ps, c, w): # angular edge lengths in radians
1084 Ps = _Nv00.PointsIter(ps, loop=1, wrap=w)
1085 p1 = Ps[0]
1086 v1 = p1._N_vector
1087 for p2 in Ps.iterate(closed=c):
1088 if w and not (c and Ps.looped):
1089 p2 = _unrollon(p1, p2)
1090 p1 = p2
1091 v2 = p2._N_vector
1092 yield v1.angleTo(v2)
1093 v1 = v2
1095 if _MODS.booleans.isBoolean(points):
1096 if not closed:
1097 notImplemented(None, closed=closed, points=_composite_)
1098 r = points._sum2(LatLon, perimeterOf, closed=True, radius=None, wrap=wrap)
1099 else:
1100 r = fsum(_rads(points, closed, wrap))
1101 return r if radius is None else (Radius(radius) * r)
1104def sumOf(nvectors, Vector=Nvector, h=None, **Vector_kwds):
1105 '''Return the I{vectorial} sum of two or more n-vectors.
1107 @arg nvectors: Vectors to be added (C{Nvector}[]).
1108 @kwarg Vector: Optional class for the vectorial sum (C{Nvector}).
1109 @kwarg h: Optional height, overriding the mean height (C{meter}).
1110 @kwarg Vector_kwds: Optional, additional B{C{Vector}} keyword arguments.
1112 @return: Vectorial sum (B{C{Vector}}).
1114 @raise VectorError: No B{C{nvectors}}.
1115 '''
1116 try:
1117 return _nsumOf(nvectors, h, Vector, Vector_kwds)
1118 except (TypeError, ValueError) as x:
1119 raise VectorError(nvectors=nvectors, Vector=Vector, cause=x)
1122def triangulate(point1, bearing1, point2, bearing2,
1123 height=None, wrap=False,
1124 LatLon=LatLon, **LatLon_kwds):
1125 '''Locate a point given two known, reference points and the (initial)
1126 bearing from those points.
1128 @arg point1: First reference point (L{LatLon}).
1129 @arg bearing1: Bearing at the first point (compass C{degrees360}).
1130 @arg point2: Second reference point (L{LatLon}).
1131 @arg bearing2: Bearing at the second point (compass C{degrees360}).
1132 @kwarg height: Optional height at the triangulated point, overriding
1133 the mean height (C{meter}).
1134 @kwarg wrap: If C{True}, wrap or I{normalize} and unroll B{C{point2}}
1135 (C{bool}).
1136 @kwarg LatLon: Optional class to return the triangulated point (L{LatLon}).
1137 @kwarg LatLon_kwds: Optional, additional B{C{LatLon}} keyword
1138 arguments, ignored if C{B{LatLon} is None}.
1140 @return: Triangulated point (B{C{LatLon}}).
1142 @raise TypeError: If B{C{point1}} or B{C{point2}} is not L{LatLon}.
1144 @raise Valuerror: Points coincide.
1145 '''
1146 return _triangulate(_Nv00.others(point1=point1), bearing1,
1147 _Nv00.others(point2=point2), bearing2,
1148 height=height, wrap=wrap,
1149 LatLon=LatLon, **LatLon_kwds)
1152def trilaterate(point1, distance1, point2, distance2, point3, distance3, # PYCHOK args
1153 radius=R_M, height=None, useZ=False, wrap=False,
1154 LatLon=LatLon, **LatLon_kwds):
1155 '''Locate a point at given distances from three other points.
1157 @arg point1: First point (L{LatLon}).
1158 @arg distance1: Distance to the first point (C{meter}, same units
1159 as B{C{radius}}).
1160 @arg point2: Second point (L{LatLon}).
1161 @arg distance2: Distance to the second point (C{meter}, same units
1162 as B{C{radius}}).
1163 @arg point3: Third point (L{LatLon}).
1164 @arg distance3: Distance to the third point (C{meter}, same units
1165 as B{C{radius}}).
1166 @kwarg radius: Mean earth radius (C{meter}).
1167 @kwarg height: Optional height at the trilaterated point, overriding
1168 the IDW height (C{meter}, same units as B{C{radius}}).
1169 @kwarg useZ: Include Z component iff non-NaN, non-zero (C{bool}).
1170 @kwarg wrap: If C{True}, wrap or I{normalize} and unroll B{C{point2}}
1171 and B{C{point3}} (C{bool}).
1172 @kwarg LatLon: Optional class to return the trilaterated point (L{LatLon}).
1173 @kwarg LatLon_kwds: Optional, additional B{C{LatLon}} keyword arguments,
1174 ignored if C{B{LatLon} is None}.
1176 @return: Trilaterated point (B{C{LatLon}}).
1178 @raise IntersectionError: No intersection, trilateration failed.
1180 @raise TypeError: Invalid B{C{point1}}, B{C{point2}} or B{C{point3}}.
1182 @raise ValueError: Coincident B{C{points}} or invalid B{C{distance1}},
1183 B{C{distance2}}, B{C{distance3}} or B{C{radius}}.
1185 @see: U{Trilateration<https://WikiPedia.org/wiki/Trilateration>}.
1186 '''
1187 return _trilaterate(_Nv00.others(point1=point1), distance1,
1188 _Nv00.others(point2=point2), distance2,
1189 _Nv00.others(point3=point3), distance3,
1190 radius=radius, height=height, useZ=useZ,
1191 wrap=wrap, LatLon=LatLon, **LatLon_kwds)
1194__all__ += _ALL_OTHER(Cartesian, LatLon, Nvector, # classes
1195 areaOf, # functions
1196 intersecant2, intersection, intersection2, ispolar,
1197 meanOf,
1198 nearestOn2, nearestOn3,
1199 perimeterOf,
1200 sumOf,
1201 triangulate, trilaterate)
1203# **) MIT License
1204#
1205# Copyright (C) 2016-2025 -- mrJean1 at Gmail -- All Rights Reserved.
1206#
1207# Permission is hereby granted, free of charge, to any person obtaining a
1208# copy of this software and associated documentation files (the "Software"),
1209# to deal in the Software without restriction, including without limitation
1210# the rights to use, copy, modify, merge, publish, distribute, sublicense,
1211# and/or sell copies of the Software, and to permit persons to whom the
1212# Software is furnished to do so, subject to the following conditions:
1213#
1214# The above copyright notice and this permission notice shall be included
1215# in all copies or substantial portions of the Software.
1216#
1217# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
1218# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
1219# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
1220# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
1221# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
1222# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
1223# OTHER DEALINGS IN THE SOFTWARE.