1

2# -*- coding: utf-8 -*-

3

4 u'''Ellipsoidal, C{N-vector}-based geodesy.

5

6 Ellipsoidal classes geodetic L{LatLon}, geocentric (ECEF) L{Cartesian}

7 and C{Nvector} and DEPRECATED L{Ned} and functions L{meanOf}, L{sumOf}

8 and DEPRECATED L{toNed}.

9

10 Pure Python implementation of n-vector-based geodetic (lat-/longitude)

11 methods by I{(C) Chris Veness 2011-2024} published under the same MIT

12 Licence**, see U{Vector-based geodesy

13<https://www.Movable-Type.co.UK/scripts/latlong-vectors.html>}.

14

15 These classes and functions work with: (a) geodetic lat-/longitude points on

16 the earth's surface and (b) 3-D vectors used as n-vectors representing points

17 on the earth's surface or vectors normal to the plane of a great circle.

18

19 See also I{Kenneth Gade} U{'A Non-singular Horizontal Position Representation'

20<https://www.NavLab.net/Publications/A_Nonsingular_Horizontal_Position_Representation.pdf>},

21 The Journal of Navigation (2010), vol 63, nr 3, pp 395-417.

22'''

23# make sure int/int division yields float quotient, see .basics

24 from __future__ import division as _; del _ # PYCHOK semicolon

25

26 from pygeodesy.basics import _isin, issubclassof, map2, _xinstanceof, \

27 _xsubclassof

28 from pygeodesy.datums import _earth_ellipsoid, _ellipsoidal_datum, _WGS84

29# from pygeodesy.dms import F_D, toDMS # _MODS

30# from pygeodesy.ecef import EcefVeness # _MODS

31 from pygeodesy.ellipsoidalBase import CartesianEllipsoidalBase, \

32 _nearestOn, LatLonEllipsoidalBase, \

33 _TOL_M, _Wrap

34 from pygeodesy.errors import _xkwds, _xkwds_pop2

35# from pygeodesy.fmath import fdot # from .nvectorBase

36# from pygeodesy.formy import _isequalTo # _MODS

37 from pygeodesy.interns import _Nv00_, _COMMASPACE_, _pole_ # PYCHOK used!

38 from pygeodesy.lazily import _ALL_LAZY, _ALL_MODS as _MODS, _ALL_OTHER

39# from pygeodesy.ltp import Ltp # _MODS

40 from pygeodesy.ltpTuples import Aer as _Aer, Ned as _Ned, Ned4Tuple, \

41 sincos2d_, _xnamed

42# from pygeodesy.named import _xnamed # from .ltpTuples

43 from pygeodesy.nvectorBase import LatLonNvectorBase, NorthPole, NvectorBase, \

44 sumOf as _sumOf, fabs, fdot

45 from pygeodesy.props import deprecated_class, deprecated_function, \

46 deprecated_method, Property_RO, property_RO

47 from pygeodesy.streprs import Fmt, fstr, _xzipairs

48 from pygeodesy.units import Bearing, Distance, Height, Scalar

49# from pygeodesy.utily import sincos2d_, _Wrap # from .ltpTuples, .ellipsoidalBase

50

51# from math import fabs # from .nvectorBase

52

53__all__ = _ALL_LAZY.ellipsoidalNvector

54__version__ = '25.04.21'

55

56

57 class Ned(_Ned):

58 '''DEPRECATED on 2024.02.04, use class L{pygeodesy.Ned}.'''

59

60 def __init__(self, north, east, down, **name):

61 deprecated_class(self.__class__)

62 _Ned.__init__(self, north, east, down, **name)

63

64 @deprecated_method # PYCHOK expected

65 def toRepr(self, prec=None, fmt=Fmt.SQUARE, sep=_COMMASPACE_, **unused):

66 '''DEPRECATED, use class L{pygeodesy.Ned}.

67

68 @kwarg prec: Number of (decimal) digits, unstripped (C{int}).

69 @kwarg fmt: Enclosing backets format (C{str}).

70 @kwarg sep: Separator between NEDs (C{str}).

71

72 @return: This Ned as "[L:f, B:degrees360, E:degrees90]" (C{str})

73 with length or slantrange C{L}, bearing or azimuth C{B}

74 and elevation C{E}.

75 '''

76 m = _MODS.dms

77 t = (fstr(self.slantrange, prec=prec),

78 m.toDMS(self.azimuth, form=m.F_D, prec=prec, ddd=0),

79 m.toDMS(self.elevation, form=m.F_D, prec=prec, ddd=0))

80 return _xzipairs('LBE', t, sep=sep, fmt=fmt)

81

82

83 class Cartesian(CartesianEllipsoidalBase):

84 '''Extended to convert geocentric, L{Cartesian} points to

85 C{Nvector} and n-vector-based, geodetic L{LatLon}.

86 '''

87 @property_RO

88 def Ecef(self):

89 '''Get the ECEF I{class} (L{EcefVeness}), I{once}.

90 '''

91 return _Ecef()

92

93 def toLatLon(self, **LatLon_and_kwds): # PYCHOK LatLon=LatLon, datum=None

94 '''Convert this cartesian to an C{Nvector}-based geodetic point.

95

96 @kwarg LatLon_and_kwds: Optional L{LatLon}, B{C{datum}} and other

97 keyword arguments. Use C{B{LatLon}=...} to

98 override this L{LatLon} class or specify

99 C{B{LatLon} is None}.

100

101 @return: The geodetic point (L{LatLon}) or if C{B{LatLon} is None},

102 an L{Ecef9Tuple}C{(x, y, z, lat, lon, height, C, M, datum)}

103 with C{C} and C{M} if available.

104

105 @raise TypeError: Invalid B{C{LatLon_and_kwds}}.

106 '''

107 kwds = _xkwds(LatLon_and_kwds, LatLon=LatLon, datum=self.datum)

108 return CartesianEllipsoidalBase.toLatLon(self, **kwds)

109

110 def toNvector(self, **Nvector_and_kwds): # PYCHOK Datums.WGS84

111 '''Convert this cartesian to C{Nvector} components, I{including height}.

112

113 @kwarg Nvector_and_kwds: Optional C{Nvector}, B{C{datum}} and other

114 keyword arguments. Use C{B{Nvector}=...} to

115 override this C{Nvector} class or specify

116 C{B{Nvector} is None}.

117

118 @return: The C{n-vector} components (C{Nvector}) or if C{B{Nvector}

119 is None}, a L{Vector4Tuple}C{(x, y, z, h)}.

120

121 @raise TypeError: Invalid B{C{Nvector_and_kwds}}.

122 '''

123 kwds = _xkwds(Nvector_and_kwds, Nvector=Nvector, datum=self.datum)

124 return CartesianEllipsoidalBase.toNvector(self, **kwds)

125

126

127 class LatLon(LatLonNvectorBase, LatLonEllipsoidalBase):

128 '''An n-vector-based, ellipsoidal L{LatLon} point.

129 '''

130 _Nv = None # cached toNvector (C{Nvector})

131

132 def _update(self, updated, *attrs, **setters): # PYCHOK args

133 '''(INTERNAL) Zap cached attributes if updated.

134 '''

135 if updated:

136 LatLonNvectorBase._update(self, updated, _Nv=self._Nv) # special case

137 LatLonEllipsoidalBase._update(self, updated, *attrs, **setters)

138

139# def crossTrackDistanceTo(self, start, end, radius=R_M):

140# '''Return the (signed) distance from this point to the great

141# circle defined by a start point and an end point or bearing.

142#

143# @arg start: Start point of great circle line (L{LatLon}).

144# @arg end: End point of great circle line (L{LatLon}) or

145# initial bearing (compass C{degrees360}) at the

146# start point.

147# @kwarg radius: Mean earth radius (C{meter}).

148#

149# @return: Distance to great circle, negative if to left or

150# positive if to right of line (C{meter}, same units

151# as B{C{radius}}).

152#

153# @raise TypeError: If B{C{start}} or B{C{end}} point is not L{LatLon}.

154# '''

155# self.others(start=start)

156#

157# if _isDegrees(end): # gc from point and bearing

158# gc = start.greatCircle(end)

159# else: # gc by two points

160# gc = start.toNvector().cross(end.toNvector())

161#

162# # (signed) angle between point and gc normal vector

163# v = self.toNvector()

164# a = gc.angleTo(v, vSign=v.cross(gc))

165# a = _copysign(PI_2, a) - a

166# return a * float(radius)

167

168 def deltaTo(self, other, wrap=False, **Ned_and_kwds):

169 '''Calculate the local delta from this to an other point.

170

171 @note: This is a linear delta, I{unrelated} to a geodesic on the

172 ellipsoid.

173

174 @arg other: The other point (L{LatLon}).

175 @kwarg wrap: If C{True}, wrap or I{normalize} the B{C{other}}

176 point (C{bool}).

177 @kwarg Ned_and_kwds: Optional C{B{Ned}=L{Ned} class and B{name}=NN}

178 to return delta and other B{C{Ned}} keyword arguments.

179

180 @return: Delta from this to the other point (B{C{Ned}}).

181

182 @raise TypeError: The B{C{other}} point is not L{LatLon} or B{C{Ned}}

183 is not an L{Ned4Tuple<pygeodesy.Ned4Tuple>} nor an

184 L{Ned<pygeodesy.Ned>} nor a DEPRECATED L{Ned}.

185

186 @raise ValueError: If ellipsoids are incompatible.

187 '''

188 self.ellipsoids(other) # throws TypeError and ValueError

189

190 p = self.others(other)

191 if wrap:

192 p = _Wrap.point(p)

193 # get delta in cartesian frame

194 dc = p.toCartesian().minus(self.toCartesian())

195 # rotate dc to get delta in n-vector reference

196 # frame using the rotation matrix row vectors

197 ned_ = map2(dc.dot, self._rotation3)

198

199 N, kwds = _xkwds_pop2(Ned_and_kwds, Ned=Ned)

200 if issubclassof(N, Ned4Tuple):

201 ned_ += _MODS.ltp.Ltp(self, ecef=self.Ecef(self.datum)),

202 else:

203 _xsubclassof(_Ned, Ned4Tuple, Ned=N)

204 return N(*ned_, **_xkwds(kwds, name=self.name))

205

206# def destination(self, distance, bearing, radius=R_M, height=None):

207# '''Return the destination point after traveling from this

208# point the given distance on the given initial bearing.

209#

210# @arg distance: Distance traveled (C{meter}, same units as

211# given earth B{C{radius}}).

212# @arg bearing: Initial bearing (compass C{degrees360}).

213# @kwarg radius: Mean earth radius (C{meter}).

214# @kwarg height: Optional height at destination point,

215# overriding default (C{meter}, same units

216# as B{C{radius}}).

217#

218# @return: Destination point (L{LatLon}).

219# '''

220# r = _m2radians(distance, radius) # angular distance in radians

221# # great circle by starting from this point on given bearing

222# gc = self.greatCircle(bearing)

223#

224# v1 = self.toNvector()

225# x = v1.times(cos(r)) # component of v2 parallel to v1

226# y = gc.cross(v1).times(sin(r)) # component of v2 perpendicular to v1

227#

228# v2 = x.plus(y).unit()

229# return v2.toLatLon(height=self._heigHt(height))

230

231 def destinationNed(self, delta):

232 '''Calculate the destination point using the supplied NED delta

233 from this point.

234

235 @arg delta: Delta from this to the other point in the local

236 tangent plane (LTP) of this point (L{Ned}).

237

238 @return: Destination point (L{LatLon}).

239

240 @raise TypeError: If B{C{delta}} is not an L{Ned<pygeodesy.Ned>}

241 or a DEPRECATED L{Ned}.

242 '''

243 _xinstanceof(_Ned, delta=delta)

244

245 nv, ev, dv = self._rotation3

246 # convert NED delta to standard coordinate frame of n-vector

247 dn = delta.ned[:3] # XXX Ned4Tuple.to3Tuple

248 # rotate dn to get delta in cartesian (ECEF) coordinate

249 # reference frame using the rotation matrix column vectors

250 dc = Cartesian(fdot(dn, nv.x, ev.x, dv.x),

251 fdot(dn, nv.y, ev.y, dv.y),

252 fdot(dn, nv.z, ev.z, dv.z))

253

254 # apply (cartesian) delta to this Cartesian to obtain destination as cartesian

255 v = self.toCartesian().plus(dc)

256 return v.toLatLon(datum=self.datum, LatLon=self.classof) # Cartesian(v.x, v.y, v.z).toLatLon(...)

257

258 def distanceTo(self, other, radius=None, wrap=False):

259 '''I{Approximate} the distance from this to an other point.

260

261 @arg other: The other point (L{LatLon}).

262 @kwarg radius: Mean earth radius, ellipsoid or datum (C{meter},

263 L{Ellipsoid}, L{Ellipsoid2}, L{Datum} or

264 L{a_f2Tuple}), overriding the mean radius C{R1}

265 of this point's datum..

266 @kwarg wrap: If C{True}, wrap or I{normalize} and unroll the

267 B{C{other}} and angular distance (C{bool}).

268

269 @return: Distance (C{meter}, same units as B{C{radius}}).

270

271 @raise TypeError: The B{C{other}} point is not L{LatLon}.

272

273 @raise ValueError: Invalid B{C{radius}}.

274 '''

275 p = self.others(other)

276 if wrap:

277 p = _Wrap.point(p)

278 a = self._N_vector.angleTo(p._N_vector, wrap=wrap)

279 E = self.datum.ellipsoid if radius is None else _earth_ellipsoid(radius)

280 return fabs(a) * E.R1 # see .utily.radians2m

281

282 @property_RO

283 def Ecef(self):

284 '''Get the ECEF I{class} (L{EcefVeness}), I{once}.

285 '''

286 return _Ecef()

287

288 @deprecated_method

289 def equals(self, other, eps=None): # PYCHOK no cover

290 '''DEPRECATED, use method L{isequalTo}.

291 '''

292 return self.isequalTo(other, eps=eps)

293

294 def isequalTo(self, other, eps=None):

295 '''Compare this point with an other point.

296

297 @arg other: The other point (L{LatLon}).

298 @kwarg eps: Optional margin (C{float}).

299

300 @return: C{True} if points are identical, including

301 datum, I{ignoring height}, C{False} otherwise.

302

303 @raise TypeError: The B{C{other}} point is not L{LatLon}.

304

305 @raise ValueError: Invalid B{C{eps}}.

306

307 @see: Method C{isequalTo3} to include I{height}.

308 '''

309 return self.datum == self.others(other).datum and \

310 _MODS.formy._isequalTo(self, other, eps=eps)

311

312# def greatCircle(self, bearing):

313# '''Return the great circle heading on the given bearing

314# from this point.

315#

316# Direction of vector is such that initial bearing vector

317# b = c × p, where p is representing this point.

318#

319# @arg bearing: Bearing from this point (compass C{degrees360}).

320#

321# @return: N-vector representing great circle (C{Nvector}).

322# '''

323# a, b, _ = self.philamheight

324# t = radians(bearing)

325#

326# sa, ca, sb, cb, st, ct = sincos2_(a, b, t)

327# return self._xnamed(Nvector(sb * ct - sa * cb * st,

328# -cb * ct - sa * sb * st,

329# ca * st)

330

331# def initialBearingTo(self, other, wrap=False):

332# '''Return the initial bearing (forward azimuth) from

333# this to an other point.

334#

335# @arg other: The other point (L{LatLon}).

336# @kwarg wrap: If C{True}, wrap or I{normalize}

337# and unroll the B{C{other}} (C{bool}).

338#

339# @return: Initial bearing (compass C{degrees360}).

340#

341# @raise TypeError: The B{C{other}} point is not L{LatLon}.

342# '''

343# p = self.others(other)

344# if wrap:

345# p = _Wrap.point(p)

346# v1 = self.toNvector()

347#

348# gc1 = v1.cross(p.toNvector()) # gc through v1 & v2

349# gc2 = v1.cross(_NP3) # gc through v1 & North pole

350#

351# # bearing is (signed) angle between gc1 & gc2

352# return degrees360(gc1.angleTo(gc2, vSign=v1))

353

354 def intermediateTo(self, other, fraction, height=None, wrap=False):

355 '''Return the point at given fraction between this and

356 an other point.

357

358 @arg other: The other point (L{LatLon}).

359 @arg fraction: Fraction between both points (C{scalar},

360 0.0 at this to 1.0 at the other point.

361 @kwarg height: Optional height, overriding the fractional

362 height (C{meter}).

363 @kwarg wrap: If C{True}, wrap or I{normalize} the

364 B{C{other}} point (C{bool}).

365

366 @return: Intermediate point (L{LatLon}).

367

368 @raise TypeError: The B{C{other}} point is not L{LatLon}.

369 '''

370 p = self.others(other)

371 if wrap:

372 p = _Wrap.point(p)

373 f = Scalar(fraction=fraction)

374 h = self._havg(other, f=f, h=height)

375 i = self.toNvector().intermediateTo(p.toNvector(), f)

376 return i.toLatLon(height=h, LatLon=self.classof) # Nvector(i.x, i.y, i.z).toLatLon(...)

377

378 @Property_RO

379 def _rotation3(self):

380 '''(INTERNAL) Get the rotation matrix from n-vector coordinate frame axes.

381 '''

382 nv = self.toNvector() # local (n-vector) coordinate frame

383

384 dv = nv.negate() # down, opposite to n-vector

385 ev = NorthPole.cross(nv, raiser=_pole_).unit() # east, pointing perpendicular to the plane

386 nv = ev.cross(dv) # north, by right hand rule

387 return nv, ev, dv # matrix rows

388

389 def toCartesian(self, **Cartesian_and_kwds): # PYCHOK Cartesian=Cartesian, datum=None

390 '''Convert this point to an C{Nvector}-based geodetic point.

391

392 @kwarg Cartesian_and_kwds: Optional L{Cartesian}, B{C{datum}} and other

393 keyword arguments. Use C{B{Cartesian}=...}

394 to override this L{Cartesian} class or specify

395 C{B{Cartesian}=None}.

396

397 @return: The geodetic point (L{Cartesian}) or if C{B{Cartesian} is None},

398 an L{Ecef9Tuple}C{(x, y, z, lat, lon, height, C, M, datum)} with

399 C{C} and C{M} if available.

400

401 @raise TypeError: Invalid B{C{Cartesian}} or other B{C{Cartesian_and_kwds}}.

402 '''

403 kwds = _xkwds(Cartesian_and_kwds, Cartesian=Cartesian, datum=self.datum)

404 return LatLonEllipsoidalBase.toCartesian(self, **kwds)

405

406 def toNvector(self, **Nvector_and_kwds): # PYCHOK signature

407 '''Convert this point to C{Nvector} components, I{including height}.

408

409 @kwarg Nvector_and_kwds: Optional C{Nvector}, B{C{datum}} and other

410 keyword arguments. Use C{B{Nvector}=...}

411 to override this C{Nvector} class or specify

412 C{B{Nvector}=None}.

413

414 @return: The C{n-vector} components (C{Nvector}) or if B{C{Nvector}}

415 is set to C{None}, a L{Vector4Tuple}C{(x, y, z, h)}.

416

417 @raise TypeError: Invalid B{C{Nvector}} or other B{C{Nvector_and_kwds}}.

418 '''

419 kwds = _xkwds(Nvector_and_kwds, Nvector=Nvector, datum=self.datum)

420 return LatLonNvectorBase.toNvector(self, **kwds)

421

422

423_Nv00 = LatLon(0, 0, name=_Nv00_) # reference instance (L{LatLon})

424

425

426 class Nvector(NvectorBase):

427 '''An n-vector is a position representation using a (unit) vector

428 normal to the earth ellipsoid. Unlike lat-/longitude points,

429 n-vectors have no singularities or discontinuities.

430

431 For many applications, n-vectors are more convenient to work

432 with than other position representations like lat-/longitude,

433 earth-centred earth-fixed (ECEF) vectors, UTM coordinates, etc.

434

435 Note commonality with L{pygeodesy.sphericalNvector.Nvector}.

436 '''

437 _datum = _WGS84 # default datum (L{Datum})

438

439 def __init__(self, x_xyz, y=None, z=None, h=0, datum=None, ll=None, **name):

440 '''New n-vector normal to the earth's surface.

441

442 @arg x_xyz: X component of vector (C{scalar}) or (3-D) vector

443 (C{Nvector}, L{Vector3d}, L{Vector3Tuple} or

444 L{Vector4Tuple}).

445 @kwarg y: Y component of vector (C{scalar}), ignored if B{C{x_xyz}}

446 is not C{scalar}, otherwise same units as B{C{x_xyz}}.

447 @kwarg z: Z component of vector (C{scalar}), ignored if B{C{x_xyz}}

448 is not C{scalar}, otherwise same units as B{C{x_xyz}}.

449 @kwarg h: Optional height above model surface (C{meter}).

450 @kwarg datum: Optional datum this n-vector is defined in

451 (L{Datum}, L{Ellipsoid}, L{Ellipsoid2} or

452 L{a_f2Tuple}).

453 @kwarg ll: Optional, original latlon (C{LatLon}).

454 @kwarg name: Optional C{B{name}=NN} (C{str}).

455

456 @raise TypeError: If B{C{datum}} is not a L{Datum}.

457 '''

458 NvectorBase.__init__(self, x_xyz, y=y, z=z, h=h, ll=ll, **name)

459 if not _isin(datum, None, self._datum):

460 self._datum = _ellipsoidal_datum(datum, **name)

461

462 @Property_RO

463 def datum(self):

464 '''Get this n-vector's datum (L{Datum}).

465 '''

466 return self._datum

467

468 @property_RO

469 def ellipsoidalNvector(self):

470 '''Get this C{Nvector}'s ellipsoidal class.

471 '''

472 return type(self)

473

474 def toCartesian(self, **Cartesian_and_kwds): # PYCHOK Cartesian=Cartesian

475 '''Convert this n-vector to C{Nvector}-based cartesian (ECEF) coordinates.

476

477 @kwarg Cartesian_and_kwds: Optional L{Cartesian}, B{C{h}}, B{C{datum}} and

478 other keyword arguments. Use C{B{Cartesian}=...}

479 to override this L{Cartesian} class or specify

480 C{B{Cartesian} is None}.

481

482 @return: The cartesian point (L{Cartesian}) or if C{B{Cartesian} is None},

483 an L{Ecef9Tuple}C{(x, y, z, lat, lon, height, C, M, datum)} with

484 C{C} and C{M} if available.

485

486 @raise TypeError: Invalid B{C{Cartesian_and_kwds}}.

487 '''

488 kwds = _xkwds(Cartesian_and_kwds, h=self.h, Cartesian=Cartesian,

489 datum=self.datum)

490 return NvectorBase.toCartesian(self, **kwds) # class or .classof

491

492 def toLatLon(self, **LatLon_and_kwds): # PYCHOK height=None, LatLon=LatLon

493 '''Convert this n-vector to an C{Nvector}-based geodetic point.

494

495 @kwarg LatLon_and_kwds: Optional L{LatLon}, B{C{height}}, B{C{datum}}

496 and other keyword arguments. Use C{B{LatLon}=...}

497 to override this L{LatLon} class or specify

498 C{B{LatLon} is None}.

499

500 @return: The geodetic point (L{LatLon}) or if C{B{LatLon} is None},

501 an L{Ecef9Tuple}C{(x, y, z, lat, lon, height, C, M, datum)}

502 with C{C} and C{M} if available.

503

504 @raise TypeError: Invalid B{C{LatLon_and_kwds}}.

505 '''

506 kwds = _xkwds(LatLon_and_kwds, height=self.h, datum=self.datum, LatLon=LatLon)

507 return NvectorBase.toLatLon(self, **kwds) # class or .classof

508

509 def unit(self, ll=None):

510 '''Normalize this vector to unit length.

511

512 @kwarg ll: Optional, original latlon (C{LatLon}).

513

514 @return: Normalised vector (C{Nvector}).

515 '''

516 u = NvectorBase.unit(self, ll=ll)

517 if u.datum != self.datum:

518 u._update(False, datum=self.datum)

519 return u

520

521

522 def _Ecef():

523 # return the Ecef class and overwrite property_RO

524 Cartesian.Ecef = LatLon.Ecef = E = _MODS.ecef.EcefVeness

525 return E

526

527

528 def meanOf(points, datum=_WGS84, height=None, wrap=False,

529 **LatLon_and_kwds):

530 '''Compute the geographic mean of several points.

531

532 @arg points: Points to be averaged (L{LatLon}[]).

533 @kwarg datum: Optional datum to use (L{Datum}).

534 @kwarg height: Optional height at mean point, overriding the mean

535 height (C{meter}).

536 @kwarg wrap: If C{True}, wrap or I{normalize} B{C{points}} (C{bool}).

537 @kwarg LatLon_and_kwds: Optional B{C{LatLon}} class to return the mean

538 points (or C{None}) and additional B{C{LatLon}} keyword

539 arguments, ignored if C{B{LatLon} is None}.

540

541 @return: Geographic mean point and height (B{C{LatLon}}) or if C{B{LatLon}

542 is None}, an L{Ecef9Tuple}C{(x, y, z, lat, lon, height, C, M,

543 datum)} with C{C} and C{M} if available.

544

545 @raise ValueError: Insufficient number of B{C{points}}.

546 '''

547 Ps = _Nv00.PointsIter(points, wrap=wrap)

548 n = sumOf(p._N_vector for p in Ps.iterate(closed=False))

549 return n.toLatLon(**_xkwds(LatLon_and_kwds, height=height, datum=datum,

550 LatLon=LatLon, name__=meanOf))

551

552

553 def nearestOn(point, point1, point2, within=True, height=None, wrap=False,

554 equidistant=None, tol=_TOL_M, LatLon=LatLon, **LatLon_kwds):

555 '''I{Iteratively} locate the closest point on the geodesic between

556 two other (ellipsoidal) points.

557

558 @arg point: Reference point (C{LatLon}).

559 @arg point1: Start point of the geodesic (C{LatLon}).

560 @arg point2: End point of the geodesic (C{LatLon}).

561 @kwarg within: If C{True}, return the closest point I{between}

562 B{C{point1}} and B{C{point2}}, otherwise the

563 closest point elsewhere on the geodesic (C{bool}).

564 @kwarg height: Optional height for the closest point (C{meter},

565 conventionally) or C{None} or C{False} for the

566 interpolated height. If C{False}, the closest

567 takes the heights of the points into account.

568 @kwarg wrap: If C{True}, wrap or I{normalize} and unroll I{only}

569 B{C{point1}} and B{C{point2}} (C{bool}).

570 @kwarg equidistant: An azimuthal equidistant projection (I{class}

571 or function L{pygeodesy.equidistant}) or C{None}

572 for the preferred C{B{point}.Equidistant}.

573 @kwarg tol: Convergence tolerance (C{meter}).

574 @kwarg LatLon: Optional class to return the closest point

575 (L{LatLon}) or C{None}.

576 @kwarg LatLon_kwds: Optional, additional B{C{LatLon}} keyword

577 arguments, ignored if C{B{LatLon} is None}.

578

579 @return: Closest point, a B{C{LatLon}} instance or if C{B{LatLon}

580 is None}, a L{LatLon4Tuple}C{(lat, lon, height, datum)}.

581

582 @raise ImportError: Package U{geographiclib

583 <https://PyPI.org/project/geographiclib>}

584 not installed or not found.

585

586 @raise TypeError: Invalid or non-ellipsoidal B{C{point}}, B{C{point1}}

587 or B{C{point2}} or invalid B{C{equidistant}}.

588

589 @raise ValueError: No convergence for the B{C{tol}}.

590

591 @see: U{The B{ellipsoidal} case<https://GIS.StackExchange.com/questions/48937/

592 calculating-intersection-of-two-circles>} and U{Karney's paper

593 <https://ArXiv.org/pdf/1102.1215.pdf>}, pp 20-21, section B{14. MARITIME

594 BOUNDARIES} for more details about the iteration algorithm.

595 '''

596 return _nearestOn(point, point1, point2, within=within, height=height, wrap=wrap,

597 equidistant=equidistant, tol=tol, LatLon=LatLon, **LatLon_kwds)

598

599

600 def sumOf(nvectors, Vector=Nvector, h=None, **Vector_kwds):

601 '''Return the vectorial sum of two or more n-vectors.

602

603 @arg nvectors: Vectors to be added (C{Nvector}[]).

604 @kwarg Vector: Optional class for the vectorial sum (C{Nvector}).

605 @kwarg h: Optional height, overriding the mean height (C{meter}).

606 @kwarg Vector_kwds: Optional, additional B{C{Vector}} keyword

607 arguments, ignored if C{B{Vector} is None}.

608

609 @return: Vectorial sum (B{C{Vector}}).

610

611 @raise VectorError: No B{C{nvectors}}.

612 '''

613 return _sumOf(nvectors, Vector=Vector, h=h, **Vector_kwds)

614

615

616@deprecated_function

617 def toNed(distance, bearing, elevation, Ned=Ned, **name):

618 '''DEPRECATED, use L{pygeodesy.Aer}C{(bearing, elevation,

619 distance).xyzLocal.toNed(B{Ned}, name=B{name})} or

620 L{XyzLocal}C{(pygeodesy.Aer(bearing, elevation,

621 distance)).toNed(B{Ned}, name=B{name})}.

622

623 Create an NED vector from distance, bearing and elevation

624 (in local coordinate system).

625

626 @arg distance: NED vector length (C{meter}).

627 @arg bearing: NED vector bearing (compass C{degrees360}).

628 @arg elevation: NED vector elevation from local coordinate

629 frame horizontal (C{degrees}).

630 @kwarg Ned: Optional class to return the NED (C{Ned}) or

631 C{None}.

632 @kwarg name: Optional C{B{name}=NN} (C{str}).

633

634 @return: An NED vector equivalent to this B{C{distance}},

635 B{C{bearing}} and B{C{elevation}} (DEPRECATED L{Ned})

636 or a DEPRECATED L{Ned3Tuple}C{(north, east, down)}

637 if C{B{Ned} is None}.

638

639 @raise ValueError: Invalid B{C{distance}}, B{C{bearing}}

640 or B{C{elevation}}.

641 '''

642 if True: # use new Aer class

643 n, e, d, _ = _Aer(bearing, elevation, distance).xyz4

644 else: # DEPRECATED

645 d = Distance(distance)

646

647 sb, cb, se, ce = sincos2d_(Bearing(bearing),

648 Height(elevation=elevation))

649 n = cb * d * ce

650 e = sb * d * ce

651 d *= se

652

653 r = _MODS.deprecated.classes.Ned3Tuple(n, e, -d) if Ned is None else \

654 Ned(n, e, -d)

655 return _xnamed(r, name)

656

657

658__all__ += _ALL_OTHER(Cartesian, LatLon, Ned, Nvector, # classes

659 meanOf, sumOf, toNed)

660

661# **) MIT License

662#

663# Copyright (C) 2016-2025 -- mrJean1 at Gmail -- All Rights Reserved.

664#

665# Permission is hereby granted, free of charge, to any person obtaining a

666# copy of this software and associated documentation files (the "Software"),

667# to deal in the Software without restriction, including without limitation

668# the rights to use, copy, modify, merge, publish, distribute, sublicense,

669# and/or sell copies of the Software, and to permit persons to whom the

670# Software is furnished to do so, subject to the following conditions:

671#

672# The above copyright notice and this permission notice shall be included

673# in all copies or substantial portions of the Software.

674#

675# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS

676# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,

677# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL

678# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR

679# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,

680# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR

681# OTHER DEALINGS IN THE SOFTWARE.