Coverage for pygeodesy/ecef.py: 95%

454 statements  

« prev     ^ index     » next       coverage.py v7.2.2, created at 2024-05-25 12:04 -0400

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>} 

8into pure Python class L{EcefKarney}, class L{EcefSudano} based on I{John Sudano}'s 

9U{paper<https://www.ResearchGate.net/publication/ 

103709199_An_exact_conversion_from_an_Earth-centered_coordinate_system_to_latitude_longitude_and_altitude>}, 

11class L{EcefVeness} transcoded from I{Chris Veness}' JavaScript classes U{LatLonEllipsoidal, 

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

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

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

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

16 

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

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

19 

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

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

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

23 

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

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

26 

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

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

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

30 

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

32 

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

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

35(2002) 76, page 451-454. 

36 

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

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

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

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

41in U{I{An analytical method to transform geocentric into geodetic coordinates} 

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

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

44 

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

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

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

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

49 

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

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

52C{lon00} to configure that value. 

53 

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

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

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

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

58''' 

59 

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

61 _xinstanceof, _xsubclassof 

62from pygeodesy.constants import EPS, EPS0, EPS02, EPS1, EPS2, EPS_2, INT0, PI, PI_2, \ 

63 _0_0, _0_0001, _0_01, _0_5, _1_0, _1_0_1T, _N_1_0, \ 

64 _2_0, _N_2_0, _3_0, _4_0, _6_0, _60_0, _90_0, _N_90_0, \ 

65 _100_0, _copysign_1_0, isnon0 # PYCHOK used! 

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

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

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

69 _xattr, _xdatum, _xkwds, _xkwds_get 

70from pygeodesy.fmath import cbrt, fdot, Fpowers, hypot, hypot1, hypot2_, sqrt0 

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

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

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

74 _x_, _xyz_, _y_, _z_ 

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

76from pygeodesy.named import _name__, _NamedBase, _NamedTuple, _Pass, _xnamed 

77from pygeodesy.namedTuples import LatLon2Tuple, LatLon3Tuple, \ 

78 PhiLam2Tuple, Vector3Tuple, Vector4Tuple 

79from pygeodesy.props import deprecated_method, Property_RO, property_RO, property_doc_ 

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

81from pygeodesy.units import _isRadius, Degrees, Height, Int, Lam, Lat, Lon, Meter, \ 

82 Phi, Scalar, Scalar_ 

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

84 sincos2, sincos2_, sincos2d, sincos2d_ 

85 

86from math import atan2, cos, degrees, fabs, radians, sqrt 

87 

88__all__ = _ALL_LAZY.ecef 

89__version__ = '24.05.21' 

90 

91_Ecef_ = 'Ecef' 

92_prolate_ = 'prolate' 

93_TRIPS = 33 # 8..9 sufficient, EcefSudano.reverse 

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

95 

96 

97class EcefError(_ValueError): 

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

99 ''' 

100 pass 

101 

102 

103class _EcefBase(_NamedBase): 

104 '''(INTERNAL) Base class for L{EcefFarrell21}, L{EcefFarrell22}, L{EcefKarney}, 

105 L{EcefSudano}, L{EcefVeness} and L{EcefYou}. 

106 ''' 

107 _datum = _WGS84 

108 _E = _EWGS84 

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

110 

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

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

113 

114 @arg a_ellipsoid: A (non-prolate) ellipsoid (L{Ellipsoid}, L{Ellipsoid2}, 

115 L{Datum} or L{a_f2Tuple}) or C{scalar} ellipsoid's 

116 equatorial radius (C{meter}). 

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

118 for C{scalar} B{C{a_ellipsoid}}, C{B{f}=0} represents a 

119 sphere, negative B{C{f}} a prolate ellipsoid. 

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

121 C{reverse} method. 

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

123 

124 @raise EcefError: If B{C{a_ellipsoid}} not L{Ellipsoid}, L{Ellipsoid2}, 

125 L{Datum} or L{a_f2Tuple} or C{scalar} or B{C{f}} not 

126 C{scalar} or if C{scalar} B{C{a_ellipsoid}} not positive 

127 or B{C{f}} not less than 1.0. 

128 ''' 

129 try: 

130 E = a_ellipsoid 

131 if f is None: 

132 pass 

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

134 E = a_f2Tuple(E, f) 

135 else: 

136 raise ValueError # _invalid_ 

137 

138 if E not in (_EWGS84, _WGS84): 

139 d = _ellipsoidal_datum(E, **name) 

140 E = d.ellipsoid 

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

142 raise ValueError # _invalid_ 

143 self._datum = d 

144 self._E = E 

145 

146 except (TypeError, ValueError) as x: 

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

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

149 

150 if name: 

151 self.name = name 

152 if lon00 is not INT0: 

153 self.lon00 = lon00 

154 

155 def __eq__(self, other): 

156 '''Compare this and an other Ecef. 

157 

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

159 

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

161 ''' 

162 return other is self or (isinstance(other, self.__class__) and 

163 other.ellipsoid == self.ellipsoid) 

164 

165 @Property_RO 

166 def datum(self): 

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

168 ''' 

169 return self._datum 

170 

171 @Property_RO 

172 def ellipsoid(self): 

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

174 ''' 

175 return self._E 

176 

177 @Property_RO 

178 def equatoradius(self): 

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

180 ''' 

181 return self.ellipsoid.a 

182 

183 a = equatorialRadius = equatoradius # Karney property 

184 

185 @Property_RO 

186 def flattening(self): # Karney property 

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

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

189 ''' 

190 return self.ellipsoid.f 

191 

192 f = flattening 

193 

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

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

196 ''' 

197 if _philam: # lat, lon in radians 

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

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

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

201 else: 

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

203 

204 E = self.ellipsoid 

205 n = E.roc1_(sa, ca) if self._isYou else E.roc1_(sa) 

206 z = (h + n * E.e21) * sa 

207 x = (h + n) * ca 

208 

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

210 return Ecef9Tuple(x * cb, x * sb, z, lat, lon, h, 

211 0, m, self.datum, 

212 name=name or self.name) 

213 

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

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

216 

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

218 latitude (C{degrees}). 

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

220 (C{degrees}). 

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

222 the surface of the ellipsoid. 

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

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

225 

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

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

228 C{(lat, lon, height)}, case C{C} 0, optional C{M} (L{EcefMatrix}) 

229 and C{datum} if available. 

230 

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

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

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

234 

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

236 and avoid double angle conversions. 

237 ''' 

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

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

240 

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

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

243 in I{radians}. 

244 

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

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

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

248 the surface of the ellipsoid. 

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

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

251 

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

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

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

255 

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

257 ''' 

258 try: # like function C{_llhn4} below 

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

260 except (TypeError, ValueError) as x: 

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

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

263 

264 @property_RO 

265 def _Geocentrics(self): 

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

267 ''' 

268 _EcefBase._Geocentrics = t = (Ecef9Tuple, # overwrite property_RO 

269 _MODS.cartesianBase.CartesianBase) 

270 return t 

271 

272 @Property_RO 

273 def _isYou(self): 

274 '''(INTERNAL) Is this an C{EcefYou}?. 

275 ''' 

276 return isinstance(self, EcefYou) 

277 

278 @property 

279 def lon00(self): 

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

281 ''' 

282 return self._lon00 

283 

284 @lon00.setter # PYCHOK setter! 

285 def lon00(self, lon00): 

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

287 ''' 

288 self._lon00 = Degrees(lon00=lon00) 

289 

290 def _Matrix(self, sa, ca, sb, cb): 

291 '''Creation a rotation matrix. 

292 

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

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

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

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

297 

298 @return: An L{EcefMatrix}. 

299 ''' 

300 return self._xnamed(EcefMatrix(sa, ca, sb, cb)) 

301 

302 def _polon(self, y, x, R, **name_lon00): 

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

304 ''' 

305 return atan2d(y, x) if R else _xkwds_get(name_lon00, lon00=self.lon00) 

306 

307 def reverse(self, xyz, y=None, z=None, M=False, **name_lon00): # PYCHOK no cover 

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

309 self._notOverloaded(xyz, y=y, z=z, M=M, **name_lon00) 

310 

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

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

313 

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

315 

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

317 ''' 

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

319 

320 

321class EcefFarrell21(_EcefBase): 

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

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

324 books?id=fW4foWASY6wC>}, page 29. 

325 ''' 

326 

327 def reverse(self, xyz, y=None, z=None, M=None, **name_lon00): # PYCHOK unused M 

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

329 I{Farrell}'s U{Table 2.1<https://Books.Google.com/books?id=fW4foWASY6wC>}, 

330 page 29. 

331 

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

333 coordinate (C{meter}). 

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

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

336 @kwarg M: I{Ignored}, rotation matrix C{M} not available. 

337 @kwarg name_lon00: Optional keyword arguments C{B{name}=NN} (C{str}) and 

338 I{"polar"} longitude C{B{lon00}=INT0} (C{degrees}), overriding 

339 the default and property C{lon00} setting and returned if 

340 C{B{x}=0} and C{B{y}=0}. 

341 

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

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

344 ones C{(x, y, z)}, case C{C=1}, C{M=None} always and C{datum} 

345 if available. 

346 

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

348 not C{scalar} for C{scalar} B{C{xyz}} or C{sqrt} domain or 

349 zero division error. 

350 

351 @see: L{EcefFarrell22} and L{EcefVeness}. 

352 ''' 

353 x, y, z, name = _xyzn4(xyz, y, z, self._Geocentrics, **name_lon00) 

354 

355 E = self.ellipsoid 

356 a = E.a 

357 a2 = E.a2 

358 b2 = E.b2 

359 e2 = E.e2 

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

361 e4 = E.e4 

362 

363 try: # names as page 29 

364 z2 = z**2 

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

366 

367 p = hypot(x, y) 

368 p2 = p**2 

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

370 F = b2 * z2 * 54 

371 t = e4 * p2 * F / G**3 

372 t = cbrt(sqrt(t * (t + _2_0)) + t + _1_0) 

373 G *= fsumf_(t , _1_0, _1_0 / t) 

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

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

376 Q1 = Q + _1_0 

377 r0 = P * p * e2 / Q1 - sqrt(fsumf_(a2 * (Q1 / Q) * _0_5, 

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

379 -P * p2 * _0_5)) 

380 r = p + e2 * r0 

381 v = b2 / (sqrt(r**2 + ez) * a) 

382 

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

384 lat = atan1d((e2_ * v + _1_0) * z, p) 

385 lon = self._polon(y, x, p, **name_lon00) 

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

387 

388 except (ValueError, ZeroDivisionError) as e: 

389 raise EcefError(x=x, y=y, z=z, cause=e) 

390 

391 return Ecef9Tuple(x, y, z, lat, lon, h, 

392 1, None, self.datum, 

393 name=name or self.name) 

394 

395 

396class EcefFarrell22(_EcefBase): 

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

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

399 books?id=fW4foWASY6wC>}, page 30. 

400 ''' 

401 

402 def reverse(self, xyz, y=None, z=None, M=None, **name_lon00): # PYCHOK unused M 

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

404 I{Farrell}'s U{Table 2.2<https://Books.Google.com/books?id=fW4foWASY6wC>}, 

405 page 30. 

406 

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

408 coordinate (C{meter}). 

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

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

411 @kwarg M: I{Ignored}, rotation matrix C{M} not available. 

412 @kwarg name_lon00: Optional keyword arguments C{B{name}=NN} (C{str}) and 

413 I{"polar"} longitude C{B{lon00}=INT0} (C{degrees}), overriding 

414 the default and property C{lon00} setting and returned in case 

415 C{B{x}=0} and C{B{y}=0}. 

416 

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

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

419 ones C{(x, y, z)}, case C{C=1}, C{M=None} always and C{datum} 

420 if available. 

421 

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

423 not C{scalar} for C{scalar} B{C{xyz}} or C{sqrt} domain or 

424 zero division error. 

425 

426 @see: L{EcefFarrell21} and L{EcefVeness}. 

427 ''' 

428 x, y, z, name = _xyzn4(xyz, y, z, self._Geocentrics, **name_lon00) 

429 

430 E = self.ellipsoid 

431 a = E.a 

432 b = E.b 

433 

434 try: # see EcefVeness.reverse 

435 p = hypot(x, y) 

436 lon = self._polon(y, x, p, **name_lon00) 

437 

438 s, c = sincos2(atan2(z * a, p * b)) # == _norm3 

439 lat = atan1d(z + s**3 * b * E.e22, 

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

441 

442 s, c = sincos2d(lat) 

443 if c: # E.roc1_(s) = E.a / sqrt(1 - E.e2 * s**2) 

444 h = p / c - (E.roc1_(s) if s else a) 

445 else: # polar 

446 h = fabs(z) - b 

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

448 

449 except (ValueError, ZeroDivisionError) as e: 

450 raise EcefError(x=x, y=y, z=z, cause=e) 

451 

452 return Ecef9Tuple(x, y, z, lat, lon, h, 

453 1, None, self.datum, 

454 name=name or self.name) 

455 

456 

457class EcefKarney(_EcefBase): 

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

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

460 C++/doc/classGeographicLib_1_1Geocentric.html>} methods. 

461 

462 @note: On methods C{.forward} and C{.forwar_}, let C{v} be a unit vector located 

463 at C{(lat, lon, h)}. We can express C{v} as column vectors in one of two 

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

465 relative to a local coordinate system at C{C(lat0, lon0, h0)}) or as C{v0} 

466 in geocentric C{x, y, z} coordinates. Then, M{v0 = M ⋅ v1} where C{M} is 

467 the rotation matrix. 

468 ''' 

469 

470 @Property_RO 

471 def hmax(self): 

472 '''Get the distance or height limit (C{meter}, conventionally). 

473 ''' 

474 return self.equatoradius / EPS_2 # self.equatoradius * _2_EPS, 12M lighyears 

475 

476 def reverse(self, xyz, y=None, z=None, M=False, **name_lon00): 

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

478 

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

480 coordinate (C{meter}). 

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

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

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

484 @kwarg name_lon00: Optional keyword arguments C{B{name}=NN} (C{str}) and 

485 I{"polar"} longitude C{B{lon00}=INT0} (C{degrees}), overriding 

486 the default and property C{lon00} setting and returned in case 

487 C{B{x}=0} and C{B{y}=0}. 

488 

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

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

491 ones C{(x, y, z)}, case C{C}, optional C{M} (L{EcefMatrix}) and 

492 C{datum} if available. 

493 

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

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

496 

497 @note: In general, there are multiple solutions and the result which minimizes 

498 C{height} is returned, i.e., the C{(lat, lon)} corresponding to the 

499 closest point on the ellipsoid. If there are still multiple solutions 

500 with different latitudes (applies only if C{z} = 0), then the solution 

501 with C{lat} > 0 is returned. If there are still multiple solutions with 

502 different longitudes (applies only if C{x} = C{y} = 0), then C{lon00} is 

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

504 is not below M{−E.a * (1 − E.e2) / sqrt(1 − E.e2 * sin(lat)**2)}. Like 

505 C{forward} above, M{v1 = Transpose(M) ⋅ v0}. 

506 ''' 

507 def _norm3(y, x): 

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

509 return (y / h, x / h, h) if h > 0 else (_0_0, _1_0, h) 

510 

511 x, y, z, name = _xyzn4(xyz, y, z, self._Geocentrics, **name_lon00) 

512 

513 E = self.ellipsoid 

514 f = E.f 

515 

516 sb, cb, R = _norm3(y, x) 

517 h = hypot(R, z) # distance to earth center 

518 if h > self.hmax: # PYCHOK no cover 

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

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

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

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

523 # Treat finite x, y, but R overflows to +INF by scaling by 2. 

524 sb, cb, R = _norm3(y * _0_5, x * _0_5) 

525 sa, ca, _ = _norm3(z * _0_5, R) 

526 C = 1 

527 

528 elif E.e4: # E.isEllipsoidal 

529 # Treat prolate spheroids by swapping R and Z here and by 

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

531 p = (R / E.a)**2 

532 q = (z / E.a)**2 * E.e21 

533 if f < 0: 

534 p, q = q, p 

535 r = fsumf_(p, q, -E.e4) 

536 e = E.e4 * q 

537 if e or r > 0: 

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

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

540 s = d = e * p / _4_0 # s = r^3 * s 

541 u = r = r / _6_0 

542 r2 = r**2 

543 r3 = r2 * r 

544 t3 = r3 + s 

545 d *= t3 + r3 

546 if d < 0: 

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

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

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

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

551 else: 

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

553 # minimizes loss of precision due to cancellation. The 

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

555 # in definition of u. 

556 if d > 0: 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

571 if f < 0: 

572 k1 -= E.e2 

573 else: 

574 k2 += E.e2 

575 sa, ca, h = _norm3(z / k1, R / k2) 

576 h *= k1 - E.e21 

577 C = 2 

578 

579 else: # e = E.e4 * q == 0 and r <= 0 

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

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

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

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

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

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

586 q = E.e4 - p 

587 if f < 0: 

588 p, q = q, p 

589 e = E.a 

590 else: 

591 e = E.b2_a 

592 sa, ca, h = _norm3(sqrt(q * E._1_e21), sqrt(p)) 

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

594 sa = neg(sa) 

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

596 C = 3 

597 

598 else: # E.e4 == 0, spherical case 

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

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

601 sa, ca, _ = _norm3((z if h else _1_0), R) 

602 h -= E.a 

603 C = 4 

604 

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

606 lon = self._polon(sb, cb, R, **name_lon00) 

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

608 return Ecef9Tuple(x, y, z, atan1d(sa, ca), lon, h, 

609 C, m, self.datum, 

610 name=name or self.name) 

611 

612 

613class EcefSudano(_EcefBase): 

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

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

616 ''' 

617 _tol = EPS2 

618 

619 def reverse(self, xyz, y=None, z=None, M=None, **name_lon00): # PYCHOK unused M 

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

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

622 

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

624 coordinate (C{meter}). 

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

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

627 @kwarg M: I{Ignored}, rotation matrix C{M} not available. 

628 @kwarg name_lon00: Optional keyword arguments C{B{name}=NN} (C{str}) and 

629 I{"polar"} longitude C{B{lon00}=INT0} (C{degrees}), overriding 

630 the default and property C{lon00} setting and returned in case 

631 C{B{x}=0} and C{B{y}=0}. 

632 

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

634 coordinates C{(lat, lon, height)} for the given geocentric ones C{(x, y, z)}, 

635 iteration C{C}, C{M=None} always and C{datum} if available. 

636 

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

638 not C{scalar} for C{scalar} B{C{xyz}} or no convergence. 

639 ''' 

640 x, y, z, name = _xyzn4(xyz, y, z, self._Geocentrics, **name_lon00) 

641 

642 E = self.ellipsoid 

643 e = E.e2 * E.a 

644 R = hypot(x, y) # Rh 

645 d = e - R 

646 

647 _a = fabs 

648 lat = atan1d(z, R * E.e21) 

649 sa, ca = sincos2d(_a(lat)) 

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

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

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

653 # = ca**2 * (z * ca + E.e2 * E.a * sa - R * sa) 

654 # = ca**2 * (z * ca + (E.e2 * E.a - R) * sa) 

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

656 # = ca**2 * (E.e2 * E.a / E.e2s2(sa) - R / ca**2) 

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

658 # (E.e2 * E.a / E.e2s2(sa) - R / ca**2) 

659 _E = EPS_2 

660 tol = self.tolerance 

661 _S2 = Fsum(sa).fsum2f_ 

662 _rt = sqrt 

663 for i in range(1, _TRIPS): 

664 ca2 = _1_0 - sa**2 

665 if ca2 < _E: # PYCHOK no cover 

666 ca = _0_0 

667 break 

668 ca = _rt(ca2) 

669 r = e / E.e2s2(sa) - R / ca2 

670 if _a(r) < _E: 

671 break 

672 lat = None 

673 sa, r = _S2(-z * ca / r, -d * sa / r) 

674 if _a(r) < tol: 

675 break 

676 else: 

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

678 raise EcefError(t, txt=Fmt.no_convergence(r, tol)) 

679 

680 if lat is None: 

681 lat = copysign0(atan1d(_a(sa), ca), z) 

682 lon = self._polon(y, x, R, **name_lon00) 

683 

684 h = fsumf_(R * ca, _a(z * sa), -E.a * E.e2s(sa)) # use Veness' 

685 # because Sudano's Eq (7) doesn't produce the correct height 

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

687 return Ecef9Tuple(x, y, z, lat, lon, h, 

688 i, None, self.datum, # C=i, M=None 

689 iteration=i, name=name or self.name) 

690 

691 @property_doc_(''' the convergence tolerance (C{float}).''') 

692 def tolerance(self): 

693 '''Get the convergence tolerance (C{scalar}). 

694 ''' 

695 return self._tol 

696 

697 @tolerance.setter # PYCHOK setter! 

698 def tolerance(self, tol): 

699 '''Set the convergence tolerance (C{scalar}). 

700 

701 @raise EcefError: Non-scalar or invalid B{C{tol}}. 

702 ''' 

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

704 

705 

706class EcefVeness(_EcefBase): 

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

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

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

710 

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

712 documents/resources/guide-coordinate-systems-great-britain.pdf>}, section I{B) Converting 

713 between 3D Cartesian and ellipsoidal latitude, longitude and height coordinates}. 

714 ''' 

715 

716 def reverse(self, xyz, y=None, z=None, M=None, **name_lon00): # PYCHOK unused M 

717 '''Conversion from geocentric C{(x, y, z)} to geodetic C{(lat, lon, height)} 

718 transcoded from I{Chris Veness}' U{JavaScript<https://www.Movable-Type.co.UK/ 

719 scripts/geodesy/docs/latlon-ellipsoidal.js.html>}. 

720 

721 Uses B. R. Bowring’s formulation for μm precision in concise form U{I{The accuracy 

722 of geodetic latitude and height equations}<https://www.ResearchGate.net/publication/ 

723 233668213>}, Survey Review, Vol 28, 218, Oct 1985. 

724 

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

726 coordinate (C{meter}). 

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

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

729 @kwarg M: I{Ignored}, rotation matrix C{M} not available. 

730 @kwarg name_lon00: Optional keyword arguments C{B{name}=NN} (C{str}) and 

731 I{"polar"} longitude C{B{lon00}=INT0} (C{degrees}), overriding 

732 the default and property C{lon00} setting and returned in case 

733 C{B{x}=0} and C{B{y}=0}. 

734 

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

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

737 ones C{(x, y, z)}, case C{C}, C{M=None} always and C{datum} if available. 

738 

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

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

741 

742 @see: Toms, Ralph M. U{I{An Efficient Algorithm for Geocentric to Geodetic 

743 Coordinate Conversion}<https://www.OSTI.gov/scitech/biblio/110235>}, 

744 Sept 1995 and U{I{An Improved Algorithm for Geocentric to Geodetic 

745 Coordinate Conversion}<https://www.OSTI.gov/scitech/servlets/purl/231228>}, 

746 Apr 1996, both from Lawrence Livermore National Laboratory (LLNL) and 

747 Sudano, John J, U{I{An exact conversion from an Earth-centered coordinate 

748 system to latitude longitude and altitude}<https://www.ResearchGate.net/ 

749 publication/3709199>}. 

750 ''' 

751 x, y, z, name = _xyzn4(xyz, y, z, self._Geocentrics, **name_lon00) 

752 

753 E = self.ellipsoid 

754 a = E.a 

755 

756 p = hypot(x, y) # distance from minor axis 

757 r = hypot(p, z) # polar radius 

758 if min(p, r) > EPS0: 

759 b = E.b * E.e22 

760 # parametric latitude (Bowring eqn 17, replaced) 

761 t = (E.b * z) / (a * p) * (_1_0 + b / r) 

762 c = _1_0 / hypot1(t) 

763 s = c * t 

764 # geodetic latitude (Bowring eqn 18) 

765 lat = atan1d(z + s**3 * b, 

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

767 

768 # height above ellipsoid (Bowring eqn 7) 

769 sa, ca = sincos2d(lat) 

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

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

772 h = fsumf_(p * ca, z * sa, -a * E.e2s(sa)) 

773 C = 1 

774 

775 # see <https://GIS.StackExchange.com/questions/28446> 

776 elif p > EPS: # lat arbitrarily zero, equatorial lon 

777 C, lat, h = 2, _0_0, (p - a) 

778 

779 else: # polar lat, lon arbitrarily lon00 

780 C, lat, h = 3, (_N_90_0 if z < 0 else _90_0), (fabs(z) - E.b) 

781 

782 lon = self._polon(y, x, p, **name_lon00) 

783 return Ecef9Tuple(x, y, z, lat, lon, h, 

784 C, None, self.datum, # M=None 

785 name=name or self.name) 

786 

787 

788class EcefYou(_EcefBase): 

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

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

791 for I{non-prolate} ellipsoids. 

792 

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

794 ellipsoidal coordinates}<https://Espace.Curtin.edu.AU/bitstream/handle/20.500.11937/ 

795 11589/115114_9021_geod2ellip_final.pdf>} Studia Geophysica et Geodaetica, 2008, 52, 

796 pages 1-18 and U{PyMap3D <https://PyPI.org/project/pymap3d>}. 

797 ''' 

798 

799 def __init__(self, a_ellipsoid=_EWGS84, f=None, **name_lon00): # PYCHOK signature 

800 _EcefBase.__init__(self, a_ellipsoid, f=f, **name_lon00) # inherited documentation 

801 self._ee2 = EcefYou._ee2(self.ellipsoid) 

802 

803 @staticmethod 

804 def _ee2(E): 

805 e2 = E.a2 - E.b2 

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

807 raise EcefError(ellipsoid=E, txt=_prolate_) 

808 return sqrt0(e2), e2 

809 

810 def reverse(self, xyz, y=None, z=None, M=None, **name_lon00): # PYCHOK unused M 

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

812 using I{Rey-Jer You}'s transformation. 

813 

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

815 coordinate (C{meter}). 

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

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

818 @kwarg M: I{Ignored}, rotation matrix C{M} not available. 

819 @kwarg name_lon00: Optional keyword arguments C{B{name}=NN} (C{str}) and 

820 I{"polar"} longitude C{B{lon00}=INT0} (C{degrees}), overriding 

821 the default and property C{lon00} setting and returned in case 

822 C{B{x}=0} and C{B{y}=0}. 

823 

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

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

826 ones C{(x, y, z)}, case C{C=1}, C{M=None} always and C{datum} if 

827 available. 

828 

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

830 B{C{z}} not C{scalar} for C{scalar} B{C{xyz}} or the 

831 ellipsoid is I{prolate}. 

832 ''' 

833 x, y, z, name = _xyzn4(xyz, y, z, self._Geocentrics, **name_lon00) 

834 

835 E = self.ellipsoid 

836 e, e2 = self._ee2 

837 

838 q = hypot(x, y) # R 

839 u = Fpowers(2, x, y, z) - e2 

840 u = u.fadd_(hypot(u, e * z * _2_0)).fover(_2_0) 

841 if u > EPS02: 

842 u = sqrt(u) 

843 p = hypot(u, e) 

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

845 sB, cB = sincos2(B) 

846 if cB and sB: 

847 p *= E.a 

848 d = (p / cB - e2 * cB) / sB 

849 if isnon0(d): 

850 B += fsumf_(u * E.b, -p, e2) / d 

851 sB, cB = sincos2(B) 

852 elif u < (-EPS2): 

853 raise EcefError(x=x, y=y, z=z, u=u, txt=_singular_) 

854 else: 

855 sB, cB = _copysign_1_0(z), _0_0 

856 

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

858 lon = self._polon(y, x, q, **name_lon00) 

859 

860 h = hypot(z - E.b * sB, q - E.a * cB) 

861 if hypot2_(x, y, z * E.a_b) < E.a2: 

862 h = neg(h) # inside ellipsoid 

863 return Ecef9Tuple(x, y, z, lat, lon, h, 

864 1, None, self.datum, # C=1, M=None 

865 name=name or self.name) 

866 

867 

868class EcefMatrix(_NamedTuple): 

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

870 

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

872 Geographic_coordinate_conversion#From_ECEF_to_ENU>} and 

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

874 ''' 

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

876 '_1_0_', '_1_1_', '_1_2_', 

877 '_2_0_', '_2_1_', '_2_2_') 

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

879 

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

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

882 ''' 

883 _NamedTuple._validate(self, underOK=True) 

884 

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

886 '''New L{EcefMatrix} matrix. 

887 

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

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

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

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

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

893 

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

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

896 ''' 

897 t = sa, ca, sb, cb 

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

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

900 

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

902 raise EcefError(unstr(EcefMatrix.__name__, *t)) 

903 

904 else: # build matrix from the following quaternion operations 

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

906 # or 

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

908 # where 

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

910 

911 # Local X axis (East) in geocentric coords 

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

913 # Local Y axis (North) in geocentric coords 

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

915 # Local Z axis (Up) in geocentric coords 

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

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

918 cb, -sb * sa, sb * ca, 

919 _0_0, ca, sa) 

920 

921 return _NamedTuple.__new__(cls, *t) 

922 

923 def column(self, column): 

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

925 ''' 

926 if 0 <= column < 3: 

927 return self[column::3] 

928 raise _IndexError(column=column) 

929 

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

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

932 

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

934 ''' 

935 return self.classof(*self) 

936 

937 __copy__ = __deepcopy__ = copy 

938 

939 @Property_RO 

940 def matrix3(self): 

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

942 ''' 

943 return tuple(map(self.row, range(3))) 

944 

945 @Property_RO 

946 def matrixTransposed3(self): 

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

948 ''' 

949 return tuple(map(self.column, range(3))) 

950 

951 def multiply(self, other): 

952 '''Matrix multiply M{M0' ⋅ M} this matrix I{Transposed} 

953 with an other matrix. 

954 

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

956 

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

958 

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

960 ''' 

961 _xinstanceof(EcefMatrix, other=other) 

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

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

964 # X = (fdot(self.column(r), *other.column(c)) for r in (0,1,2) for c in (0,1,2)) 

965 X = (fdot(self[r::3], *other[c::3]) for r in range(3) for c in range(3)) 

966 return _xnamed(EcefMatrix(*X), EcefMatrix.multiply.__name__) 

967 

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

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

970 

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

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

973 

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

975 

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

977 ''' 

978 if xyz0: 

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

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

981 xyz = tuple(c - c0 for c, c0 in zip(xyz, xyz0)) 

982 

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

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

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

986 return (fdot(xyz, *self[0::3]), # .column(0) 

987 fdot(xyz, *self[1::3]), # .column(1) 

988 fdot(xyz, *self[2::3])) # .column(2) 

989 

990 def row(self, row): 

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

992 ''' 

993 if 0 <= row < 3: 

994 r = row * 3 

995 return self[r:r+3] 

996 raise _IndexError(row=row) 

997 

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

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

1000 

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

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

1003 

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

1005 

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

1007 ''' 

1008 if xyz0: 

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

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

1011 _xyz = _1_0_1T + xyz 

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

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

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

1015 xyz_ = (fdot(_xyz, xyz0[0], *self[0:3]), # .row(0) 

1016 fdot(_xyz, xyz0[1], *self[3:6]), # .row(1) 

1017 fdot(_xyz, xyz0[2], *self[6:9])) # .row(2) 

1018 else: 

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

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

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

1022 xyz_ = (fdot(xyz, *self[0:3]), # .row(0) 

1023 fdot(xyz, *self[3:6]), # .row(1) 

1024 fdot(xyz, *self[6:9])) # .row(2) 

1025 return xyz_ 

1026 

1027 

1028class Ecef9Tuple(_NamedTuple): 

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

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

1031 C{C} (see the C{Ecef*.reverse} methods) and optionally, the rotation 

1032 matrix C{M} (L{EcefMatrix}) and C{datum}, with C{lat} and C{lon} in 

1033 C{degrees} and C{x}, C{y}, C{z} and C{height} in C{meter}, conventionally. 

1034 ''' 

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

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

1037 

1038 @property_RO 

1039 def _CartesianBase(self): 

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

1041 ''' 

1042 Ecef9Tuple._CartesianBase = C = _MODS.cartesianBase.CartesianBase # overwrite property_RO 

1043 return C 

1044 

1045 @deprecated_method 

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

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

1048 return self.toDatum(datum2) 

1049 

1050 @Property_RO 

1051 def lam(self): 

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

1053 ''' 

1054 return self.philam.lam 

1055 

1056 @Property_RO 

1057 def lamVermeille(self): 

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

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

1060 

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

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

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

1064 ''' 

1065 x, y = self.x, self.y 

1066 if y > EPS0: 

1067 r = atan2(x, hypot(y, x) + y) * _N_2_0 + PI_2 

1068 elif y < -EPS0: 

1069 r = atan2(x, hypot(y, x) - y) * _2_0 - PI_2 

1070 else: # y == 0 

1071 r = PI if x < 0 else _0_0 

1072 return Lam(Vermeille=r) 

1073 

1074 @Property_RO 

1075 def latlon(self): 

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

1077 ''' 

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

1079 

1080 @Property_RO 

1081 def latlonheight(self): 

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

1083 ''' 

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

1085 

1086 @Property_RO 

1087 def latlonheightdatum(self): 

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

1089 ''' 

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

1091 

1092 @Property_RO 

1093 def latlonVermeille(self): 

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

1095 

1096 @see: Property C{lonVermeille}. 

1097 ''' 

1098 return LatLon2Tuple(self.lat, self.lonVermeille, name=self.name) 

1099 

1100 @Property_RO 

1101 def lonVermeille(self): 

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

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

1104 

1105 @see: Property C{lamVermeille}. 

1106 ''' 

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

1108 

1109 @Property_RO 

1110 def phi(self): 

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

1112 ''' 

1113 return self.philam.phi 

1114 

1115 @Property_RO 

1116 def philam(self): 

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

1118 ''' 

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

1120 

1121 @Property_RO 

1122 def philamheight(self): 

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

1124 ''' 

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

1126 

1127 @Property_RO 

1128 def philamheightdatum(self): 

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

1130 ''' 

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

1132 

1133 @Property_RO 

1134 def philamVermeille(self): 

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

1136 

1137 @see: Property C{lamVermeille}. 

1138 ''' 

1139 return PhiLam2Tuple(radians(self.lat), self.lamVermeille, name=self.name) 

1140 

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

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

1143 C{Cartesian}. 

1144 

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

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

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

1148 or C{None}. 

1149 @kwarg Cartesian_kwds: Optional, additional B{C{Cartesian}} keyword arguments, ignored 

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

1151 

1152 @return: A C{B{Cartesian}(x, y, z, **B{Cartesian_kwds})} instance or 

1153 a L{Vector4Tuple}C{(x, y, z, h)} if C{B{Cartesian} is None}. 

1154 

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

1156 ''' 

1157 if Cartesian in (None, Vector4Tuple): 

1158 r = self.xyzh 

1159 elif Cartesian is Vector3Tuple: 

1160 r = self.xyz 

1161 else: 

1162 _xsubclassof(self._CartesianBase, Cartesian=Cartesian) 

1163 r = Cartesian(self, **_xkwds(Cartesian_kwds, name=self.name)) 

1164 return r 

1165 

1166 def toDatum(self, datum2): 

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

1168 

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

1170 

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

1172 

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

1174 ''' 

1175 if self.datum in (None, datum2): # PYCHOK _Names_ 

1176 r = self.copy() 

1177 else: 

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

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

1180 # and returns another Ecef9Tuple iff LatLon is None 

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

1182 return r 

1183 

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

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

1186 

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

1188 or C{None}. 

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

1190 B{C{LatLon}} keyword arguments. 

1191 

1192 @return: An instance of C{B{LatLon}(lat, lon, **B{LatLon_kwds})} 

1193 or if B{C{LatLon}} is C{None}, a L{LatLon3Tuple}C{(lat, lon, 

1194 height)} respectively L{LatLon4Tuple}C{(lat, lon, height, 

1195 datum)} depending on whether C{datum} is un-/specified. 

1196 

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

1198 ''' 

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

1200 kwds = _xkwds(LatLon_kwds, height=self.height, datum=D, name=self.name) # PYCHOK Ecef9Tuple 

1201 d = kwds.get(_datum_, LatLon) 

1202 if LatLon is None: 

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

1204 if d is not None: 

1205 # assert d is not LatLon 

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

1207 else: 

1208 if d is None: 

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

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

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

1212 return r 

1213 

1214 def toLocal(self, ltp, Xyz=None, **Xyz_kwds): 

1215 '''Convert this geocentric to I{local} C{x}, C{y} and C{z}. 

1216 

1217 @kwarg ltp: The I{local tangent plane} (LTP) to use (L{Ltp}). 

1218 @kwarg Xyz: Optional class to return C{x}, C{y} and C{z} 

1219 (L{XyzLocal}, L{Enu}, L{Ned}) or C{None}. 

1220 @kwarg Xyz_kwds: Optional, additional B{C{Xyz}} keyword 

1221 arguments, ignored if C{B{Xyz} is None}. 

1222 

1223 @return: An B{C{Xyz}} instance or if C{B{Xyz} is None}, 

1224 a L{Local9Tuple}C{(x, y, z, lat, lon, height, 

1225 ltp, ecef, M)} with C{M=None}, always. 

1226 

1227 @raise TypeError: Invalid B{C{ltp}}. 

1228 ''' 

1229 return _MODS.ltp._xLtp(ltp)._ecef2local(self, Xyz, Xyz_kwds) 

1230 

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

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

1233 

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

1235 C{None}. 

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

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

1238 

1239 @return: A C{Vector}C{(x, y, z, **Vector_kwds)} instance or a 

1240 L{Vector3Tuple}C{(x, y, z)} if B{C{Vector}} is C{None}. 

1241 

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

1243 ''' 

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

1245 *self.xyz, **_xkwds(Vector_kwds, name=self.name)) # PYCHOK Ecef9Tuple 

1246 

1247# def _T_x_M(self, T): 

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

1249# ''' 

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

1251 

1252 @Property_RO 

1253 def xyz(self): 

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

1255 ''' 

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

1257 

1258 @Property_RO 

1259 def xyzh(self): 

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

1261 ''' 

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

1263 

1264 

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

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

1267 ''' 

1268 if Ecef is None: 

1269 Ecef = EcefKarney 

1270 else: 

1271 _xinstanceof(*_Ecefs, Ecef=Ecef) 

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

1273 

1274 

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

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

1277 ''' 

1278 try: 

1279 lat, lon = latlonh.lat, latlonh.lon 

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

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

1282 except AttributeError: 

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

1284 

1285 try: 

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

1287 except (TypeError, ValueError) as x: 

1288 t = _lat_, _lon_, _height_ 

1289 if suffix: 

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

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

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

1293 

1294 

1295def _xEcef(Ecef): # PYCHOK .latlonBase 

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

1297 ''' 

1298 if issubclassof(Ecef, _EcefBase): 

1299 return Ecef 

1300 raise _TypesError(_Ecef_, Ecef, *_Ecefs) 

1301 

1302 

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

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

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

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

1307 ''' 

1308 try: 

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

1310 try: 

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

1312 if not isinstance(xyz, Types): 

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

1314 except AttributeError: 

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

1316 return t 

1317 except (TypeError, ValueError) as x: 

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

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

1320# assert _xyz_y_z == _MODS.basics._args_kwds_names(_xyzn4)[:3] 

1321 

1322 

1323_Ecefs = (EcefKarney, EcefSudano, EcefVeness, EcefYou, 

1324 EcefFarrell21, EcefFarrell22) 

1325__all__ += _ALL_DOCS(_EcefBase) 

1326 

1327# **) MIT License 

1328# 

1329# Copyright (C) 2016-2024 -- mrJean1 at Gmail -- All Rights Reserved. 

1330# 

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

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

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

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

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

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

1337# 

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

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

1340# 

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

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

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

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

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

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

1347# OTHER DEALINGS IN THE SOFTWARE.