Coverage for pygeodesy/ecef.py: 95%

1

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

3

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

5

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

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

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

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

10 3709199_An_exact_conversion_from_an_Earth-centered_coordinate_system_to_latitude_longitude_and_altitude>},

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

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

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

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

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

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

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

18

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

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

21

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

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

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

25

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

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

28

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

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

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

32

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

34

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

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

37(2002) 76, page 451-454.

38

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

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

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

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

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

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

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

46

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

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

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

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

51

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

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

54 C{lon00} to configure that value.

55

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

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

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

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

60'''

61

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

63 _xinstanceof, _xsubclassof, typename # _args_kwds_names

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

65 _0_5, _1_0, _1_0_1T, _2_0, _3_0, _4_0, _6_0, _90_0, \

66 _copysign_1_0, isnon0 # PYCHOK used!

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

68 from pygeodesy.ecefLocals import _EcefLocal

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

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

71 _xattr, _xdatum, _xkwds, _xkwds_get

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

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

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

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

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

77 _x_, _xyz_, _y_, _z_

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

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

80 from pygeodesy.namedTuples import LatLon2Tuple, LatLon3Tuple, \

81 PhiLam2Tuple, Vector3Tuple, Vector4Tuple

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

83 property_RO, property_ROver

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

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

86 Phi, Radians_, Scalar, Scalar_

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

88 sincos2, sincos2_, sincos2d_

89# from pygeodesy.vector3d import Vector3d # _MODS

90

91 from math import cos, degrees, fabs, radians, sqrt

92

93 __all__ = _ALL_LAZY.ecef

94 __version__ = '25.08.31'

95

96 _Ecef_ = 'Ecef'

97 _prolate_ = 'prolate'

98 _TRIPS = 33 # 8..9 sufficient, EcefEDA.-/EcefSudano.reverse

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

100

101

102 class EcefError(_ValueError):

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

104 '''

105 pass

106

107

108 class _EcefBase(_NamedBase):

109 '''(INTERNAL) Base class for C{Ecef*} conversion classes.

110 '''

111 _datum = _WGS84

112 _E = _EWGS84

113 _isYou = False

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

115

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

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

118

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

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

121 equatorial radius (C{meter}).

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

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

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

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}} not L{Ellipsoid}, L{Ellipsoid2},

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

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

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

133 '''

134 try:

135 E = a_ellipsoid

136 if f is None:

137 pass

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

139 E = a_f2Tuple(E, f)

140 else:

141 raise ValueError() # _invalid_

142

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

144 d = _ellipsoidal_datum(E, **name)

145 E = d.ellipsoid

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

147 raise ValueError() # _invalid_

148 self._datum = d

149 self._E = E

150

151 except (TypeError, ValueError) as x:

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

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

154

155 if name:

156 self.name = name

157 if lon00 is not INT0:

158 self.lon00 = lon00

159

160 def __eq__(self, other):

161 '''Compare this and an other Ecef.

162

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

164

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

166 '''

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

168 other.ellipsoid == self.ellipsoid)

169

170 @Property_RO

171 def datum(self):

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

173 '''

174 return self._datum

175

176 @Property_RO

177 def ellipsoid(self):

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

179 '''

180 return self._E

181

182 @Property_RO

183 def equatoradius(self):

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

185 '''

186 return self.ellipsoid.a

187

188 a = equatorialRadius = equatoradius # Karney property

189

190 @Property_RO

191 def flattening(self): # Karney property

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

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

194 '''

195 return self.ellipsoid.f

196

197 f = flattening

198

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

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

201 '''

202 if _philam: # lat, lon in radians

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

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

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

206 else:

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

208

209 E = self.ellipsoid

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

211 c = (h + n) * ca

212 x = cb * c

213 y = sb * c

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

215

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

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

218 0, m, self.datum, # C=0, always

219 name=self._name__(name))

220

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

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

223

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

225 latitude (C{degrees}).

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

227 (C{degrees}).

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

229 the surface of the ellipsoid.

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

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

232

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

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

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

236 and C{datum} if available.

237

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

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

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

241

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

243 and avoid double angle conversions.

244 '''

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

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

247

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

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

250 in I{radians}.

251

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

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

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

255 the surface of the ellipsoid.

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

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

258

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

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

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

262

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

264 '''

265 try: # like function C{_llhn4} below

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

267 except (TypeError, ValueError) as x:

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

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

270

271 @property_ROver

272 def _Geocentrics(self):

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

274 '''

275 return (Ecef9Tuple, # overwrite property_ROver

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

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, p, **lon00_name):

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

304 '''

305 return atan2d(y, x) if p else _xkwds_get(lon00_name, lon00=self.lon00)

306

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

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

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

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 def _xyzllhCpn9(self, xyz, y, z, **lon00_name):

321 '''(INTERNAL) Get C{x, y, z} and determine case C{C}, C{lat}, C{lon}, etc.

322 '''

323 x, y, z, n = _xyzn4(xyz, y, z, self._Geocentrics, **lon00_name)

324

325 s, c, p = _norm3(y, x) # distance to polar axis

326 if p < EPS0: # polar

327 lat = copysign0(_90_0, z)

328 h = fabs(z) - self.ellipsoid.b

329 C = 2

330 p = 0 # force lon00

331 elif fabs(z) < EPS0: # equatorial

332 lat = _0_0

333 h = p - self.ellipsoid.a

334 C = 3

335 else: # see _norm7 below, EcefKarney

336 h = hypot(z, p) # distance to earth center

337 if h > self.ellipsoid._heightMax:

338 lat = atan1d(z / h, p / h)

339 C = 4 # too high

340# p *= _0_5

341 else: # pass h for EcefVeness

342 lat = None # -90..90

343 C = 1 # normal

344 lon = self._polon(s, c, p, **lon00_name)

345 return x, y, z, lat, lon, h, C, p, self._name__(n)

346

347

348 class EcefFarrell21(_EcefBase):

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

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

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

352 '''

353

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

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

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

357 page 29, aka the I{Heikkinen application} of U{Ferrari's solution

358 <https://WikiPedia.org/wiki/Geographic_coordinate_conversion>}.

359

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

361 coordinate (C{meter}).

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

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

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

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

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

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

368

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

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

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

372

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

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

375 zero division error.

376

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

378 '''

379 x, y, z, lat, lon, h, C, p, name = self._xyzllhCpn9(xyz, y, z, **lon00_name)

380 if lat is None:

381 E = self.ellipsoid

382 a = E.a

383 a2 = E.a2

384 b2 = E.b2

385 e2 = E.e2

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

387 e4 = E.e4

388

389 try: # names as page 29

390 z2 = z**2

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

392

393 p2 = p**2

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

395 F = b2 * z2 * 54

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

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

398 c = cbrt(s + c + _1_0)

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

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

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

402 Q1 = Q + _1_0

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

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

405 -P * p2 * _0_5)

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

407 r = p + r * e2

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

409

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

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

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

413

414 except (ValueError, ZeroDivisionError) as X:

415 raise EcefError(x=x, y=y, z=z, cause=X)

416

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

418 C, None, self.datum, name=name)

419

420

421 class EcefFarrell22(_EcefBase):

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

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

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

425 '''

426

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

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

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

430 page 30.

431

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

433 coordinate (C{meter}).

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

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

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

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

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

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

440

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

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

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

444

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

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

447 zero division error.

448

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

450 '''

451 x, y, z, lat, lon, h, C, p, name = self._xyzllhCpn9(xyz, y, z, **lon00_name)

452 if lat is None:

453 E = self.ellipsoid

454 a, b = E.a, E.b

455 s, c, _ = _norm3(z * a, p * b)

456 s, c, _ = _norm3(z + s**3 * b * E.e22,

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

458 lat = atan1d(s, c)

459 h = (p / fabs(c) - (E.roc1_(s) if s else a)) if c else (fabs(z) - b)

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

461

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

463 C, None, self.datum, name=name)

464

465

466 class EcefKarney(_EcefBase):

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

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

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

470

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

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

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

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

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

476 the rotation matrix.

477 '''

478

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

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

481

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

483 coordinate (C{meter}).

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

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

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

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

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

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

490

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

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

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

494 C{datum} if available.

495

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

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

498

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

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

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

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

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

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

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

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

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

508 '''

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

510

511 E = self.ellipsoid

512 f = E.f

513

514 sa, ca, sb, cb, R, h, C = _norm7(y, x, z, E)

515 if C: # PYCHOK no cover

516 pass # too high, far

517 elif E.e4: # E.isEllipsoidal

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

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

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

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

522 if f < 0:

523 p, q = q, p

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

525 e = E.e4 * q

526 if e or r > 0:

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

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

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

530 u = r = r / _6_0

531 r2 = r**2

532 r3 = r2 * r

533 t3 = r3 + s

534 d *= t3 + r3

535 if d < 0:

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

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

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

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

540 else:

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

542 # minimizes loss of precision due to cancellation. The

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

544 # in definition of u.

545 if d > 0:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

560 if f < 0:

561 k1 -= E.e2

562 else:

563 k2 += E.e2

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

565 h *= k1 - E.e21

566 C = 1

567

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

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

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

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

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

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

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

575 q = E.e4 - p

576 if f < 0:

577 p, q = q, p

578 e = E.a

579 else:

580 e = E.b2_a

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

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

583 sa = neg(sa)

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

585 C = 3

586

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

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

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

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

591 h -= E.a

592 C = 2

593

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

595 lon = self._polon(sb, cb, R, **lon00_name)

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

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

598 C, m, self.datum, name=self._name__(name))

599

600

601 class EcefSudano(_EcefBase):

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

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

604 '''

605 _tol = EPS0 # DEPRECATED

606

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

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

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

610

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

612 coordinate (C{meter}).

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

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

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

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

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

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

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

620

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

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

623 case C{C}, C{M=None} always and C{datum} if available.

624

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

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

627

628 @see: Class L{EcefUPC}.

629 '''

630 x, y, z, lat, lon, h, C, p, name = self._xyzllhCpn9(xyz, y, z, **lon00_name)

631 if lat is None:

632 E = self.ellipsoid

633 e = E.e2 * E.a

634 d = e - p

635

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

637 _S2 = Fsum(sa).fsum2f_

638 tol = Scalar_(tol=tol, low=self.tolerance, Error=EcefError)

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

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

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

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

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

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

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

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

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

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

649 ca2 = _1_0 - sa**2

650 if ca2 < EPS02:

651 break

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

653 if fabs(D) < EPS0:

654 break

655 ca = sqrt(ca2)

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

657 if fabs(D) < tol:

658 break

659 else:

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

661 raise EcefError(t, txt=Fmt.no_convergence(D, tol))

662

663 sa = copysign0(sa, z)

664 lat = atan1d(sa, ca)

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

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

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

668 else:

669 i = 0

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

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

672 iteration=i, name=name)

673

674 @deprecated_property

675 def tolerance(self):

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

677 return self._tol

678

679 @tolerance.setter # PYCHOK setter!

680 def tolerance(self, tol):

681 self._tol = Scalar_(tolerance=tol, low=EPS0, Error=EcefError)

682

683

684 class EcefUPC(_EcefBase):

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

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

687 '''

688

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

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

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

692

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

694 coordinate (C{meter}).

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

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

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

698 @kwarg tol: Convergence tolerance for the latitude (C{radians}).

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

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

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

702

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

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

705 case C{C}, C{M=None} always and C{datum} if available.

706

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

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

709

710 @see: Class L{EcefSudano}.

711 '''

712 x, y, z, lat, lon, h, C, p, name = self._xyzllhCpn9(xyz, y, z, **lon00_name)

713 if lat is None:

714 E = self.ellipsoid

715 a = E.a

716 e2 = E.e2 # signed

717

718 z_p = z / p

719 ph0 = atan1(z_p, E.e21)

720 tol = Radians_(tol=tol, low=EPS0, Error=EcefError)

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

722 s, c = sincos2(ph0)

723 N = a / sqrt(_1_0 - s**2 * e2)

724 h = p / c - N

725 phi = atan1(z_p, _1_0 - e2 * (N / (N + h)))

726 r = fabs(phi - ph0)

727 if r < tol:

728 lat = copysign0(degrees(phi), z)

729 break

730 ph0 = phi

731 else:

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

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

734 else:

735 i = 0

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

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

738 iteration=i, name=name)

739

740

741 class EcefVeness(_EcefBase):

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

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

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

745

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

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

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

749 '''

750

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

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

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

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

755

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

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

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

759

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

761 coordinate (C{meter}).

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

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

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

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

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

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

768

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

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

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

772

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

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

775

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

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

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

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

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

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

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

783 publication/3709199>}.

784 '''

785 x, y, z, lat, lon, h, C, p, name = self._xyzllhCpn9(xyz, y, z, **lon00_name)

786 if lat is None:

787 E = self.ellipsoid

788 a = E.a

789 B = E.b * E.e22

790 # parametric latitude (Bowring eqn 17, replaced)

791 t = (E.b * z) / (a * p) * (_1_0 + B / h) # h = hypot(z, p)

792 c = _1_0 / hypot1(t)

793 s = c * t

794 # geodetic latitude (Bowring eqn 18)

795 s, c, _ = _norm3(z + s**3 * B,

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

797 lat = atan1d(s, c)

798 h = E._heightB(s, c, z, p) # height (Bowring eqn 7)

799

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

801 C, None, self.datum, name=name)

802

803

804 class EcefYou(_EcefBase):

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

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

807 for I{non-prolate} ellipsoids.

808

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

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

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

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

813 '''

814 _isYou = True

815

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

817 _EcefBase.__init__(self, a_ellipsoid, f=f, **lon00_name) # inherited documentation

818

819 E = self.ellipsoid

820 e2 = E.a2 - E.b2

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

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

823 self._ee2 = sqrt0(e2), e2

824

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

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

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

828

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

830 coordinate (C{meter}).

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

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

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

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

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

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

837

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

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

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

841

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

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

844 ellipsoid is I{prolate}.

845 '''

846 x, y, z, lat, lon, h, C, p, name = self._xyzllhCpn9(xyz, y, z, **lon00_name)

847 if lat is None:

848 E = self.ellipsoid

849 a, b = E.a, E.b

850 e, e2 = self._ee2

851

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

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

854 u *= _0_5

855 if u > EPS02:

856 u = sqrt(u)

857 q = hypot(u, e)

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

859 sB, cB = sincos2(B)

860 if cB and sB:

861 q *= a

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

863 if isnon0(d):

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

865 sB, cB = sincos2(B)

866 elif u < (-EPS02):

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

868 else: # near polar # PYCHOK no cover

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

870

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

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

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

874 h = neg(h) # inside ellipsoid

875

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

877 C, None, self.datum, name=name)

878

879

880 class EcefMatrix(_NamedTuple):

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

882

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

884 Geographic_coordinate_conversion#From_ECEF_to_ENU>} and

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

886 '''

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

888 '_1_0_', '_1_1_', '_1_2_',

889 '_2_0_', '_2_1_', '_2_2_')

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

891

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

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

894 '''

895 _NamedTuple._validate(self, underOK=True)

896

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

898 '''New L{EcefMatrix} matrix.

899

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

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

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

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

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

905

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

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

908 '''

909 t = sa, ca, sb, cb

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

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

912

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

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

915

916 else: # build matrix from the following quaternion operations

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

918 # or

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

920 # where

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

922

923 # Local X axis (East) in geocentric coords

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

925 # Local Y axis (North) in geocentric coords

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

927 # Local Z axis (Up) in geocentric coords

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

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

930 cb, -sb * sa, sb * ca,

931 _0_0, ca, sa)

932

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

934

935 def column(self, column):

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

937 '''

938 if 0 <= column < 3:

939 return self[column::3]

940 raise _IndexError(column=column)

941

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

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

944

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

946 '''

947 return self.classof(*self)

948

949 __copy__ = __deepcopy__ = copy

950

951 @Property_RO

952 def matrix3(self):

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

954 '''

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

956

957 @Property_RO

958 def matrixTransposed3(self):

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

960 '''

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

962

963 def multiply(self, other):

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

965 with an other matrix.

966

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

968

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

970

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

972 '''

973 _xinstanceof(EcefMatrix, other=other)

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

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

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

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

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

979

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

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

982

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

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

985

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

987

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

989 '''

990 if xyz0:

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

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

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

994

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

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

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

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

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

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

1001

1002 def row(self, row):

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

1004 '''

1005 if 0 <= row < 3:

1006 r = row * 3

1007 return self[r: r+3]

1008 raise _IndexError(row=row)

1009

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

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

1012

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

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

1015

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

1017

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

1019 '''

1020 if xyz0:

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

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

1023 _xyz = _1_0_1T + xyz

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

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

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

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

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

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

1030 else:

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

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

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

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

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

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

1037 return xyz_

1038

1039

1040 class Ecef9Tuple(_NamedTuple, _EcefLocal):

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

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

1043 optionally, rotation matrix C{M} (L{EcefMatrix}) and C{datum}, with C{lat}

1044 and C{lon} in C{degrees} and C{x}, C{y}, C{z} and C{height} in C{meter},

1045 conventionally. Case C{C=1} means normal, C{C=2} near polar and C{C=3}

1046 equatorial latitude and C{C=4} height exceeds C{heightMax}.

1047 '''

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

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

1050

1051 @property_ROver

1052 def _CartesianBase(self):

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

1054 '''

1055 return _MODS.cartesianBase.CartesianBase # overwrite property_ROver

1056

1057 @deprecated_method

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

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

1060 return self.toDatum(datum2)

1061

1062 @property_RO

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

1064 return self

1065

1066 @property_RO

1067 def ellipsoid(self):

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

1069 '''

1070 return (self.datum or _WGS84).ellipsoid

1071

1072 @Property_RO

1073 def lam(self):

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

1075 '''

1076 return self.philam.lam

1077

1078 @Property_RO

1079 def lamVermeille(self):

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

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

1082

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

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

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

1086 '''

1087 x, y = self.x, self.y

1088 a = fabs(y)

1089 if a > EPS0:

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

1091 if y < 0:

1092 r = -r

1093 else: # y == 0

1094 r = PI if x < 0 else _0_0

1095 return Lam(Vermeille=r)

1096

1097 @Property_RO

1098 def latlon(self):

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

1100 '''

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

1102

1103 @Property_RO

1104 def latlonheight(self):

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

1106 '''

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

1108

1109 @Property_RO

1110 def latlonheightdatum(self):

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

1112 '''

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

1114

1115 @Property_RO

1116 def latlonVermeille(self):

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

1118

1119 @see: Property C{lonVermeille}.

1120 '''

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

1122

1123 @Property_RO

1124 def lonVermeille(self):

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

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

1127

1128 @see: Property C{lamVermeille}.

1129 '''

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

1131

1132 @Property_RO

1133 def Mx(self):

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

1135 '''

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

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

1138

1139 @Property_RO

1140 def phi(self):

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

1142 '''

1143 return self.philam.phi

1144

1145 @Property_RO

1146 def philam(self):

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

1148 '''

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

1150

1151 @Property_RO

1152 def philamheight(self):

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

1154 '''

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

1156

1157 @Property_RO

1158 def philamheightdatum(self):

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

1160 '''

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

1162

1163 @Property_RO

1164 def philamVermeille(self):

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

1166

1167 @see: Property C{lamVermeille}.

1168 '''

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

1170

1171 phiVermeille = phi

1172

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

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

1175 C{Cartesian}.

1176

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

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

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

1180 or C{None}.

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

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

1183

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

1185 is None}.

1186

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

1188 '''

1189 if _isin(Cartesian, None, Vector4Tuple):

1190 r = self.xyzh

1191 elif Cartesian is Vector3Tuple:

1192 r = self.xyz

1193 else:

1194 _xsubclassof(self._CartesianBase, Cartesian=Cartesian)

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

1196 return r

1197

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

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

1200

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

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

1203

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

1205

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

1207 '''

1208 n = _name__(name, _or_nameof=self)

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

1210 r = self.copy(name=n)

1211 else:

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

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

1214 # and returns another Ecef9Tuple iff LatLon is None

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

1216 return r

1217

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

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

1220

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

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

1223 keyword arguments.

1224

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

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

1227 specified or not.

1228

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

1230 '''

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

1232 kwds = _name1__(LatLon_kwds, _or_nameof=self)

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

1234 d = kwds.get(_datum_, LatLon)

1235 if LatLon is None:

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

1237 if d is not None:

1238 # assert d is not LatLon

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

1240 else:

1241 if d is None:

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

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

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

1245 return r

1246

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

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

1249

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

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

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

1253

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

1255 C{B{Vector} is None}.

1256

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

1258

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

1260 '''

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

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

1263

1264# def _T_x_M(self, T):

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

1266# '''

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

1268

1269 @Property_RO

1270 def xyz(self):

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

1272 '''

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

1274

1275 @Property_RO

1276 def xyzh(self):

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

1278 '''

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

1280

1281

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

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

1284 '''

1285 if Ecef is None:

1286 Ecef = EcefKarney

1287 else:

1288 _xinstanceof(*_Ecefs, Ecef=Ecef)

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

1290

1291

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

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

1294 '''

1295 try:

1296 lat, lon = latlonh.lat, latlonh.lon

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

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

1299 except AttributeError:

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

1301 try:

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

1303 except (TypeError, ValueError) as x:

1304 t = _lat_, _lon_, _height_

1305 if suffix:

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

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

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

1309

1310

1311 def _norm3(y, x):

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

1313 '''

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

1315 return (y / h, x / h, h) if h else (_0_0, _1_0, h)

1316

1317

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

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

1320 '''

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

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

1323 if h > E._heightMax:

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

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

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

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

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

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

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

1331 C = 4

1332 else:

1333 C = 0

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

1335

1336

1337 def _xEcef(Ecef): # PYCHOK .latlonBase

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

1339 '''

1340 if issubclassof(Ecef, _EcefBase):

1341 return Ecef

1342 raise _TypesError(_Ecef_, Ecef, *_Ecefs)

1343

1344

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

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

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

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

1349 '''

1350 try:

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

1352 try:

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

1354 if not isinstance(xyz, Types):

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

1356 except AttributeError:

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

1358 except (TypeError, ValueError) as x:

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

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

1361 return t

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

1363

1364

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

1366 if issubclassof(_, _EcefBase) and

1367 _ is not _EcefBase)

1368 __all__ += _ALL_DOCS(_EcefBase)

1369

1370# **) MIT License

1371#

1372# Copyright (C) 2016-2026 -- mrJean1 at Gmail -- All Rights Reserved.

1373#

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

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

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

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

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

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

1380#

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

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

1383#

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

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

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

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

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

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

1390# OTHER DEALINGS IN THE SOFTWARE.