Coverage for pygeodesy/ecef.py: 95%

622 statements  

« prev     ^ index     » next       coverage.py v7.17.0, created at 2026-11-10 16:16 -0500

1 

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

3 

4u'''I{Geocentric} Earth-Centered, Earth-Fixed (ECEF) coordinates. 

5 

6Geocentric conversions transcoded from I{Charles Karney}'s C++ class U{Geocentric 

7<https://GeographicLib.SourceForge.io/C++/doc/classGeographicLib_1_1Geocentric.html>} into pure 

8Python class L{EcefKarney}, class L{EcefFukushima} from I{Toshio Fukushima}'s U{Fortran 

9<https://www.ResearchGate.net/publication/277721539>} version, class L{EcefOlson} from I{D.K. Olson}'s 

10U{paper<https://IEEEXplore.IEEE.org/document/481290>}, class L{EcefOsen} with I{Karl Osen}'s U{improvements 

11<https://HAL.Science/hal-01704943v2>} of I{Jijie Zhu}'s U{algorithm <https://IEEEXplore.IEEE.org/document/303772>}, 

12class L{EcefSudano} based on I{John Sudano}'s U{paper<https://www.ResearchGate.net/publication/3709199>}, 

13class L{EcefUPC} using the I{Universitat Politècnica de Catalunya}'s U{method, page 186 

14<https://GSSC.ESA.int/navipedia/GNSS_Book/ESA_GNSS-Book_TM-23_Vol_I.pdf>}, class L{EcefVeness} transcoded 

15from I{Chris Veness}' JavaScript classes U{LatLonEllipsoidal, Cartesian 

16<https://www.Movable-Type.co.UK/scripts/geodesy/docs/latlon-ellipsoidal.js.html>}, class L{EcefYou} 

17implementing I{Rey-Jer You}'s U{transformations<https://www.ResearchGate.net/publication/240359424>} 

18and classes L{EcefFarrell22} and L{EcefFarrell22} from I{Jay A. Farrell}'s U{Table 2.1 and 2.2 

19<https://Books.Google.com/books?id=fW4foWASY6wC>}, page 29-30. 

20 

21Following is a copy of I{Karney}'s U{Detailed Description 

22<https://GeographicLib.SourceForge.io/C++/doc/classGeographicLib_1_1Geocentric.html>}. 

23 

24Convert between geodetic coordinates C{lat}-, C{lon}gitude and height C{h} (measured vertically 

25from the surface of the ellipsoid) to geocentric C{x}, C{y} and C{z} coordinates, also known as 

26I{Earth-Centered, Earth-Fixed} (U{ECEF<https://WikiPedia.org/wiki/ECEF>}). 

27 

28The origin of geocentric coordinates is at the center of the earth. The C{z} axis goes thru 

29the North pole, C{lat} = 90°. The C{x} axis goes thru C{lat} = 0°, C{lon} = 0°. 

30 

31The I{local (cartesian) origin} is at (C{lat0}, C{lon0}, C{height0}). The I{local} C{x} axis points 

32East, the I{local} C{y} axis points North and the I{local} C{z} axis is normal to the ellipsoid. The 

33plane C{z = -height0} is tangent to the ellipsoid, hence the alternate name I{local tangent plane}. 

34 

35Forward conversion from geodetic to geocentric (ECEF) coordinates is straightforward. 

36 

37For the reverse transformation we use Hugues Vermeille's U{Direct transformation from geocentric 

38coordinates to geodetic coordinates<https://DOI.org/10.1007/s00190-002-0273-6>}, J. Geodesy 

39(2002) 76, page 451-454. 

40 

41Several changes have been made to ensure that the method returns accurate results for all finite 

42inputs (even if h is infinite). The changes are described in Appendix B of C. F. F. Karney 

43U{Geodesics on an ellipsoid of revolution<https://ArXiv.org/abs/1102.1215v1>}, Feb. 2011, 85, 

44105-117 (U{preprint<https://ArXiv.org/abs/1102.1215v1>}). Vermeille similarly updated his method 

45in U{An analytical method to transform geocentric into geodetic coordinates 

46<https://DOI.org/10.1007/s00190-010-0419-x>}, J. Geodesy (2011) 85, page 105-117. See U{Geocentric 

47coordinates<https://GeographicLib.SourceForge.io/C++/doc/geocentric.html>} for more information. 

48 

49The errors in these routines are close to round-off. Specifically, for points within 5,000 Km of 

50the surface of the ellipsoid (either inside or outside the ellipsoid), the error is bounded by 7 

51nm (7 nanometers) for the WGS84 ellipsoid. See U{Geocentric coordinates 

52<https://GeographicLib.SourceForge.io/C++/doc/geocentric.html>} for further information on the errors. 

53 

54@note: The C{reverse} methods of all C{Ecef...} classes return by default C{INT0} as the (geodetic) 

55longitude for I{polar} ECEF location C{x == y == 0}. Use keyword argument C{lon00} or property 

56C{lon00} to configure that value. 

57 

58@see: Module L{ltp} and class L{LocalCartesian}, a transcription of I{Charles Karney}'s C++ class 

59U{LocalCartesian<https://GeographicLib.SourceForge.io/C++/doc/classGeographicLib_1_1LocalCartesian.html>}, 

60for conversion between geodetic and I{local cartesian} coordinates in a I{local tangent 

61plane} as opposed to I{geocentric} (ECEF) ones. 

62''' 

63 

64from pygeodesy.basics import copysign0, _isin, isscalar, issubclassof, neg, map1, \ 

65 _xinstanceof, _xsubclassof, typename # _args_kwds_names 

66from pygeodesy.constants import EPS, EPS0, EPS02, EPS1, INT0, PI, PI_2, _0_0, _0_25, \ 

67 _0_5, _1_0, _1_0_1T, _1_5, _2_0, _3_0, _4_0, _6_0, \ 

68 _copysign_1_0, _isNAN0, _over, isnon0 # PYCHOK used! 

69from pygeodesy.datums import _ellipsoidal_datum, _WGS84, a_f2Tuple, _EWGS84 

70from pygeodesy.ecefLocals import _EcefLocal 

71# from pygeodesy.ellipsoids import a_f2Tuple, _EWGS84 # from .datums 

72from pygeodesy.errors import _IndexError, LenError, _ValueError, _TypesError, \ 

73 _xattr, _xdatum, _xkwds 

74from pygeodesy.fmath import cbrt, _fdotf, hypot, hypot1, hypot2_ 

75from pygeodesy.fsums import Fsum, fsumf_, Fmt, unstr 

76# from pygeodesy.internals import typename # from .basics 

77from pygeodesy.interns import NN, _a_, _C_, _datum_, _ellipsoid_, _f_, _height_, \ 

78 _lat_, _lon_, _M_, _name_, _singular_, _SPACE_, \ 

79 _x_, _xyz_, _y_, _z_ 

80from pygeodesy.lazily import _ALL_DOCS, _ALL_LAZY, _ALL_MODS as _MODS 

81from pygeodesy.named import _name__, _name1__, _NamedBase, _NamedTuple, _Pass, _xnamed 

82from pygeodesy.namedTuples import LatLon2Tuple, LatLon3Tuple, \ 

83 PhiLam2Tuple, Vector3Tuple, Vector4Tuple 

84from pygeodesy.props import deprecated_method, deprecated_property, Property_RO, \ 

85 property_RO, property_ROver 

86# from pygeodesy.streprs import Fmt, unstr # from .fsums 

87from pygeodesy.units import _isRadius, Degrees_, Height, Int, Lam, Lat, Lon, \ 

88 Meter, Phi, Scalar, Scalar_ 

89from pygeodesy.utily import atan1, atan1d, atan2, atan2d, degrees90, degrees180, \ 

90 sincos2, sincos2_, sincos2d_ 

91# from pygeodesy.vector3d import Vector3d # _MODS 

92 

93from math import acos, asin, cos, degrees, fabs, radians, sqrt 

94 

95__all__ = _ALL_LAZY.ecef 

96__version__ = '26.09.28' 

97 

98_Ecef_ = 'Ecef' 

99_prolate_ = 'prolate' 

100_TOL = 1.e-12 # degrees > 1.e-14 

101_TRIPS = 33 # 8..9 sufficient 

102_xyz_y_z = _xyz_, _y_, _z_ # _args_kwds_names(_xyzn4)[:3] 

103 

104 

105def _Degrees2Radians(tol): # for EcefUPC 

106 return Degrees_(tol=tol, low=EPS, Error=EcefError).toRadians() 

107 

108 

109class _EcefBase(_NamedBase): 

110 '''(INTERNAL) Base class for C{Ecef*} convertor classes. 

111 ''' 

112 _datum = _WGS84 

113 _E = _EWGS84 

114 _e_e2 = False # or True or 2-tuple for EcefYou only 

115 _lon00 = INT0 # arbitrary, "polar" lon for LocalCartesian, Ltp 

116 

117 def __init__(self, a_ellipsoid=_EWGS84, f=None, lon00=INT0, **name): 

118 '''New C{Ecef*} converter. 

119 

120 @arg a_ellipsoid: An ellipsoid (L{Ellipsoid}, L{Ellipsoid2} or L{a_f2Tuple}) 

121 or a datum (L{Datum}) or the ellipsoid's equatorial 

122 radius (C{meter}). 

123 @kwarg f: C{None} or the ellipsoid flattening (C{scalar}), required if 

124 C{B{a_ellipsoid} is scalar}. 

125 @kwarg lon00: An arbitrary, I{"polar"} longitude (C{degrees}), see the 

126 C{reverse} method. 

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

128 

129 @raise EcefError: If B{C{a_ellipsoid}} is not an L{Ellipsoid}, L{Ellipsoid2}, 

130 L{a_f2Tuple} or L{Datum} instance or a positive C{scalar} 

131 or if B{C{f}} is not C{scalar} and less than C{1.0}. 

132 ''' 

133 try: 

134 E = a_ellipsoid 

135 if f is None: 

136 pass 

137 elif _isRadius(E) and isscalar(f): 

138 E = a_f2Tuple(E, f) 

139 else: 

140 raise ValueError() # _invalid_ 

141 

142 if not _isin(E, _EWGS84, _WGS84): 

143 d = _ellipsoidal_datum(E, **name) 

144 E = d.ellipsoid 

145 if E.a < EPS or E.f > EPS1: 

146 raise ValueError() # _invalid_ 

147 self._datum = d 

148 self._E = E 

149 

150 if self._e_e2: # EcefYou 

151 E = self.ellipsoid 

152 e2 = E.a2 - E.b2 

153 if e2 < 0 or E.f < 0: 

154 raise ValueError(_prolate_) 

155 self._e_e2 = sqrt(e2), e2 

156 

157 except (TypeError, ValueError) as x: 

158 t = unstr(self.classname, a=a_ellipsoid, f=f) 

159 raise EcefError(_SPACE_(t, _ellipsoid_), cause=x) 

160 

161 if name: 

162 self.name = name 

163 if lon00 is not INT0: 

164 self.lon00 = lon00 

165 

166 def __eq__(self, other): 

167 '''Compare this and an other Ecef. 

168 

169 @arg other: The other ecef (C{Ecef*}). 

170 

171 @return: C{True} if equal, C{False} otherwise. 

172 ''' 

173 return other is self or (isinstance(other, type(self)) and 

174 other.ellipsoid == self.ellipsoid) 

175 

176 @Property_RO 

177 def datum(self): 

178 '''Get the datum (L{Datum}). 

179 ''' 

180 return self._datum 

181 

182 @Property_RO 

183 def ellipsoid(self): 

184 '''Get the ellipsoid (L{Ellipsoid} or L{Ellipsoid2}). 

185 ''' 

186 return self._E 

187 

188 @Property_RO 

189 def equatoradius(self): 

190 '''Get the C{ellipsoid}'s equatorial radius, semi-axis (C{meter}). 

191 ''' 

192 return self.ellipsoid.a 

193 

194 a = equatorialRadius = equatoradius # Karney property 

195 

196 @Property_RO 

197 def flattening(self): # Karney property 

198 '''Get the C{ellipsoid}'s flattening (C{scalar}), positive for 

199 I{oblate}, negative for I{prolate} or C{0} for I{near-spherical}. 

200 ''' 

201 return self.ellipsoid.f 

202 

203 f = flattening 

204 

205 def _forward(self, lat, lon, h, name, M=False, _philam=False): # in .ltp.LocalCartesian.forward and -.reset 

206 '''(INTERNAL) Common for all C{Ecef*}. 

207 

208 @note: From C{Karney}'s, let C{v} be a unit vector located at C{(lat, 

209 lon, h)}. We can express C{v} as column vectors in one of two 

210 ways, C{v1} in East, North, Up (ENU) coordinates (where the 

211 components are relative to a local coordinate system at C{C(lat0, 

212 lon0, h0)}) or as C{v0} in geocentric C{x, y, z} coordinates. 

213 Then, M{v0 = M ⋅ v1} where C{M} is the rotation matrix. 

214 ''' 

215 if _philam: # lat, lon in radians 

216 sa, ca, sb, cb = sincos2_(lat, lon) 

217 lat = Lat(degrees90( lat), Error=EcefError) 

218 lon = Lon(degrees180(lon), Error=EcefError) 

219 else: 

220 sa, ca, sb, cb = sincos2d_(lat, lon) 

221 

222 E = self.ellipsoid 

223 n = E.roc1_(sa, ca) if self._e_e2 else E.roc1_(sa) 

224 H = _isNAN0(h) 

225 c = (H + n) * ca 

226 x = cb * c 

227 y = sb * c 

228 z = (H + n * E.e21) * sa 

229 

230 m = self._Matrix(sa, ca, sb, cb) if M else None 

231 n = self._name__(name) 

232 return Ecef9Tuple(x, y, z, lat, lon, h, 0, # C=0, forward 

233 m, self.datum, name=n) 

234 

235 def forward(self, latlonh, lon=None, height=0, M=False, **name): 

236 '''Convert from geodetic C{(lat, lon, height)} to geocentric C{(x, y, z)}. 

237 

238 @arg latlonh: Either a C{LatLon}, an L{Ecef9Tuple} or C{scalar} 

239 latitude (C{degrees}). 

240 @kwarg lon: Optional C{scalar} longitude for C{scalar} B{C{latlonh}} 

241 (C{degrees}). 

242 @kwarg height: Optional height (C{meter}), vertically above (or below) 

243 the surface of the ellipsoid. 

244 @kwarg M: Optionally, return the rotation L{EcefMatrix} (C{bool}). 

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

246 

247 @return: An L{Ecef9Tuple}C{(x, y, z, lat, lon, height, C, M, datum)} with 

248 geocentric C{(x, y, z)} coordinates for the given geodetic ones 

249 C{(lat, lon, height)}, case C{C} (0, forward), rotation matrix 

250 C{M} (L{EcefMatrix} or C{None}) and C{datum}. 

251 

252 @raise EcefError: If B{C{latlonh}} not C{LatLon}, L{Ecef9Tuple} or 

253 C{scalar} or B{C{lon}} not C{scalar} for C{scalar} 

254 B{C{latlonh}} or C{abs(lat)} exceeds 90°. 

255 

256 @note: Use method C{.forward_} to specify C{lat} and C{lon} in C{radians} 

257 and avoid double angle conversions. 

258 ''' 

259 llhn = _llhn4(latlonh, lon, height, **name) 

260 return self._forward(*llhn, M=M) 

261 

262 def forward_(self, phi, lam, height=0, M=False, **name): 

263 '''Like method C{.forward} except with geodetic lat- and longitude given 

264 in I{radians}. 

265 

266 @arg phi: Latitude in I{radians} (C{scalar}). 

267 @arg lam: Longitude in I{radians} (C{scalar}). 

268 @kwarg height: Optional height (C{meter}), vertically above (or below) 

269 the surface of the ellipsoid. 

270 @kwarg M: Optionally, return the rotation L{EcefMatrix} (C{bool}). 

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

272 

273 @return: An L{Ecef9Tuple}C{(x, y, z, lat, lon, height, C, M, datum)} 

274 with C{lat} set to C{degrees90(B{phi})} and C{lon} to 

275 C{degrees180(B{lam})}. 

276 

277 @raise EcefError: If B{C{phi}} or B{C{lam}} invalid or not C{scalar}. 

278 ''' 

279 try: # like function C{_llhn4} below 

280 plhn = Phi(phi), Lam(lam), Height(height), _name__(name) 

281 except (TypeError, ValueError) as x: 

282 raise EcefError(phi=phi, lam=lam, height=height, cause=x) 

283 return self._forward(*plhn, M=M, _philam=True) 

284 

285 @property_ROver 

286 def _Geocentrics(self): 

287 '''(INTERNAL) Get the valid geocentric classes. I{once}. 

288 ''' 

289 return (Ecef9Tuple, # overwrite property_ROver 

290 _MODS.vector3d.Vector3d) # _MODS.cartesianBase.CartesianBase 

291 

292 @property 

293 def lon00(self): 

294 '''Get the I{"polar"} longitude (C{degrees}), see method C{reverse}. 

295 ''' 

296 return self._lon00 

297 

298 @lon00.setter # PYCHOK setter! 

299 def lon00(self, lon00): 

300 '''Set the I{"polar"} longitude (C{degrees}), see method C{reverse}. 

301 ''' 

302 self._lon00 = _Lon00(lon00) 

303 

304 def _Matrix(self, *sa_ca_sb_cb): 

305 '''(INTERNAL) Create a rotation L{EcefMatrix}. 

306 ''' 

307 return self._xnamed(EcefMatrix(*sa_ca_sb_cb)) 

308 

309 def _polon(self, y, x, p, lon00=None, name=NN): # PYCHOK name unused 

310 '''(INTERNAL) Handle I{"polar"} longitude. 

311 ''' 

312 return atan2d(y, x) if p else (self.lon00 if lon00 is None else _Lon00(lon00)) 

313 

314 def reverse(self, xyz, y=None, z=None, M=False, **lon00_name): 

315 '''Convert from geocentric C{(x, y, z)} to geodetic C{(lat, lon, height)}. 

316 

317 @arg xyz: A geocentric (C{Cartesian}, L{Ecef9Tuple}) or C{scalar} ECEF X 

318 coordinate (C{meter}). 

319 @kwarg y: ECEF Y coordinate for C{scalar} B{C{xyz}} and B{C{z}} (C{meter}). 

320 @kwarg z: ECEF Z coordinate for C{scalar} B{C{xyz}} and B{C{y}} (C{meter}). 

321 @kwarg M: Optionally, return the rotation L{EcefMatrix} (C{bool}). 

322 @kwarg lon00_name: Optional C{B{name}=NN} (C{str}) and optional keyword argument 

323 C{B{lon00}=INT0} (C{degrees}), an arbitrary I{"polar"} longitude 

324 returned if C{B{x}=0} and C{B{y}=0}, see property C{lon00}. 

325 

326 @return: An L{Ecef9Tuple}C{(x, y, z, lat, lon, height, C, M, datum)} with 

327 geodetic coordinates C{(lat, lon, height)} for the given geocentric 

328 ones C{(x, y, z)}, case indicator C{C} (C{int} 1..6), rotation matrix 

329 C{M} (L{EcefMatrix} or C{None}) and C{datum}. 

330 

331 @raise EcefError: Invalid B{C{xyz}} or C{scalar} C{x} or B{C{y}} and/or B{C{z}} 

332 not C{scalar} for C{scalar} B{C{xyz}}. 

333 ''' 

334 x, y, z, name = _xyzn4(xyz, y, z, self._Geocentrics, **lon00_name) 

335 

336 E = self.ellipsoid 

337 i = None 

338 

339 sa, ca, sb, cb, h, p, C = _norm7(y, x, z, E) 

340 if C: # PYCHOK no cover 

341 pass # too high, too far 

342 

343 elif p < EPS: # near polar 

344 p = 0 # force lon00 

345 sa = _copysign_1_0(z) 

346 ca = _0_0 

347 h = fabs(z) - E.b 

348 C = 2 # polar 

349 

350 elif E.e4: # E.isEllipsoidal 

351 s, t = _equatorial2(E, p, z) 

352 r = fsumf_(s, t, -E.e4) 

353 if t or r > 0: 

354 try: 

355 sa, ca, h, i, C = self._reverse5(1, h, p, z, y, x, r, s, t) # PYLINT E1111 

356 except (TypeError, ValueError) as X: 

357 t = unstr(self.reverse, x=x, y=y, z=z) 

358 raise EcefError(t, cause=X) 

359 

360 else: # near equatorial plane: e = E.e4 * q == 0 and r <= 0 

361 # This leads to k = 0 (oblate, equatorial plane) and k + E.e^2 = 0 

362 # (prolate, rotation axis) and the generation of 0/0 in the general 

363 # formulas for phi and h, using the general formula and division 

364 # by 0 in formula for h. Handle this case by taking the limits: 

365 # f > 0: z -> 0, k -> E.e2 * sqrt(q) / sqrt(E.e4 - s) 

366 # f < 0: r -> 0, k + E.e2 -> -E.e2 * sqrt(q) / sqrt(E.e4 - s) 

367 sa, ca, h, C = _equatorial4(E, s, z) 

368 

369 else: # E.isSpherical: E.e4 == 0 

370 # Dealing with underflow in the general case with E.e2 = 0 is 

371 # difficult. Origin maps to North pole, same as with ellipsoid. 

372 sa, ca, _ = _norm3((z if h else _1_0), p) 

373 h -= E.a 

374 C = 4 # spherical 

375 

376 lat = atan1d(sa, ca) 

377 # lon00 <https://GitHub.com/mrJean1/PyGeodesy/issues/77> 

378 lon = self._polon(sb, cb, p, **lon00_name) 

379 m = self._Matrix(sa, ca, sb, cb) if M else None 

380 return Ecef9Tuple(x, y, z, lat, lon, h, C, m, self.datum, 

381 iteration=i, name=self._name__(name)) # PYCHOK return 

382 

383 def _reverse5(self, *C_h_p_z_y_x_r_s_t): # PYCHOK no cover 

384 '''I{Must be overloaded}.''' 

385 self._notOverloaded(*C_h_p_z_y_x_r_s_t) 

386 

387 def toStr(self, prec=9, **unused): # PYCHOK signature 

388 '''Return this C{Ecef*} as a string. 

389 

390 @kwarg prec: Precision, number of decimal digits (0..9). 

391 

392 @return: This C{Ecef*} (C{str}). 

393 ''' 

394 return self.attrs(_a_, _f_, _datum_, _name_, prec=prec) # _ellipsoid_ 

395 

396 

397class EcefError(_ValueError): 

398 '''An ECEF or C{Ecef*} related issue. 

399 ''' 

400 pass # PYLINT W0107 

401 

402 

403class EcefFarrell21(_EcefBase): 

404 '''Conversion between geodetic and geocentric, I{Earth-Centered, Earth-Fixed} (ECEF) 

405 coordinates based on I{Jay A. Farrell}'s U{Table 2.1<https://Books.Google.com/ 

406 books?id=fW4foWASY6wC>}, page 29, aka the I{Heikkinen application} of U{Ferrari's 

407 solution<https://WikiPedia.org/wiki/Geographic_coordinate_conversion>}. 

408 

409 @see: Classes L{EcefFarrell22} and L{EcefVeness}. 

410 ''' 

411 def _reverse5(self, C, h, p, z, *unused): # PYCHOK signature 

412 E = self.ellipsoid 

413 a = E.a 

414 a2 = E.a2 

415 b2 = E.b2 

416 e2 = E.e2 

417 e2_ = E.e2abs * E.a2_b2 # (E.e * E.a_b)**2 = 0.0820944... WGS84 

418 e4 = E.e4 

419 

420 z2 = z**2 # names as page 29 

421 ez = z2 * (_1_0 - e2) # E.e2s2(z) 

422 

423 p2 = p**2 

424 G = p2 + ez - e2 * (a2 - b2) # p2 + ez - e4 * a2 

425 F = b2 * z2 * 54 

426 c = e4 * p2 * F / G**3 

427 s = sqrt(c * (c + _2_0)) 

428 c = cbrt(s + c + _1_0) 

429 G *= fsumf_(c, _1_0, _1_0 / c) # k 

430 P = F / (G**2 * _3_0) 

431 Q = sqrt(_2_0 * e4 * P + _1_0) 

432 Q1 = Q + _1_0 

433 s = fsumf_(a2 * (Q1 / Q) * _0_5, 

434 -P * ez / (Q1 * Q), 

435 -P * p2 * _0_5) 

436 r = p * P * e2 / Q1 - sqrt(s) 

437 r = p + r * e2 

438 v = b2 / (sqrt(r**2 + ez) * a) # z0 / z 

439 

440 h = hypot(r, z) * (_1_0 - v) 

441 z += e2_ * v * z # lat = atan1d(z, p) 

442 return z, p, h, None, C 

443 # note, phi and lam are swapped on page 29 

444 

445 

446class EcefFarrell22(_EcefBase): 

447 '''Conversion between geodetic and geocentric, I{Earth-Centered, Earth-Fixed} (ECEF) 

448 coordinates based on I{Jay A. Farrell}'s U{Table 2.2<https://Books.Google.com/ 

449 books?id=fW4foWASY6wC>}, page 30, aka I{B.R. Bowring's closed-form}. 

450 

451 @see: Classes L{EcefFarrell21} and L{EcefVeness}. 

452 ''' 

453 def _reverse5(self, C, h, p, z, *unused): # PYCHOK signature 

454 E = self.ellipsoid 

455 a, b = E.a, E.b 

456 s, c, _ = _norm3(z * a, p * b) # Bowring theta 

457 sa, ca = _latB2(E, s, c, z, p) # Bowring lat 

458 if ca: 

459 h = p / fabs(ca) 

460 if sa: 

461 h -= E.roc1_(sa) 

462 else: 

463 h -= a 

464# C = 3 # XXX 1? 

465 else: 

466 h = fabs(z) - b 

467 C = 2 

468 # lat = atan1d(sa, ca) 

469 return sa, ca, h, None, C 

470 # note, phi and lam are swapped on page 30 

471 

472 

473class EcefFukushima(_EcefBase): 

474 '''Conversion between geodetic and geocentric, I{Earth-Centered, Earth-Fixed} (ECEF) coordinates 

475 transcoded from I{Toshio Fukushima}'s U{Fortran<https://www.ResearchGate.net/publication/277721539>} 

476 implementation. 

477 

478 @see: Fukushima, T. U{Transformation from Cartesian to Geodetic Coordinates Accelerated by 

479 Halley’s Method<https://www.researchgate.net/publication/227215135>} and Eleiche, M. 

480 U{A comparison between Fukushima-Halley algorithm and Trilateration algorithm for 

481 geodetic conversion<https://link.Springer.com/article/10.1007/s12145-022-00779-7>}. 

482 ''' 

483 def _reverse5(self, C, h, p, z, *unused): # PYCHOK signature 

484 E = self.ellipsoid 

485 a = E.a 

486 e2 = E.e2 

487 e4 = _1_5 * E.e4 # e4T 

488 ec = _1_0 - E.f # sqrt(_1_0 - e2) 

489# assert (a * ec) == E.b 

490 

491 za = fabs(z) 

492 s0 = za / a 

493 zc = s0 * ec 

494 pn = p / a 

495 # Newton Correction Factors 

496 c0 = pn * ec 

497 c2 = c0**2 

498# c3 = c2 * c0 

499 s2 = s0**2 

500# s3 = s2 * s0 

501# a2 = s2 + c2 

502 a0 = hypot(s0, c0) # sqrt(a2) 

503 a3 = a0**3 # a0 * a2 

504 d0 = a3 * zc + e2 * s2 * s0 # s3 

505 f0 = a3 * pn - e2 * c2 * c0 # c3 

506 # Halley Correction Factor 

507 b0 = e4 * s2 * c2 * pn * (a0 - ec) 

508 sa = d0 * f0 - b0 * s0 

509 ca = (f0**2 - b0 * c0) * ec 

510 

511 # lat = atan1d(sa, ca) 

512 h = hypot(ec * sa, ca) 

513 h = fsumf_(p * ca, za * sa, -h * a) 

514 h = _over(h, hypot(sa, ca)) 

515 return sa, ca, h, None, C 

516 

517 

518class EcefKarney(_EcefBase): 

519 '''Conversion between geodetic and geocentric, I{Earth-Centered, Earth-Fixed} (ECEF) coordinates 

520 transcoded from I{Karney}'s C++ U{Geocentric<https://GeographicLib.SourceForge.io/C++/doc/ 

521 classGeographicLib_1_1Geocentric.html>} methods. 

522 

523 @note: In general, there are multiple solutions and the result which minimizes C{height} is 

524 returned, i.e., the C{(lat, lon)} corresponding to the closest point on the ellipsoid. 

525 If there are still multiple solutions with different latitudes (applies only if C{z} 

526 = 0), then the solution with C{lat} > 0 is returned. If there are still multiple 

527 solutions with different longitudes (applies only if C{x} = C{y} = 0), then C{lon00} 

528 is returned. The returned C{lon} is in the range [−180°, 180°] and C{height} is not 

529 below M{−E.a * (1 − E.e2) / sqrt(1 − E.e2 * sin(lat)**2)}. Like C{forward} above, 

530 M{v1 = Transpose(M) ⋅ v0}. 

531 ''' 

532 def _reverse5(self, C, h, p, z, y, x, r, s, q): # PYCHOK unused y, x 

533 E = self.ellipsoid 

534 e = E.e4 * q # p renamed to s 

535 # Avoid possible division by zero when r = 0 by multiplying 

536 # equations for s and t by r^3 and r, respectively. 

537 d = s = e * s / _4_0 # s = r^3 * s 

538 u = r = r / _6_0 

539 r2 = r**2 

540 r3 = r2 * r 

541 t3 = r3 + s 

542 d *= t3 + r3 

543 if d < 0: 

544 # t is complex, but the way u is defined, the result is real. 

545 # There are three possible cube roots. We choose the root 

546 # which avoids cancellation. Note, d < 0 implies r < 0. 

547 u += cos(atan2(sqrt(-d), -t3) / _3_0) * r * _2_0 

548 else: 

549 # Pick the sign on the sqrt to maximize abs(t3). This 

550 # minimizes loss of precision due to cancellation. The 

551 # result is unchanged because of the way the t is used 

552 # in definition of u. 

553 if d > 0: 

554 t3 += copysign0(sqrt(d), t3) # t3 = (r * t)^3 

555 # N.B. cbrt always returns the real root, cbrt(-8) = -2. 

556 t = cbrt(t3) # t = r * t 

557 if t: # t can be zero; but then r2 / t -> 0. 

558 u = fsumf_(u, t, r2 / t) 

559 v = sqrt(u**2 + e) # guaranteed positive 

560 # Avoid loss of accuracy when u < 0. Underflow doesn't occur in 

561 # E.e4 * q / (v - u) because u ~ e^4 when q is small and u < 0. 

562 u = (e / (v - u)) if u < 0 else (u + v) # u+v, guaranteed positive 

563 # Need to guard against w going negative due to roundoff in u - q. 

564 w = E.e2abs * (u - q) / (_2_0 * v) 

565 # Rearrange expression for k to avoid loss of accuracy due to 

566 # subtraction. Division by 0 not possible because u > 0, w >= 0. 

567 k1 = k2 = (u / (sqrt(w**2 + u) + w)) if w > 0 else sqrt(u) 

568 if E.f < 0: 

569 k1 -= E.e2 

570 else: 

571 k2 += E.e2 

572 sa, ca, h = _norm3(z / k1, p / k2) 

573 h *= k1 - E.e21 

574 return sa, ca, h, None, C 

575 

576 

577class EcefOlson(_EcefBase): 

578 '''Conversion between geodetic and geocentric, I{Earth-Centered, Earth-Fixed} (ECEF) coordinates 

579 based on I{D.K. Olson}'s U{method<https://IEEEXplore.IEEE.org/document/481290>}. 

580 

581 @see: U{Possibly wrong<https://PossiblyWrong.Wordpress.com/2014/02/14/>} and U{geodesic-fortran 

582 <https://JacobWilliams.GitHub.io/geodesic-fortran/proc/olson.html>}. 

583 ''' 

584 @Property_RO 

585 def _E_a1_a4(self): 

586 E = self.ellipsoid 

587 a1 = E.a * E.e2 

588 a2 = a1**2 

589 a3 = a1 * E.e2 * _0_5 

590 a4 = a2 * 2.5 

591 return E, a1, a2, a3, a4 

592 

593 def _reverse5(self, C, r, p, z, *unused): # PYCHOK signature 

594 E, a1, a2, a3, a4 = self._E_a1_a4 

595 _1_r = _1_0 / r 

596 

597 u = a2 * _1_r 

598 v = a3 - a4 * _1_r 

599 za = fabs(z) 

600 try: 

601 s = za * _1_r 

602 c = p * _1_r 

603 if c > 0.5477225575: # == sqrt(0.3) 

604 c *= (a1 + u + s**2 * v) * c * _1_r 

605 s *= _1_0 + c 

606 ra = asin(s) 

607 s2 = s**2 

608 c = sqrt(_1_0 - s2) 

609 else: 

610 u -= a3 

611 s *= (a1 - u - c**2 * v) * s * _1_r 

612 c *= _1_0 - s 

613 ra = acos(c) 

614 s2 = _1_0 - c**2 

615 s = sqrt(s2) 

616 g = _1_0 - E.e2 * s2 

617 rg = E.a / sqrt(g) 

618 rf = rg * E.e21 

619 u = p - rg * c 

620 v = za - rf * s 

621 

622 h = c * u + s * v 

623 m = c * v - s * u 

624 r = m * g / (h * g + rf) 

625 h += m * r * _0_5 

626 ra += r 

627 if z < 0: 

628 ra = -ra 

629 sa, ca = sincos2(ra) 

630 

631 except (TypeError, ValueError): # domain error 

632 sa, ca, h, C = _subterrestrial4(E, z, p) 

633 return sa, ca, h, None, C 

634 

635 

636class EcefOsen(_EcefBase): 

637 '''Conversion between geodetic and geocentric, I{Earth-Centered, Earth-Fixed} (ECEF) coordinates 

638 based on I{Karl Osen}'s U{boost<https://HAL.Science/hal-01704943v2>} of I{Jijie Zhu}'s U{algorithm 

639 <https://IEEEXplore.IEEE.org/document/303772>}. 

640 ''' 

641 @Property_RO 

642 def _E_Hmin_L_L2_E21a2(self): 

643 E = self.ellipsoid 

644 Hmin = pow(E.e, 12) / _6_0 # Hmin 64-bit 

645 L = E.e2 * _0_5 

646 L2 = E.e4 * _0_25 # L**2 

647 E21a2 = E.e21 / E.a2 

648 return E, Hmin, L, L2, E21a2 

649 

650 def _reverse5(self, C, _, w, z, *unused): # PYCHOK signature 

651 E, Hmin, L, L2, E21a2 = self._E_Hmin_L_L2_E21a2 

652 w2 = w**2 

653 m = w2 / E.a2 # invaa 

654 n = z**2 * E21a2 # p1meedaa 

655 mn = m + n # mpn 

656 P = (mn - E.e4) / _6_0 # L2 * _4_0 

657 G = m * n * L2 

658 H = P**3 * _2_0 + G 

659 if H < Hmin: # below 43 Km from center 

660 sa, ca, h, C = _subterrestrial4(E, z, w) 

661 else: 

662 i = -(mn * _0_5 + L2) 

663 B = cbrt((H + G) * _0_5 + sqrt(H * G)) # pow(H + G + 2 * sqrt(H * G), inv3) * invcbrt2 

664 B = i / _3_0 - B - P**2 / B 

665 k = (L2 - mn) * L2 

666 

667 # compute left part of t 

668 t2 = sqrt(B**2 - k) # t1 

669 t3 = t2 - (B + i) * _0_5 

670 # compute right part of t 

671 t5 = (B - i) * _0_5 

672 # t5 may accidentally drop just below zero due to 

673 # numeric turbulence at lats near +/- 45.3 degrees 

674 t6 = sqrt(fabs(t5)) 

675 # add left and right parts 

676 t = sqrt(t3) + (t6 if m < n else -t6) 

677 

678 # Newton-Raphson to compute t correction 

679 g = (m - n) * E.e2 # L * 2 

680 F = ((t**2 + i * _2_0) * t + g) * t + k 

681 D = (t**3 + i * t) * _4_0 + g # dFdt 

682 dt = -F / D 

683 

684 # compute latitude (range -PI/2..PI/2) 

685 t += dt 

686 u = t + L 

687 v = t - L 

688 sa = z * u # zu 

689 ca = w * v # wv 

690 

691 uv = u * v # invuv 

692 h = hypot(w - ca / uv, 

693 z - sa / uv * E.e21) # p1mee 

694 if u < 1: 

695 h = -h 

696 return sa, ca, h, None, C 

697 

698 

699class EcefSudano(_EcefBase): 

700 '''Conversion between geodetic and geocentric, I{Earth-Centered, Earth-Fixed} (ECEF) coordinates 

701 based on I{John J. Sudano}'s U{paper<https://www.ResearchGate.net/publication/3709199>}. 

702 ''' 

703 _TOL = \ 

704 _tol = EPS 

705 

706 def reverse(self, xyz, y=None, z=None, M=False, tol=EPS, **lon00_name): # PYCHOK tol 

707 '''Convert from geocentric C{(x, y, z)} to geodetic C{(lat, lon, height)} using 

708 I{Sudano}'s U{iterative method<https://www.ResearchGate.net/publication/3709199>}. 

709 

710 @kwarg tol: Convergence tolerance for C{sin(latitude)} (C{scalar}). 

711 

712 @see: L{Parent method<_EcefBase.reverse>} for all other information. 

713 

714 @raise EcefError: Invalid B{C{xyz}} or C{scalar} C{x} or B{C{y}} and/or B{C{z}} not 

715 C{scalar} for C{scalar} B{C{xyz}} or no convergence for C{B{tol}}. 

716 ''' 

717 if tol != self._TOL: 

718 self._tol = Scalar_(tol=tol, low=EPS, Error=EcefError) 

719 return _EcefBase.reverse(self, xyz, y=y, z=z, M=M, **lon00_name) 

720 

721 def _reverse5(self, C, h, p, z, *unused): # PYCHOK signature 

722 E = self.ellipsoid 

723 e = E.e2 * E.a 

724 d = e - p 

725 

726 sa, ca, _ = _norm3(fabs(z), p * E.e21) 

727 # Sudano's Eq (A-6) and (A-7) refactored/reduced, 

728 # replacing Rn from Eq (A-4) with n = E.a / ca: 

729 # N = ca**2 * ((z + E.e2 * n * sa) * ca - p * sa) 

730 # = ca**2 * (z * ca + E.e2 * E.a * sa - p * sa) 

731 # = ca**2 * (z * ca + (E.e2 * E.a - p) * sa) 

732 # D = ca**3 * (E.e2 * n / E.e2s2(sa)) - p 

733 # = ca**2 * (E.e2 * E.a / E.e2s2(sa) - p / ca**2) 

734 # N / D = (z * ca + (E.e2 * E.a - p) * sa) / 

735 # (E.e2 * E.a / E.e2s2(sa) - p / ca**2) 

736 tol = self._tol 

737 _S2 = Fsum(sa).fsum2f_ 

738 for i in range(1, _TRIPS): # 6+ max 

739 ca2 = _1_0 - sa**2 

740 if ca2 < EPS02: 

741 break 

742 D = p / ca2 - e / E.e2s2(sa) 

743 if fabs(D) < EPS0: 

744 break 

745 ca = sqrt(ca2) 

746 sa, D = _S2(z * ca / D, d * sa / D) 

747 if fabs(D) < tol: 

748 break 

749 else: # PYCHOK no cover 

750 raise ValueError(Fmt.no_convergence(fabs(D), tol)) 

751 

752 sa = copysign0(sa, z) 

753 # lat = atan1d(sa, ca) 

754 # h = (fabs(z) + p - E.a * cos(a + E.e21) * sa / ca) / (ca + sa) 

755 # Sudano's Eq (7) doesn't produce the correct height, ... 

756 h = _heightB(E, sa, ca, z, p) # ... use Veness' (Bowring eqn 7) 

757 return sa, ca, h, i, C 

758 

759 @deprecated_property 

760 def tolerance(self): 

761 '''DEPRECATED on 2025.08.22, use keyword argument C{tol}.''' 

762 return self._tol 

763 

764 @tolerance.setter # PYCHOK setter! 

765 def tolerance(self, tol): 

766 self._tol = Scalar_(tolerance=tol, low=EPS, Error=EcefError) 

767 

768 

769class EcefUPC(_EcefBase): 

770 '''Conversion between geodetic and geocentric, I{Earth-Centered, Earth-Fixed} (ECEF) coordinates based on 

771 I{UPC}'s U{method<https://GSSC.ESA.int/navipedia/index.php/Ellipsoidal_and_Cartesian_Coordinates_Conversion>}. 

772 ''' 

773 _TOL = _TOL 

774 _tol = _Degrees2Radians(_TOL) 

775 

776 def reverse(self, xyz, y=None, z=None, M=False, tol=_TOL, **lon00_name): # PYCHOK tol 

777 '''Convert from geocentric C{(x, y, z)} to geodetic C{(lat, lon, height)} using I{UPC}'s 

778 U{iterative method<https://GSSC.ESA.int/navipedia/GNSS_Book/ESA_GNSS-Book_TM-23_Vol_I.pdf>}, page 186. 

779 

780 @kwarg tol: Convergence tolerance for the C{latitude} (C{degrees}). 

781 

782 @see: L{Parent method<_EcefBase.reverse>} for all other information. 

783 

784 @raise EcefError: Invalid B{C{xyz}} or C{scalar} C{x} or B{C{y}} and/or B{C{z}} not 

785 C{scalar} for C{scalar} B{C{xyz}} or no convergence for C{B{tol}}. 

786 ''' 

787 if tol != _TOL: 

788 self._tol = _Degrees2Radians(tol) 

789 return _EcefBase.reverse(self, xyz, y=y, z=z, M=M, **lon00_name) 

790 

791 def _reverse5(self, C, h, p, z, *unused): # PYCHOK signature 

792 E = self.ellipsoid 

793 a = E.a 

794 e2 = E.e2 # signed 

795 

796 za = fabs(z) 

797 ph_ = atan1(za, E.e21 * p) 

798 tol = self._tol 

799 for i in range(1, _TRIPS): # 5..6 max 

800 s, c = sincos2(ph_) 

801 N = a / sqrt(_1_0 - s**2 * e2) # N + h == N + p / c - N == p / c 

802 ca = p - N * c * e2 # == p * (1 - N * e2 / (N + h)) == p * (1 - N * e2 * c / p) 

803 ph = atan1(za, ca) # atan1(z / p, 1 - N * e2 / (N + h)) == atan1(z, ca) 

804 r = fabs(ph - ph_) 

805 if r < tol: 

806 # lat = copysign0(degrees(ph), z) 

807 # == atan1d(z, ca) 

808 h = p / c - N 

809 break 

810 ph_ = ph 

811 else: # PYCHOK no cover 

812 r, tol = map1(degrees, r, tol) 

813 raise ValueError(Fmt.no_convergence(r, tol)) 

814 return z, ca, h, i, C 

815 

816 

817class EcefVeness(_EcefBase): 

818 '''Conversion between geodetic and geocentric, I{Earth-Centered, Earth-Fixed} (ECEF) coordinates 

819 transcoded from I{Chris Veness}' JavaScript classes U{LatLonEllipsoidal, Cartesian<https:// 

820 www.Movable-Type.co.UK/scripts/geodesy/docs/latlon-ellipsoidal.js.html>}. 

821 

822 @note: Uses B. R. Bowring’s formulation for μm precision in concise form U{The accuracy of 

823 geodetic latitude and height equations<https://www.ResearchGate.net/publication/233668213>}, 

824 Survey Review, Vol 28, 218, Oct 1985. 

825 

826 @see: U{A Guide to Coordinate Systems in Great Britain<https://www.OrdnanceSurvey.co.UK/documents/ 

827 resources/guide-coordinate-systems-great-britain.pdf>}, section I{B) Converting between 3D 

828 Cartesian and ellipsoidal latitude, longitude and height coordinates}. 

829 

830 @see: Toms, Ralph M. U{An Efficient Algorithm for Geocentric to Geodetic Coordinate Conversion 

831 <https://www.OSTI.gov/scitech/biblio/110235>}, Sept 1995 and U{An Improved Algorithm for 

832 Geocentric to Geodetic Coordinate Conversion<https://www.OSTI.gov/scitech/servlets/purl/231228>}, 

833 Apr 1996, both from Lawrence Livermore National Laboratory (LLNL). 

834 ''' 

835 def _reverse5(self, C, h, p, z, *unused): # PYCHOK signature 

836 # assert h >= p > 0 # h = hypot(z, p) 

837 E = self.ellipsoid 

838 B = E.b * E.e22 

839 # parametric latitude, Bowring eqn 17, replaced 

840 t = (E.b * z) / (E.a * p) * (B / h + _1_0) # theta 

841 c = _1_0 / hypot1(t) # t == atan2(z * E.a, p * E.b) 

842 s = c * t # s, c == sincos2(t) 

843 sa, ca = _latB2(E, s, c, z, p) # lat, Bowring eqn 18 

844 # lat = atan1d(sa, ca) 

845 h = _heightB(E, sa, ca, z, p) # height, Bowring eqn 7 

846 return sa, ca, h, None, C 

847 

848 

849class EcefYou(_EcefBase): 

850 '''Conversion between geodetic and geocentric, I{Earth-Centered, Earth-Fixed} (ECEF) coordinates 

851 using I{Rey-Jer You}'s U{transformation<https://www.ResearchGate.net/publication/240359424>} 

852 for I{non-prolate} ellipsoids. 

853 

854 @see: Featherstone, W.E., Claessens, S.J. U{Closed-form transformation between geodetic and 

855 ellipsoidal coordinates<https://Espace.Curtin.edu.AU/bitstream/handle/20.500.11937/11589/ 

856 115114_9021_geod2ellip_final.pdf>} Studia Geophysica et Geodaetica, 2008, 52, pages 1-18 

857 and U{PyMap3D<https://PyPI.org/project/pymap3d>}. 

858 ''' 

859 _e_e2 = True # or 2-tuple C{(e, e2)} 

860 

861 def _reverse5(self, C, h, p, z, y, x, *unused): # PYCHOK signature 

862 E = self.ellipsoid 

863 a, b = E.a, E.b 

864 e, e2 = self._e_e2 

865 

866 u = hypot2_(x, y, z) - e2 

867 u += hypot(u, e * z * _2_0) 

868 u *= _0_5 

869 if u > EPS02: 

870 u = sqrt(u) 

871 q = hypot(u, e) 

872 B = atan1(q * z, u * p) # beta0 = atan(q / u * z / p) 

873 sB, cB = sincos2(B) 

874 if cB and sB: 

875 q *= a 

876 d = (q / cB - e2 * cB) / sB 

877 if isnon0(d): 

878 B += fsumf_(u * b, -q, e2) / d 

879 sB, cB = sincos2(B) 

880 elif u < (-EPS02): 

881 raise EcefError(u=u, txt=_singular_) 

882 else: # near polar # PYCHOK no cover 

883 sB, cB, C = _copysign_1_0(z), _0_0, 2 

884 

885 h = hypot(p - a * cB, z - b * sB) 

886 if hypot2_(x, y, z * E.a_b) < E.a2: # or lat < 0 or z < 0 

887 h = neg(h) # inside ellipsoid 

888 # lat = atand(E.a_b * tan(B)) == atan1d(a * sB, b * cB) 

889 return (a * sB), (b * cB), h, None, C 

890 

891 

892class EcefMatrix(_NamedTuple): 

893 '''A rotation matrix known as I{East-North-Up (ENU) to ECEF}. 

894 

895 @see: U{From ENU to ECEF<https://WikiPedia.org/wiki/ 

896 Geographic_coordinate_conversion#From_ECEF_to_ENU>} and 

897 U{Issue #74<https://Github.com/mrJean1/PyGeodesy/issues/74>}. 

898 ''' 

899 _Names_ = ('_0_0_', '_0_1_', '_0_2_', # row-major order 

900 '_1_0_', '_1_1_', '_1_2_', 

901 '_2_0_', '_2_1_', '_2_2_') 

902 _Units_ = (Scalar,) * len(_Names_) 

903 

904 def _validate(self, **unused): # PYCHOK unused 

905 '''(INTERNAL) Allow C{_Names_} with leading underscore. 

906 ''' 

907 _NamedTuple._validate(self, underOK=True) 

908 

909 def __new__(cls, sa, ca, sb, cb, *_more, **name): 

910 '''New L{EcefMatrix} matrix. 

911 

912 @arg sa: C{sin(phi)} (C{float}). 

913 @arg ca: C{cos(phi)} (C{float}). 

914 @arg sb: C{sin(lambda)} (C{float}). 

915 @arg cb: C{cos(lambda)} (C{float}). 

916 @arg _more: (INTERNAL) from C{.multiply}. 

917 

918 @raise EcefError: If B{C{sa}}, B{C{ca}}, B{C{sb}} or 

919 B{C{cb}} outside M{[-1.0, +1.0]}. 

920 ''' 

921 t = sa, ca, sb, cb 

922 if _more: # all 9 matrix elements ... 

923 t += _more # ... from .multiply 

924 

925 elif max(map(fabs, t)) > _1_0: 

926 raise EcefError(unstr(EcefMatrix, *t)) 

927 

928 else: # build matrix from the following quaternion operations 

929 # qrot(lam, [0,0,1]) * qrot(phi, [0,-1,0]) * [1,1,1,1]/2 

930 # or 

931 # qrot(pi/2 + lam, [0,0,1]) * qrot(-pi/2 + phi, [-1,0,0]) 

932 # where 

933 # qrot(t,v) = [cos(t/2), sin(t/2)*v[1], sin(t/2)*v[2], sin(t/2)*v[3]] 

934 

935 # Local X axis (East) in geocentric coords 

936 # M[0] = -slam; M[3] = clam; M[6] = 0; 

937 # Local Y axis (North) in geocentric coords 

938 # M[1] = -clam * sphi; M[4] = -slam * sphi; M[7] = cphi; 

939 # Local Z axis (Up) in geocentric coords 

940 # M[2] = clam * cphi; M[5] = slam * cphi; M[8] = sphi; 

941 t = (-sb, -cb * sa, cb * ca, 

942 cb, -sb * sa, sb * ca, 

943 _0_0, ca, sa) 

944 

945 return _NamedTuple.__new__(cls, *t, **name) 

946 

947 def column(self, column): 

948 '''Get this matrix' B{C{column}} 0, 1 or 2 as C{3-tuple}. 

949 ''' 

950 if 0 <= column < 3: 

951 return self[column::3] 

952 raise _IndexError(column=column) 

953 

954 @property_RO 

955 def _columns(self): 

956 for c in range(3): 

957 yield self[c::3] 

958 

959 def copy(self, **unused): # PYCHOK signature 

960 '''Make a shallow or deep copy of this instance. 

961 

962 @return: The copy (C{This class} or subclass thereof). 

963 ''' 

964 return self.classof(*self) 

965 

966 __copy__ = __deepcopy__ = copy 

967 

968 @Property_RO 

969 def matrix3(self): 

970 '''Get this matrix' rows (C{3-tuple} of 3 C{3-tuple}s). 

971 ''' 

972 return tuple(self._rows) 

973 

974 @Property_RO 

975 def matrixTransposed3(self): 

976 '''Get this matrix' I{Transposed} rows (C{3-tuple} of 3 C{3-tuple}s). 

977 ''' 

978 return tuple(self._columns) 

979 

980 def multiply(self, other): 

981 '''Matrix multiply M{M0' ⋅ M} this matrix I{Transposed} with an other matrix. 

982 

983 @arg other: The other matrix (L{EcefMatrix}). 

984 

985 @return: The matrix product (L{EcefMatrix}). 

986 

987 @raise TypeError: If B{C{other}} is not an L{EcefMatrix}. 

988 ''' 

989 _xinstanceof(EcefMatrix, other=other) 

990 # like LocalCartesian.MatrixMultiply, C{self.matrixTransposed3 X other.matrix3} 

991 # <https://GeographicLib.SourceForge.io/C++/doc/LocalCartesian_8cpp_source.html> 

992 X = (_fdotf(t, *c) for t in self._columns for c in other._columns) 

993 return _xnamed(EcefMatrix(*X), typename(EcefMatrix.multiply)) 

994 

995 def rotate(self, xyz, *xyz0): 

996 '''Forward rotation M{M0' ⋅ ([x, y, z] - [x0, y0, z0])'}. 

997 

998 @arg xyz: Local C{(x, y, z)} coordinates (C{3-tuple}). 

999 @arg xyz0: Optional, local C{(x0, y0, z0)} origin (C{3-tuple}). 

1000 

1001 @return: Rotated C{(x, y, z)} location (C{3-tuple}). 

1002 

1003 @raise LenError: Unequal C{len(B{xyz})} and C{len(B{xyz0})}. 

1004 ''' 

1005 if xyz0: 

1006 if len(xyz0) != len(xyz): 

1007 raise LenError(self.rotate, xyz0=len(xyz0), xyz=len(xyz)) 

1008 xyz = tuple(s - s0 for s, s0 in zip(xyz, xyz0)) 

1009 

1010 # x' = M[0] * x + M[3] * y + M[6] * z 

1011 # y' = M[1] * x + M[4] * y + M[7] * z 

1012 # z' = M[2] * x + M[5] * y + M[8] * z 

1013 return tuple(_fdotf(xyz, *c) for c in self._columns) 

1014 

1015 def row(self, row): 

1016 '''Get this matrix' B{C{row}} 0, 1 or 2 as C{3-tuple}. 

1017 ''' 

1018 if 0 <= row < 3: 

1019 r = row * 3 

1020 return self[r:r+3] 

1021 raise _IndexError(row=row) 

1022 

1023 @property_RO 

1024 def _rows(self): 

1025 for r in (0, 3, 6): 

1026 yield self[r:r+3] 

1027 

1028 def unrotate(self, xyz, *xyz0): 

1029 '''Inverse rotation M{[x0, y0, z0] + M0 ⋅ [x,y,z]'}. 

1030 

1031 @arg xyz: Local C{(x, y, z)} coordinates (C{3-tuple}). 

1032 @arg xyz0: Optional, local C{(x0, y0, z0)} origin (C{3-tuple}). 

1033 

1034 @return: Unrotated C{(x, y, z)} location (C{3-tuple}). 

1035 

1036 @raise LenError: Unequal C{len(B{xyz})} and C{len(B{xyz0})}. 

1037 ''' 

1038 if xyz0: 

1039 if len(xyz0) != len(xyz): 

1040 raise LenError(self.unrotate, xyz0=len(xyz0), xyz=len(xyz)) 

1041 _xyz = _1_0_1T + xyz 

1042 # x' = x0 + M[0] * x + M[1] * y + M[2] * z 

1043 # y' = y0 + M[3] * x + M[4] * y + M[5] * z 

1044 # z' = z0 + M[6] * x + M[7] * y + M[8] * z 

1045 xyz_ = (_fdotf(_xyz, s, *r) for s, r in zip(xyz0, self._rows)) 

1046 else: 

1047 # x' = M[0] * x + M[1] * y + M[2] * z 

1048 # y' = M[3] * x + M[4] * y + M[5] * z 

1049 # z' = M[6] * x + M[7] * y + M[8] * z 

1050 xyz_ = (_fdotf(xyz, *r) for r in self._rows) 

1051 return tuple(xyz_) 

1052 

1053 

1054class Ecef9Tuple(_NamedTuple, _EcefLocal): 

1055 '''9-Tuple C{(x, y, z, lat, lon, height, C, M, datum)} with I{geocentric} C{x}, 

1056 C{y} and C{z} plus I{geodetic} C{lat}, C{lon} and C{height}, case C{C} and 

1057 optionally, rotation matrix C{M} (L{EcefMatrix} or C{None}) and C{datum}, 

1058 with C{lat} and C{lon} in C{degrees} and C{x}, C{y}, C{z} and C{height} in 

1059 C{meter}, conventionally. Case C{C=0} means C{x, y,z} from foward, C{C=1} 

1060 C{lat, lon, height} from reverse, C{C=2} near-polar C{lat, lon}, C{C=3} 

1061 near-equatorial C{lat, lon}, C{C=4} spherical C{lat, lon}, C{C=5} if the 

1062 C{height} exceeds C{datum}'s C{ellipsoid.heightMax} and C{C=6} for points 

1063 too subterrestrial. 

1064 ''' 

1065 _Names_ = (_x_, _y_, _z_, _lat_, _lon_, _height_, _C_, _M_, _datum_) 

1066 _Units_ = ( Meter, Meter, Meter, Lat, Lon, Height, Int, _Pass, _Pass) 

1067 

1068 @property_ROver 

1069 def _CartesianBase(self): 

1070 '''(INTERNAL) Get class C{CartesianBase}, I{once}. 

1071 ''' 

1072 return _MODS.cartesianBase.CartesianBase # overwrite property_ROver 

1073 

1074 @deprecated_method 

1075 def convertDatum(self, datum2): # for backward compatibility 

1076 '''DEPRECATED, use method L{toDatum}.''' 

1077 return self.toDatum(datum2) 

1078 

1079 @property_RO 

1080 def _ecef9(self): # in ._EcefLocal._Ltp_ecef2local 

1081 return self 

1082 

1083 @property_RO 

1084 def ellipsoid(self): 

1085 '''Get the ellipsoid (L{Ellipsoid}). 

1086 ''' 

1087 return (self.datum or _WGS84).ellipsoid 

1088 

1089 @Property_RO 

1090 def lam(self): 

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

1092 ''' 

1093 return self.philam.lam 

1094 

1095 @Property_RO 

1096 def lamVermeille(self): 

1097 '''Get the longitude in C{radians} M{[-PI*3/2..+PI*3/2]} after U{Vermeille 

1098 <https://Search.ProQuest.com/docview/639493848>} (2004), page 95. 

1099 

1100 @see: U{Karney<https://GeographicLib.SourceForge.io/C++/doc/geocentric.html>}, 

1101 U{Vermeille<https://Search.ProQuest.com/docview/847292978>} 2011, pp 112-113, 116 

1102 and U{Featherstone, et.al.<https://Search.ProQuest.com/docview/872827242>}, page 7. 

1103 ''' 

1104 x, y = self.x, self.y 

1105 a = fabs(y) 

1106 if a > EPS0: 

1107 r = PI_2 - atan2(x, hypot(x, a) + a) * _2_0 

1108 if y < 0: 

1109 r = -r 

1110 else: # y == 0 

1111 r = PI if x < 0 else _0_0 

1112 return Lam(Vermeille=r) 

1113 

1114 @Property_RO 

1115 def latlon(self): 

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

1117 ''' 

1118 return LatLon2Tuple(self.lat, self.lon, name=self.name) 

1119 

1120 @Property_RO 

1121 def latlonheight(self): 

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

1123 ''' 

1124 return self.latlon.to3Tuple(self.height) 

1125 

1126 @Property_RO 

1127 def latlonheightdatum(self): 

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

1129 ''' 

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

1131 

1132 @Property_RO 

1133 def latlonVermeille(self): 

1134 '''Get the latitude and I{Vermeille} longitude in C{degrees [-225..+225]} (L{LatLon2Tuple}C{(lat, lon)}). 

1135 

1136 @see: Property C{lonVermeille}. 

1137 ''' 

1138 v = self.lonVermeille 

1139 return LatLon2Tuple(self.lat, v, name=self.name + v.name) 

1140 

1141 @Property_RO 

1142 def lonVermeille(self): 

1143 '''Get the longitude in C{degrees [-225..+225]} after U{Vermeille 

1144 <https://Search.ProQuest.com/docview/639493848>} 2004, page 95. 

1145 

1146 @see: Property C{lamVermeille}. 

1147 ''' 

1148 return Lon(Vermeille=degrees(self.lamVermeille)) 

1149 

1150 @Property_RO 

1151 def Mx(self): 

1152 '''Compute rotation matrix (L{EcefMatrix}), seperate from C{M}. 

1153 ''' 

1154 sa, ca, sb, cb, _, _, _ = _norm7(self.y, self.x, self.z, self.ellipsoid) 

1155 return EcefMatrix(sa, ca, sb, cb, name=self.name) 

1156 

1157 @Property_RO 

1158 def phi(self): 

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

1160 ''' 

1161 return self.philam.phi 

1162 

1163 @Property_RO 

1164 def philam(self): 

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

1166 ''' 

1167 return PhiLam2Tuple(radians(self.lat), radians(self.lon), name=self.name) 

1168 

1169 @Property_RO 

1170 def philamheight(self): 

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

1172 ''' 

1173 return self.philam.to3Tuple(self.height) 

1174 

1175 @Property_RO 

1176 def philamheightdatum(self): 

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

1178 ''' 

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

1180 

1181 @Property_RO 

1182 def philamVermeille(self): 

1183 '''Get the latitude and I{Vermeille} longitude in C{radians [-PI*3/2..+PI*3/2]} (L{PhiLam2Tuple}C{(phi, lam)}). 

1184 

1185 @see: Property C{lamVermeille}. 

1186 ''' 

1187 v = self.lamVermeille 

1188 return PhiLam2Tuple(radians(self.lat), v, name=self.name + v.name) 

1189 

1190 phiVermeille = phi 

1191 

1192 def toCartesian(self, Cartesian=None, **Cartesian_kwds): 

1193 '''Return the geocentric C{(x, y, z)} coordinates as an ellipsoidal or spherical 

1194 C{Cartesian}. 

1195 

1196 @kwarg Cartesian: Optional class to return C{(x, y, z)} (L{ellipsoidalKarney.Cartesian}, 

1197 L{ellipsoidalNvector.Cartesian}, L{ellipsoidalVincenty.Cartesian}, 

1198 L{sphericalNvector.Cartesian} or L{sphericalTrigonometry.Cartesian}) 

1199 or C{None}. 

1200 @kwarg Cartesian_kwds: Optionally, additional B{C{Cartesian}} keyword arguments, ignored 

1201 if C{B{Cartesian} is None}. 

1202 

1203 @return: A B{C{Cartesian}} instance or a L{Vector4Tuple}C{(x, y, z, h)} if C{B{Cartesian} 

1204 is None}. 

1205 

1206 @raise TypeError: Invalid B{C{Cartesian}} or B{C{Cartesian_kwds}} item. 

1207 ''' 

1208 if _isin(Cartesian, None, Vector4Tuple): 

1209 r = self.xyzh 

1210 elif Cartesian is Vector3Tuple: 

1211 r = self.xyz 

1212 else: 

1213 _xsubclassof(self._CartesianBase, Cartesian=Cartesian) 

1214 r = Cartesian(self, **_name1__(Cartesian_kwds, _or_nameof=self)) 

1215 return r 

1216 

1217 def toDatum(self, datum2, **name): 

1218 '''Convert this C{Ecef9Tuple} to an other datum. 

1219 

1220 @arg datum2: Datum to convert I{to} (L{Datum}). 

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

1222 

1223 @return: The converted 9-Tuple (C{Ecef9Tuple}). 

1224 

1225 @raise TypeError: The B{C{datum2}} is not a L{Datum}. 

1226 ''' 

1227 n = _name__(name, _or_nameof=self) 

1228 if _isin(self.datum, None, datum2): # PYCHOK _Names_ 

1229 r = self.copy(name=n) 

1230 else: 

1231 c = self._CartesianBase(self, datum=self.datum, name=n) # PYCHOK _Names_ 

1232 # c.toLatLon converts datum, x, y, z, lat, lon, etc. 

1233 # and returns another Ecef9Tuple iff LatLon is None 

1234 r = c.toLatLon(datum=datum2, LatLon=None) 

1235 return r 

1236 

1237 def toLatLon(self, LatLon=None, **LatLon_kwds): 

1238 '''Return the geodetic C{(lat, lon, height[, datum])} coordinates. 

1239 

1240 @kwarg LatLon: Optional class to return C{(lat, lon, height[, datum])} or C{None}. 

1241 @kwarg LatLon_kwds: Optional B{C{height}}, B{C{datum}} and other B{C{LatLon}} 

1242 keyword arguments. 

1243 

1244 @return: A B{C{LatLon}} instance or if C{B{LatLon} is None}, a L{LatLon4Tuple}C{(lat, 

1245 lon, height, datum)} or L{LatLon3Tuple}C{(lat, lon, height)} if C{datum} is 

1246 specified or not. 

1247 

1248 @raise TypeError: Invalid B{C{LatLon}} or B{C{LatLon_kwds}} item. 

1249 ''' 

1250 lat, lon, D = self.lat, self.lon, self.datum # PYCHOK Ecef9Tuple 

1251 kwds = _name1__(LatLon_kwds, _or_nameof=self) 

1252 kwds = _xkwds(kwds, height=self.height, datum=D) # PYCHOK Ecef9Tuple 

1253 d = kwds.get(_datum_, LatLon) 

1254 if LatLon is None: 

1255 r = LatLon3Tuple(lat, lon, kwds[_height_], name=kwds[_name_]) 

1256 if d is not None: 

1257 # assert d is not LatLon 

1258 r = r.to4Tuple(d) # checks type(d) 

1259 else: 

1260 if d is None: 

1261 _ = kwds.pop(_datum_) # remove None datum 

1262 r = LatLon(lat, lon, **kwds) 

1263 _xdatum(_xattr(r, datum=D), D) 

1264 return r 

1265 

1266 def toVector(self, Vector=None, **Vector_kwds): 

1267 '''Return these geocentric C{(x, y, z)} coordinates as vector. 

1268 

1269 @kwarg Vector: Optional vector class to return C{(x, y, z)} or C{None}. 

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

1271 ignored if C{B{Vector} is None}. 

1272 

1273 @return: A B{C{Vector}} instance or a L{Vector3Tuple}C{(x, y, z)} if 

1274 C{B{Vector} is None}. 

1275 

1276 @raise TypeError: Invalid B{C{Vector}} or B{C{Vector_kwds}} item. 

1277 

1278 @see: Propertes C{xyz} and C{xyzh} 

1279 ''' 

1280 return self.xyz if Vector is None else Vector( 

1281 *self.xyz, **_name1__(Vector_kwds, _or_nameof=self)) # PYCHOK Ecef9Tuple 

1282 

1283# def _T_x_M(self, T): 

1284# '''(INTERNAL) Update M{self.M = T.multiply(self.M)}. 

1285# ''' 

1286# return self.dup(M=T.multiply(self.M)) 

1287 

1288 @Property_RO 

1289 def xyz(self): 

1290 '''Get the geocentric C{(x, y, z)} coordinates (L{Vector3Tuple}C{(x, y, z)}). 

1291 ''' 

1292 return Vector3Tuple(self.x, self.y, self.z, name=self.name) 

1293 

1294 @Property_RO 

1295 def xyzh(self): 

1296 '''Get the geocentric C{(x, y, z)} coordinates and C{height} (L{Vector4Tuple}C{(x, y, z, h)}) 

1297 ''' 

1298 return self.xyz.to4Tuple(self.height) 

1299 

1300 

1301def _4Ecef(this, Ecef): # in .datums.Datum.ecef, .ellipsoids.Ellipsoid.ecef 

1302 '''Return an ECEF converter for C{this} L{Datum} or L{Ellipsoid}. 

1303 ''' 

1304 if Ecef is None: 

1305 Ecef = EcefKarney 

1306 else: 

1307 _xinstanceof(*_Ecefs, Ecef=Ecef) 

1308 return Ecef(this, name=this.name) 

1309 

1310 

1311def _equatorial2(E, p, z): 

1312 '''(INTERNAL) Equatorial plane from C{EcefKarney}. 

1313 ''' 

1314 # Treat prolate spheroids by swapping p and z here and by 

1315 # switching the arguments to phi = atan2(...) at the end 

1316 # of method C{EcefKarney._reverse5} 

1317 p = (p / E.a)**2 

1318 z = (z / E.a)**2 * E.e21 

1319 return (z, p) if E.f < 0 else (p, z) 

1320 

1321 

1322def _equatorial4(E, s, z): 

1323 '''(INTERNAL) C{C=3}, near equatorial plane from C{EcefKarney}. 

1324 ''' 

1325 t = E.e4 - s 

1326 if E.f < 0: 

1327 s, t = t, s 

1328 e = E.a 

1329 else: 

1330 e = E.b2_a 

1331 sa, ca, h = _norm3(*map1(sqrt, E._1_e21 * t, s)) 

1332 if z < 0: # for tiny negative z, not for prolate 

1333 sa = neg(sa) 

1334 h *= neg(e / E.e2abs) 

1335 return sa, ca, h, 3 # equatorial 

1336 

1337 

1338def _heightB(E, sa, ca, z, p): # in ecef.EcefSudano, ecec.EcefVeness 

1339 '''(INTERNAL) Height above ellipsoid (Bowring eqn 7) at C{lat}. 

1340 ''' 

1341 # sa, ca = sincos2d(lat) 

1342 # p = hypot(x, y) # distance to polar axis 

1343 

1344 # r = a / E.e2s(sa) # length of normal terminated by polar axis 

1345 # h = p * ca + z * sa - (a * a / r) 

1346 return (p * ca + fabs(z * sa) - E.a * E.e2s(sa)) if sa else (p - E.a) 

1347 

1348 

1349def _latB2(E, s, c, z, p): 

1350 '''(INTERNAL) Geodetic latitude, Bowring eqn 18. 

1351 ''' 

1352 sa, ca, _ = _norm3(z + s**3 * E.b * E.e22, 

1353 p - c**3 * E.a * E.e2) 

1354 return sa, ca 

1355 

1356 

1357def _llhn4(latlonh, lon, height, suffix=NN, Error=EcefError, **name): # in .ltp 

1358 '''(INTERNAL) Get a C{(lat, lon, h, name)} 4-tuple. 

1359 ''' 

1360 try: 

1361 lat, lon = latlonh.lat, latlonh.lon 

1362 h = _xattr(latlonh, height=_xattr(latlonh, h=height)) 

1363 n = _name__(name, _or_nameof=latlonh) # == latlonh._name__(name) 

1364 except AttributeError: 

1365 lat, h, n = latlonh, height, _name__(**name) 

1366 try: 

1367 return Lat(lat), Lon(lon), Height(h), n 

1368 except (TypeError, ValueError) as x: 

1369 t = _lat_, _lon_, _height_ 

1370 if suffix: 

1371 t = (_ + suffix for _ in t) 

1372 d = dict(zip(t, (lat, lon, h))) 

1373 raise Error(cause=x, **d) 

1374 

1375 

1376def _Lon00(lon00): 

1377 '''(INTERNAL) Return C{lon00}. 

1378 ''' 

1379 return Lon(lon00=lon00, clip=0) 

1380 

1381 

1382def _norm3(y, x, eps=0): 

1383 '''(INTERNAL) Return C{y, x, h} normalized. 

1384 ''' 

1385 h = hypot(y, x) # EPS0, EPS_2 

1386 return (y / h, x / h, h) if h > eps else (_0_0, _1_0, h) # copysign_1_0(x) 

1387 

1388 

1389def _norm7(y, x, z=0, E=_EWGS84): 

1390 '''(INTERNAL) Return C{phi, lam, h, p, C}. 

1391 ''' 

1392 sb, cb, p = _norm3(y, x) # lam, distance to polar axis 

1393 sa, ca, h = _norm3(z, p) # phi, distance to earth center 

1394 if h > E.heightMax: 

1395 # We are really far away (> 12M light years). Treat the earth 

1396 # as a point and h above as an acceptable approximation to the 

1397 # height. This avoids overflow, e.g., in the computation of d 

1398 # below. It's possible that h has overflowed to INF, that's OK. 

1399 # Treat finite x, y, but p overflows to +INF by scaling by 2. 

1400 sb, cb, p = _norm3(y * _0_5, x * _0_5) 

1401 sa, ca, _ = _norm3(z * _0_5, p) 

1402 C = 5 # near polar 

1403 else: 

1404 C = None # tbd 

1405 return sa, ca, sb, cb, h, p, C 

1406 

1407 

1408def _subterrestrial4(E, z, p): 

1409 '''(INTERNAL) Case C{C=6}, too subterrestrial. 

1410 ''' 

1411 sa, ca, h = _norm3(z, p) # like _norm7 

1412 h -= E.b if fabs(z) > p else E.a 

1413 return sa, ca, h, 6 # near center 

1414 

1415 

1416def _xEcef(Ecef): # PYCHOK .latlonBase 

1417 '''(INTERNAL) Validate B{C{Ecef}} I{class}. 

1418 ''' 

1419 if issubclassof(Ecef, _EcefBase): 

1420 return Ecef 

1421 raise _TypesError(_Ecef_, Ecef, *_Ecefs) 

1422 

1423 

1424# kwd lon00 unused but will throw a TypeError if misspelled, etc. 

1425def _xyzn4(xyz, y, z, Types, Error=EcefError, lon00=0, # PYCHOK unused 

1426 _xyz_y_z_names=_xyz_y_z, **name): # in .ltp, .pychlv 

1427 '''(INTERNAL) Get an C{(x, y, z, name)} 4-tuple. 

1428 ''' 

1429 try: 

1430 n = _name__(name, _or_nameof=xyz) # == xyz._name__(name) 

1431 try: 

1432 t = xyz.x, xyz.y, xyz.z, n 

1433 if not isinstance(xyz, Types): 

1434 raise _TypesError(_xyz_y_z_names[0], xyz, *Types) 

1435 except AttributeError: 

1436 t = map1(float, xyz, y, z) + (n,) 

1437 except (TypeError, ValueError) as x: 

1438 d = dict(zip(_xyz_y_z_names, (xyz, y, z))) 

1439 raise Error(cause=x, **d) 

1440 return t 

1441# assert _xyz_y_z == _args_kwds_names(_xyzn4)[:3] 

1442 

1443 

1444_Ecefs = tuple(_ for _ in locals().values() 

1445 if issubclassof(_, _EcefBase) and 

1446 _ is not _EcefBase) 

1447__all__ += _ALL_DOCS(_EcefBase) 

1448 

1449# **) MIT License 

1450# 

1451# Copyright (C) 2016-2027 -- mrJean1 at Gmail -- All Rights Reserved. 

1452# 

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

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

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

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

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

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

1459# 

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

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

1462# 

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

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

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

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

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

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

1469# OTHER DEALINGS IN THE SOFTWARE.