Coverage for pygeodesy/nvectorBase.py: 95%

256 statements  

« prev     ^ index     » next       coverage.py v7.6.1, created at 2025-05-29 12:40 -0400

1 

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

3 

4u'''(INTERNAL) Private elliposiodal and spherical C{Nvector} base classes 

5L{LatLonNvectorBase} and L{NvectorBase} and function L{sumOf}. 

6 

7Pure Python implementation of C{n-vector}-based geodesy tools for ellipsoidal 

8earth models, transcoded from JavaScript originals by I{(C) Chris Veness 2005-2016} 

9and published under the same MIT Licence**, see U{Vector-based geodesy 

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

11''' 

12 

13from pygeodesy.basics import _isin, map1 

14from pygeodesy.constants import EPS, EPS0, EPS1, EPS_2, R_M, \ 

15 _0_0, _1_0, _2_0, _N_2_0 

16# from pygeodesy.datums import _spherical_datum # from .formy 

17from pygeodesy.errors import IntersectionError, _ValueError, VectorError, \ 

18 _xattrs, _xkwds, _xkwds_pop2 

19from pygeodesy.fmath import fidw, hypot 

20from pygeodesy.fsums import Fsum, fsumf_ 

21from pygeodesy.formy import _isequalTo, _spherical_datum 

22# from pygeodesy.internals import _under # from .named 

23from pygeodesy.interns import NN, _1_, _2_, _3_, _bearing_, _coincident_, \ 

24 _COMMASPACE_, _distance_, _h_, _insufficient_, \ 

25 _intersection_, _no_, _point_, _pole_, _SPACE_ 

26from pygeodesy.latlonBase import LatLonBase, _ALL_DOCS, _ALL_LAZY, _MODS 

27# from pygeodesy.lazily import _ALL_DOCS, _ALL_LAZY, _ALL_MODS as _MODS # from .latlonBase 

28from pygeodesy.named import _xother3, _under 

29from pygeodesy.namedTuples import LatLon2Tuple, PhiLam2Tuple, Trilaterate5Tuple, \ 

30 Vector3Tuple, Vector4Tuple 

31from pygeodesy.props import deprecated_method, Property_RO, property_doc_, \ 

32 property_RO, property_ROnce, _update_all 

33from pygeodesy.streprs import Fmt, hstr, unstr 

34from pygeodesy.units import Bearing, Height, Radius_, Scalar 

35from pygeodesy.utily import atan2, sincos2d, _unrollon, _unrollon3 

36from pygeodesy.vector3d import Vector3d, _xyzhdlln4 

37 

38from math import degrees, fabs, sqrt 

39 

40__all__ = _ALL_LAZY.nvectorBase 

41__version__ = '25.05.12' 

42 

43 

44class NvectorBase(Vector3d): # XXX kept private 

45 '''(INTERNAL) Base class for ellipsoidal and spherical C{Nvector}s. 

46 ''' 

47 _datum = None # L{Datum}, overriden 

48 _h = Height(h=0) # height (C{meter}) 

49 _H = NN # height prefix (C{str}), '↑' in JS version 

50 

51 def __init__(self, x_xyz, y=None, z=None, h=0, datum=None, **ll_name): 

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

53 

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

55 L{Vector3d}, L{Vector3Tuple} or L{Vector4Tuple}). 

56 @kwarg y: Y component of vector (C{scalar}), required if B{C{x_xyz}} is 

57 C{scalar} and same units as B{C{x_xyz}}, ignored otherwise. 

58 @kwarg z: Z component of vector (C{scalar}), like B{C{y}}. 

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

60 @kwarg datum: Optional, I{pass-thru} datum (L{Datum}). 

61 @kwarg ll_name: Optional C{B{name}=NN} (C{str}) and optional, original 

62 latlon C{B{ll}=None} (C{LatLon}). 

63 

64 @raise TypeError: Non-scalar B{C{x}}, B{C{y}} or B{C{z}} coordinate or 

65 B{C{x_xyz}} not an C{Nvector}, L{Vector3Tuple} or 

66 L{Vector4Tuple} or invalid B{C{datum}}. 

67 ''' 

68 h, d, ll, n = _xyzhdlln4(x_xyz, h, datum, **ll_name) 

69 Vector3d.__init__(self, x_xyz, y=y, z=z, ll=ll, name=n) 

70 if h: 

71 self.h = h 

72 if d is not None: 

73 self._datum = _spherical_datum(d, name=n) # pass-thru 

74 

75 @Property_RO 

76 def datum(self): 

77 '''Get the I{pass-thru} datum (C{Datum}) or C{None}. 

78 ''' 

79 return self._datum 

80 

81 @property_ROnce 

82 def Ecef(self): 

83 '''Get the ECEF I{class} (L{EcefKarney}), I{once}. 

84 ''' 

85 return _MODS.ecef.EcefKarney 

86 

87 @property_RO 

88 def ellipsoidalNvector(self): 

89 '''Get the C{Nvector type} iff ellipsoidal, overloaded in L{pygeodesy.ellipsoidalNvector.Nvector}. 

90 ''' 

91 return False 

92 

93 @property_doc_(''' the height above surface (C{meter}).''') 

94 def h(self): 

95 '''Get the height above surface (C{meter}). 

96 ''' 

97 return self._h 

98 

99 @h.setter # PYCHOK setter! 

100 def h(self, h): 

101 '''Set the height above surface (C{meter}). 

102 

103 @raise TypeError: If B{C{h}} invalid. 

104 

105 @raise VectorError: If B{C{h}} invalid. 

106 ''' 

107 h = Height(h=h, Error=VectorError) 

108 if self._h != h: 

109 _update_all(self) 

110 self._h = h 

111 

112 @property_doc_(''' the height prefix (C{str}).''') 

113 def H(self): 

114 '''Get the height prefix (C{str}). 

115 ''' 

116 return self._H 

117 

118 @H.setter # PYCHOK setter! 

119 def H(self, H): 

120 '''Set the height prefix (C{str}). 

121 ''' 

122 self._H = str(H) if H else NN 

123 

124 def hStr(self, prec=-2, m=NN): 

125 '''Return a string for the height B{C{h}}. 

126 

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

128 @kwarg m: Optional unit of the height (C{str}). 

129 

130 @see: Function L{pygeodesy.hstr}. 

131 ''' 

132 return NN(self.H, hstr(self.h, prec=prec, m=m)) 

133 

134 @Property_RO 

135 def isEllipsoidal(self): 

136 '''Check whether this n-vector is ellipsoidal (C{bool} or C{None} if unknown). 

137 ''' 

138 return self.datum.isEllipsoidal if self.datum else None 

139 

140 @Property_RO 

141 def isSpherical(self): 

142 '''Check whether this n-vector is spherical (C{bool} or C{None} if unknown). 

143 ''' 

144 return self.datum.isSpherical if self.datum else None 

145 

146 @Property_RO 

147 def lam(self): 

148 '''Get the (geodetic) longitude in C{radians} (C{float}). 

149 ''' 

150 return self.philam.lam 

151 

152 @Property_RO 

153 def lat(self): 

154 '''Get the (geodetic) latitude in C{degrees} (C{float}). 

155 ''' 

156 return self.latlon.lat 

157 

158 @Property_RO 

159 def latlon(self): 

160 '''Get the (geodetic) lat-, longitude in C{degrees} (L{LatLon2Tuple}C{(lat, lon)}). 

161 ''' 

162 return n_xyz2latlon(self, name=self.name) 

163 

164 @Property_RO 

165 def latlonheight(self): 

166 '''Get the (geodetic) lat-, longitude in C{degrees} and height (L{LatLon3Tuple}C{(lat, lon, height)}). 

167 ''' 

168 return self.latlon.to3Tuple(self.h) 

169 

170 @Property_RO 

171 def latlonheightdatum(self): 

172 '''Get the lat-, longitude in C{degrees} with height and datum (L{LatLon4Tuple}C{(lat, lon, height, datum)}). 

173 ''' 

174 return self.latlonheight.to4Tuple(self.datum) 

175 

176 @Property_RO 

177 def lon(self): 

178 '''Get the (geodetic) longitude in C{degrees} (C{float}). 

179 ''' 

180 return self.latlon.lon 

181 

182 @Property_RO 

183 def phi(self): 

184 '''Get the (geodetic) latitude in C{radians} (C{float}). 

185 ''' 

186 return self.philam.phi 

187 

188 @Property_RO 

189 def philam(self): 

190 '''Get the (geodetic) lat-, longitude in C{radians} (L{PhiLam2Tuple}C{(phi, lam)}). 

191 ''' 

192 return n_xyz2philam(self, name=self.name) 

193 

194 @Property_RO 

195 def philamheight(self): 

196 '''Get the (geodetic) lat-, longitude in C{radians} and height (L{PhiLam3Tuple}C{(phi, lam, height)}). 

197 ''' 

198 return self.philam.to3Tuple(self.h) 

199 

200 @Property_RO 

201 def philamheightdatum(self): 

202 '''Get the lat-, longitude in C{radians} with height and datum (L{PhiLam4Tuple}C{(phi, lam, height, datum)}). 

203 ''' 

204 return self.philamheight.to4Tuple(self.datum) 

205 

206 @property_RO 

207 def sphericalNvector(self): 

208 '''Get the C{Nvector type} iff spherical, overloaded in L{pygeodesy.sphericalNvector.Nvector}. 

209 ''' 

210 return False 

211 

212 @deprecated_method 

213 def to2ab(self): # PYCHOK no cover 

214 '''DEPRECATED, use property L{philam}.''' 

215 return self.philam 

216 

217 @deprecated_method 

218 def to3abh(self, height=None): # PYCHOK no cover 

219 '''DEPRECATED, use property L{philamheight} or C{philam.to3Tuple(B{height})}.''' 

220 return self.philamheight if _isin(height, None, self.h) else \ 

221 self.philam.to3Tuple(height) 

222 

223 def toCartesian(self, h=None, Cartesian=None, datum=None, **name_Cartesian_kwds): # PYCHOK signature 

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

225 

226 @kwarg h: Optional height, overriding this n-vector's height (C{meter}). 

227 @kwarg Cartesian: Optional class to return the (ECEF) coordinates (C{Cartesian}). 

228 @kwarg datum: Optional datum (C{Datum}), overriding this datum. 

229 @kwarg name_Cartesian_kwds: Optional C{B{name}=NN} (C{str}) and optionally, additional 

230 B{C{Cartesian}} keyword arguments, ignored if C{B{Cartesian} is None}. 

231 

232 @return: The (ECEF) coordinates (B{C{Cartesian}}) or if C{B{Cartesian} is None}, an 

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

234 if available. 

235 

236 @raise TypeError: Invalid B{C{Cartesian}} or B{C{name_Cartesian_kwds}} argument. 

237 

238 @raise ValueError: Invalid B{C{h}}. 

239 ''' 

240 if h is None: 

241 h = self.h 

242 elif not isinstance(h, Height): 

243 h = Height(h=h, Error=VectorError) 

244 _, r, v = self._toEcefDrv3(Cartesian, None, datum, h, **name_Cartesian_kwds) 

245 if r is None: 

246 r = v.toCartesian(Cartesian, **self._name1__(name_Cartesian_kwds)) # h=0 

247 return r 

248 

249 def _toEcefDrv3(self, CC, LL, datum, h, name=NN, **unused): 

250 '''(INTERNAL) Helper for methods C{toCartesian} and C{toLatLon}. 

251 ''' 

252 D = self.datum if _isin(datum, None, self.datum) else \ 

253 _spherical_datum(datum, name=self.name) 

254 if LL is None: 

255 v = Vector3d(self, name=name or self.name) # .toVector3d(norm=False) 

256 E = D.ellipsoid 

257 r = E.a_b # Kenneth Gade eqn 22 

258 n = v.times_(r, r, _1_0).length 

259 n = (E.b / n) if n > EPS0 else _0_0 

260 r = E.a2_b2 * n + h # fma 

261 v = v.times_(r, r, n + h) 

262 r = self.Ecef(D).reverse(v, M=True) if CC is None else None 

263 else: 

264 r = v = None 

265 return D, r, v 

266 

267 @deprecated_method 

268 def to2ll(self): # PYCHOK no cover 

269 '''DEPRECATED, use property L{latlon}.''' 

270 return self.latlon 

271 

272 @deprecated_method 

273 def to3llh(self, height=None): # PYCHOK no cover 

274 '''DEPRECATED, use property C{latlonheight} or C{latlon.to3Tuple(B{height})}.''' 

275 return self.latlonheight if _isin(height, None, self.h) else \ 

276 self.latlon.to3Tuple(height) 

277 

278 def toLatLon(self, height=None, LatLon=None, datum=None, **name_LatLon_kwds): 

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

280 

281 @kwarg height: Optional height, overriding this n-vector's 

282 height (C{meter}). 

283 @kwarg LatLon: Optional class to return the geodetic point 

284 (C{LatLon}) or C{None}. 

285 @kwarg datum: Optional, spherical datum (C{Datum}). 

286 @kwarg name_LatLon_kwds: Optional C{B{name}=NN} (C{str}) and optionally, 

287 additional B{C{LatLon}} keyword arguments, ignored if 

288 C{B{LatLon} is None}. 

289 

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

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

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

293 

294 @raise TypeError: Invalid B{C{LatLon}} or B{C{name_LatLon_kwds}} argument. 

295 

296 @raise ValueError: Invalid B{C{height}}. 

297 ''' 

298 h = self.h if height is None else ( 

299 height if isinstance(height, Height) else 

300 Height(height, Error=VectorError)) 

301 # use the .toCartesian() logic for better height accuracy instead of 

302 # r = self.Ecef(D).forward(self.lat, self.lon, height=h, M=True) 

303 D, r, _ = self._toEcefDrv3(None, LatLon, datum, h, **name_LatLon_kwds) 

304 if r is None: 

305 kwds = _xkwds(name_LatLon_kwds, height=h, datum=D) 

306 r = LatLon(self.lat, self.lon, **self._name1__(kwds)) 

307 return r 

308 

309 def toStr(self, prec=5, fmt=Fmt.PAREN, sep=_COMMASPACE_): # PYCHOK expected 

310 '''Return a string representation of this n-vector. 

311 

312 Height component is only included if non-zero. 

313 

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

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

316 @kwarg sep: Optional separator between components (C{str}). 

317 

318 @return: Comma-separated C{"(x, y, z [, h])"} enclosed in 

319 B{C{fmt}} brackets (C{str}). 

320 ''' 

321 t = Vector3d.toStr(self, prec=prec, fmt=NN, sep=sep) 

322 if self.h: 

323 t = sep.join((t, self.hStr())) 

324 return (fmt % (t,)) if fmt else t 

325 

326 def toVector3d(self, norm=True): 

327 '''Convert this n-vector to a 3-D vector, I{ignoring height}. 

328 

329 @kwarg norm: If C{True}, normalize the 3-D vector (C{bool}). 

330 

331 @return: The (normalized) vector (L{Vector3d}). 

332 ''' 

333 v = Vector3d.unit(self) if norm else self 

334 return Vector3d(v, name=self.name) 

335 

336 @deprecated_method 

337 def to4xyzh(self, h=None): # PYCHOK no cover 

338 '''DEPRECATED, use property L{xyzh} or C{xyz.to4Tuple(B{h})}.''' 

339 return self.xyzh if _isin(h, None, self.h) else Vector4Tuple( 

340 self.x, self.y, self.z, h, name=self.name) 

341 

342 def unit(self, ll=None): 

343 '''Normalize this n-vector to unit length. 

344 

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

346 

347 @return: Normalized vector (C{Nvector}). 

348 ''' 

349 return _xattrs(Vector3d.unit(self, ll=ll), self, _under(_h_)) 

350 

351 @Property_RO 

352 def xyzh(self): 

353 '''Get this n-vector's components (L{Vector4Tuple}C{(x, y, z, h)}) 

354 ''' 

355 return self.xyz.to4Tuple(self.h) 

356 

357 

358class _N_vector_(NvectorBase): 

359 '''(INTERNAL) Minimal, low-overhead C{n-vector}. 

360 ''' 

361 def __init__(self, x, y, z, h=0, **name): 

362 self._x, self._y, self._z = x, y, z 

363 if h: 

364 self._h = h 

365 if name: 

366 self.name = name 

367 

368 

369NorthPole = _N_vector_(0, 0, +1, name='NorthPole') # North pole 

370SouthPole = _N_vector_(0, 0, -1, name='SouthPole') # South pole 

371 

372 

373class LatLonNvectorBase(LatLonBase): 

374 '''(INTERNAL) Base class for n-vector-based ellipsoidal and spherical C{LatLon}s. 

375 ''' 

376 

377 def _update(self, updated, *attrs, **setters): # PYCHOK _Nv=None 

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

379 

380 @see: C{ellipsoidalNvector.LatLon} and C{sphericalNvector.LatLon} for 

381 the special case of B{C{_Nv}}. 

382 ''' 

383 if updated: 

384 _Nv, setters = _xkwds_pop2(setters, _Nv=None) 

385 if _Nv is not None: 

386 if _Nv._fromll is not None: 

387 _Nv._fromll = None 

388 self._Nv = None 

389 LatLonBase._update(self, updated, *attrs, **setters) 

390 

391# def distanceTo(self, other, **kwds): # PYCHOK no cover 

392# '''I{Must be overloaded}.''' 

393# self._notOverloaded(other, **kwds) 

394 

395 def intersections2(self, radius1, other, radius2, **kwds): # PYCHOK expected 

396 '''B{Not implemented}, throws a C{NotImplementedError} always.''' 

397 self._notImplemented(radius1, other, radius2, **kwds) 

398 

399 def others(self, *other, **name_other_up): 

400 '''Refined class comparison. 

401 

402 @arg other: The other instance (C{LatLonNvectorBase}). 

403 @kwarg name_other_up: Overriding C{name=other} and C{up=1} 

404 keyword arguments. 

405 

406 @return: The B{C{other}} if compatible. 

407 

408 @raise TypeError: Incompatible B{C{other}} C{type}. 

409 ''' 

410 if other: 

411 other0 = other[0] 

412 if isinstance(other0, (self.__class__, LatLonNvectorBase)): # XXX NvectorBase? 

413 return other0 

414 

415 other, name, up = _xother3(self, other, **name_other_up) 

416 if not isinstance(other, (self.__class__, LatLonNvectorBase)): # XXX NvectorBase? 

417 LatLonBase.others(self, other, name=name, up=up + 1) 

418 return other 

419 

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

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

422 

423 @kwarg Nvector_and_kwds: Optional C{Nvector} class and C{Nvector} keyword arguments, 

424 Specify C{B{Nvector}=...} to override this C{Nvector} class 

425 or use C{B{Nvector}=None}. 

426 

427 @return: An C{Nvector} or if C{Nvector is None}, a L{Vector4Tuple}C{(x, y, z, h)}. 

428 

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

430 ''' 

431 return LatLonBase.toNvector(self, **_xkwds(Nvector_and_kwds, Nvector=NvectorBase)) 

432 

433 def triangulate(self, bearing1, other, bearing2, height=None, wrap=False): # PYCHOK signature 

434 '''Locate a point given this, an other point and the (initial) bearing 

435 from this and the other point. 

436 

437 @arg bearing1: Bearing at this point (compass C{degrees360}). 

438 @arg other: The other point (C{LatLon}). 

439 @arg bearing2: Bearing at the other point (compass C{degrees360}). 

440 @kwarg height: Optional height at the triangulated point, overriding 

441 the mean height (C{meter}). 

442 @kwarg wrap: If C{True}, use this and the B{C{other}} point 

443 I{normalized} (C{bool}). 

444 

445 @return: Triangulated point (C{LatLon}). 

446 

447 @raise TypeError: Invalid B{C{other}} point. 

448 

449 @raise Valuerror: Points coincide. 

450 ''' 

451 return _triangulate(self, bearing1, self.others(other), bearing2, 

452 height=height, wrap=wrap, LatLon=self.classof) 

453 

454 def trilaterate(self, distance1, point2, distance2, point3, distance3, 

455 radius=R_M, height=None, useZ=False, wrap=False): 

456 '''Locate a point at given distances from this and two other points. 

457 

458 @arg distance1: Distance to this point (C{meter}, same units 

459 as B{C{radius}}). 

460 @arg point2: Second reference point (C{LatLon}). 

461 @arg distance2: Distance to point2 (C{meter}, same units as 

462 B{C{radius}}). 

463 @arg point3: Third reference point (C{LatLon}). 

464 @arg distance3: Distance to point3 (C{meter}, same units as 

465 B{C{radius}}). 

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

467 @kwarg height: Optional height at trilaterated point, overriding 

468 the mean height (C{meter}, same units as B{C{radius}}). 

469 @kwarg useZ: Include Z component iff non-NaN, non-zero (C{bool}). 

470 @kwarg wrap: If C{True}, use this, B{C{point2}} and B{C{point3}} 

471 I{normalized} (C{bool}). 

472 

473 @return: Trilaterated point (C{LatLon}). 

474 

475 @raise IntersectionError: No intersection, trilateration failed. 

476 

477 @raise TypeError: Invalid B{C{point2}} or B{C{point3}}. 

478 

479 @raise ValueError: Some B{C{points}} coincide or invalid B{C{distance1}}, 

480 B{C{distance2}}, B{C{distance3}} or B{C{radius}}. 

481 

482 @see: U{Trilateration<https://WikiPedia.org/wiki/Trilateration>}, I{Veness}' 

483 JavaScript U{Trilateration<https://www.Movable-Type.co.UK/scripts/ 

484 latlong-vectors.html>} and method C{LatLon.trilaterate5} of other, 

485 non-C{Nvector LatLon} classes. 

486 ''' 

487 return _trilaterate(self, distance1, self.others(point2=point2), distance2, 

488 self.others(point3=point3), distance3, 

489 radius=radius, height=height, useZ=useZ, 

490 wrap=wrap, LatLon=self.classof) 

491 

492 def trilaterate5(self, distance1, point2, distance2, point3, distance3, # PYCHOK signature 

493 area=False, eps=EPS1, radius=R_M, wrap=False): 

494 '''B{Not implemented} for C{B{area}=True} and falls back to method C{trilaterate}. 

495 

496 @return: A L{Trilaterate5Tuple}C{(min, minPoint, max, maxPoint, n)} with a 

497 single trilaterated intersection C{minPoint I{is} maxPoint}, C{min 

498 I{is} max} the nearest intersection margin and count C{n = 1}. 

499 

500 @raise NotImplementedError: Keyword argument C{B{area}=True} not (yet) supported. 

501 

502 @see: Method L{trilaterate} for other and more details. 

503 ''' 

504 if area: 

505 self._notImplemented(area=area) 

506 

507 t = _trilaterate(self, distance1, self.others(point2=point2), distance2, 

508 self.others(point3=point3), distance3, 

509 radius=radius, useZ=True, wrap=wrap, 

510 LatLon=self.classof) 

511 # ... and handle B{C{eps}} and C{IntersectionError} 

512 # like function C{.latlonBase._trilaterate5} 

513 d = self.distanceTo(t, radius=radius, wrap=wrap) # PYCHOK distanceTo 

514 d = min(fabs(distance1 - d), fabs(distance2 - d), fabs(distance3 - d)) 

515 if d < eps: # min is max, minPoint is maxPoint 

516 return Trilaterate5Tuple(d, t, d, t, 1) # n = 1 

517 t = _SPACE_(_no_(_intersection_), Fmt.PAREN(min.__name__, Fmt.f(d, prec=3))) 

518 raise IntersectionError(area=area, eps=eps, radius=radius, wrap=wrap, txt=t) 

519 

520 

521def n_xyz2latlon(x_xyz, y=0, z=0, **name): 

522 '''Convert C{n-vector} to (geodetic) lat- and longitude in C{degrees}. 

523 

524 @arg x_xyz: X component (C{scalar}) or (3-D) vector (C{Nvector}, 

525 L{Vector3d}, L{Vector3Tuple} or L{Vector4Tuple}). 

526 @kwarg y: Y component of vector (C{scalar}), required if C{B{x_xyz} is 

527 scalar} and same units as B{C{x_xyz}}, ignored otherwise. 

528 @kwarg z: Z component of vector (C{scalar}), like B{C{y}}. 

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

530 

531 @return: A L{LatLon2Tuple}C{(lat, lon)}. 

532 

533 @see: Function L{n_xyz2philam}. 

534 ''' 

535 ll = map(degrees, n_xyz2philam(x_xyz, y, z)) 

536 return LatLon2Tuple(*ll, **name) 

537 

538 

539def n_xyz2philam(x_xyz, y=0, z=0, **name): 

540 '''Convert C{n-vector} to (geodetic) lat- and longitude in C{radians}. 

541 

542 @arg x_xyz: X component (C{scalar}) or (3-D) vector (C{Nvector}, 

543 L{Vector3d}, L{Vector3Tuple} or L{Vector4Tuple}). 

544 @kwarg y: Y component of vector (C{scalar}), required if C{B{x_xyz} is 

545 scalar} and same units as B{C{x_xyz}}, ignored otherwise. 

546 @kwarg z: Z component of vector (C{scalar}), like B{C{y}}. 

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

548 

549 @return: A L{PhiLam2Tuple}C{(phi, lam)}. 

550 

551 @see: Function L{n_xyz2latlon}. 

552 ''' 

553 try: 

554 x, y, z = x_xyz.xyz 

555 except AttributeError: 

556 x = x_xyz 

557 return PhiLam2Tuple(atan2(z, hypot(x, y)), atan2(y, x), **name) 

558 

559 

560def _nsumOf(nvs, h_None, Vector, Vector_kwds): # .sphericalNvector, .vector3d 

561 '''(INTERNAL) Separated to allow callers to embellish exceptions. 

562 ''' 

563 X, Y, Z, n = Fsum(), Fsum(), Fsum(), 0 

564 H = Fsum() if h_None is None else n 

565 for n, v in enumerate(nvs or ()): # one pass 

566 X += v.x 

567 Y += v.y 

568 Z += v.z 

569 H += v.h 

570 if n < 1: 

571 raise ValueError(_SPACE_(Fmt.PARENSPACED(len=n), _insufficient_)) 

572 

573 x, y, z = map1(float, X, Y, Z) 

574 h = H.fover(n) if h_None is None else h_None 

575 return Vector3Tuple(x, y, z).to4Tuple(h) if Vector is None else \ 

576 Vector(x, y, z, **_xkwds(Vector_kwds, h=h)) 

577 

578 

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

580 '''Return the I{vectorial} sum of two or more n-vectors. 

581 

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

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

584 or C{None}. 

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

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

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

588 

589 @return: Vectorial sum (B{C{Vector}}) or a L{Vector4Tuple}C{(x, y, 

590 z, h)} if C{B{Vector} is None}. 

591 

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

593 ''' 

594 try: 

595 return _nsumOf(nvectors, h, Vector, Vector_kwds) 

596 except (TypeError, ValueError) as x: 

597 raise VectorError(nvectors=nvectors, Vector=Vector, cause=x) 

598 

599 

600def _triangulate(point1, bearing1, point2, bearing2, height=None, 

601 wrap=False, **LatLon_and_kwds): 

602 # (INTERNAL) Locate a point given two known points and initial 

603 # bearings from those points, see C{LatLon.triangulate} above 

604 

605 def _gc(p, b, _i_): 

606 n = p.toNvector() 

607 de = NorthPole.cross(n, raiser=_pole_).unit() # east vector @ n 

608 dn = n.cross(de) # north vector @ n 

609 s, c = sincos2d(Bearing(b, name=_bearing_ + _i_)) 

610 dest = de.times(s) 

611 dnct = dn.times(c) 

612 d = dnct.plus(dest) # direction vector @ n 

613 return n.cross(d) # great circle point + bearing 

614 

615 if wrap: 

616 point2 = _unrollon(point1, point2, wrap=wrap) 

617 if _isequalTo(point1, point2, eps=EPS): 

618 raise _ValueError(points=point2, wrap=wrap, txt=_coincident_) 

619 

620 gc1 = _gc(point1, bearing1, _1_) # great circle p1 + b1 

621 gc2 = _gc(point2, bearing2, _2_) # great circle p2 + b2 

622 

623 n = gc1.cross(gc2, raiser=_point_) # n-vector of intersection point 

624 h = point1._havg(point2, h=height) 

625 kwds = _xkwds(LatLon_and_kwds, height=h) 

626 return n.toLatLon(**kwds) # Nvector(n.x, n.y, n.z).toLatLon(...) 

627 

628 

629def _trilaterate(point1, distance1, point2, distance2, point3, distance3, 

630 radius=R_M, height=None, useZ=False, 

631 wrap=False, **LatLon_and_kwds): 

632 # (INTERNAL) Locate a point at given distances from 

633 # three other points, see LatLon.triangulate above 

634 

635 def _nr2(p, d, r, _i_, *qs): # .toNvector and angular distance squared 

636 for q in qs: 

637 if _isequalTo(p, q, eps=EPS): 

638 raise _ValueError(points=p, txt=_coincident_) 

639 return p.toNvector(), (Scalar(d, name=_distance_ + _i_) / r)**2 

640 

641 p1, r = point1, Radius_(radius) 

642 p2, p3, _ = _unrollon3(p1, point2, point3, wrap) 

643 

644 n1, r12 = _nr2(p1, distance1, r, _1_) 

645 n2, r22 = _nr2(p2, distance2, r, _2_, p1) 

646 n3, r32 = _nr2(p3, distance3, r, _3_, p1, p2) 

647 

648 # the following uses x,y coordinate system with origin at n1, x axis n1->n2 

649 y = n3.minus(n1) 

650 x = n2.minus(n1) 

651 z = None 

652 

653 d = x.length # distance n1->n2 

654 if d > EPS_2: # and y.length > EPS_2: 

655 X = x.unit() # unit vector in x direction n1->n2 

656 i = X.dot(y) # signed magnitude of x component of n1->n3 

657 Y = y.minus(X.times(i)).unit() # unit vector in y direction 

658 j = Y.dot(y) # signed magnitude of y component of n1->n3 

659 if fabs(j) > EPS_2: 

660 # courtesy of U{Carlos Freitas<https://GitHub.com/mrJean1/PyGeodesy/issues/33>} 

661 x = fsumf_(r12, -r22, d**2) / (d * _2_0) # n1->intersection x- and ... 

662 y = fsumf_(r12, -r32, i**2, j**2, x * i * _N_2_0) / (j * _2_0) # ... y-component 

663 # courtesy of U{AleixDev<https://GitHub.com/mrJean1/PyGeodesy/issues/43>} 

664 z = fsumf_(max(r12, r22, r32), -(x**2), -(y**2)) # XXX not just r12! 

665 if z > EPS: 

666 n = n1.plus(X.times(x)).plus(Y.times(y)) 

667 if useZ: # include Z component 

668 Z = X.cross(Y) # unit vector perpendicular to plane 

669 n = n.plus(Z.times(sqrt(z))) 

670 if height is None: 

671 h = fidw((point1.height, point2.height, point3.height), 

672 map1(fabs, distance1, distance2, distance3)) 

673 else: 

674 h = Height(height) 

675 kwds = _xkwds(LatLon_and_kwds, height=h) 

676 return n.toLatLon(**kwds) # Nvector(n.x, n.y, n.z).toLatLon(...) 

677 

678 # no intersection, d < EPS_2 or fabs(j) < EPS_2 or z < EPS 

679 t = _SPACE_(_no_, _intersection_, NN) 

680 raise IntersectionError(point1=point1, distance1=distance1, 

681 point2=point2, distance2=distance2, 

682 point3=point3, distance3=distance3, 

683 txt=unstr(t, z=z, useZ=useZ, wrap=wrap)) 

684 

685 

686__all__ += _ALL_DOCS(LatLonNvectorBase, NvectorBase, sumOf) # classes 

687 

688# **) MIT License 

689# 

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

691# 

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

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

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

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

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

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

698# 

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

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

701# 

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

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

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

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

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

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

708# OTHER DEALINGS IN THE SOFTWARE.