Coverage for pygeodesy/ecef.py: 95%
455 statements
« prev ^ index » next coverage.py v7.6.1, created at 2025-01-06 12:20 -0500
« prev ^ index » next coverage.py v7.6.1, created at 2025-01-06 12:20 -0500
2# -*- coding: utf-8 -*-
4u'''I{Geocentric} Earth-Centered, Earth-Fixed (ECEF) coordinates.
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.
17Following is a copy of I{Karney}'s U{Detailed Description
18<https://GeographicLib.SourceForge.io/C++/doc/classGeographicLib_1_1Geocentric.html>}.
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>}).
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°.
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}.
31Forward conversion from geodetic to geocentric (ECEF) coordinates is straightforward.
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.
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.
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.
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.
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'''
60from pygeodesy.basics import copysign0, isscalar, issubclassof, neg, map1, \
61 _xinstanceof, _xsubclassof # _args_kwds_names
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__, _name1__, _NamedBase, _NamedLocal, \
77 _NamedTuple, _Pass, _xnamed
78from pygeodesy.namedTuples import LatLon2Tuple, LatLon3Tuple, \
79 PhiLam2Tuple, Vector3Tuple, Vector4Tuple
80from pygeodesy.props import deprecated_method, Property_RO, property_RO, \
81 property_ROver, property_doc_
82# from pygeodesy.streprs import Fmt, unstr # from .fsums
83from pygeodesy.units import _isRadius, Degrees, Height, Int, Lam, Lat, Lon, Meter, \
84 Phi, Scalar, Scalar_
85from pygeodesy.utily import atan1, atan1d, atan2, atan2d, degrees90, degrees180, \
86 sincos2, sincos2_, sincos2d, sincos2d_
87# from pygeodesy.vector3d import Vector3d # _MODS
89from math import cos, degrees, fabs, radians, sqrt
91__all__ = _ALL_LAZY.ecef
92__version__ = '24.12.06'
94_Ecef_ = 'Ecef'
95_prolate_ = 'prolate'
96_TRIPS = 33 # 8..9 sufficient, EcefSudano.reverse
97_xyz_y_z = _xyz_, _y_, _z_ # _args_kwds_names(_xyzn4)[:3]
100class EcefError(_ValueError):
101 '''An ECEF or C{Ecef*} related issue.
102 '''
103 pass
106class _EcefBase(_NamedBase):
107 '''(INTERNAL) Base class for L{EcefFarrell21}, L{EcefFarrell22}, L{EcefKarney},
108 L{EcefSudano}, L{EcefVeness} and L{EcefYou}.
109 '''
110 _datum = _WGS84
111 _E = _EWGS84
112 _lon00 = INT0 # arbitrary, "polar" lon for LocalCartesian, Ltp
114 def __init__(self, a_ellipsoid=_EWGS84, f=None, lon00=INT0, **name):
115 '''New C{Ecef*} converter.
117 @arg a_ellipsoid: A (non-prolate) ellipsoid (L{Ellipsoid}, L{Ellipsoid2},
118 L{Datum} or L{a_f2Tuple}) or C{scalar} ellipsoid's
119 equatorial radius (C{meter}).
120 @kwarg f: C{None} or the ellipsoid flattening (C{scalar}), required
121 for C{scalar} B{C{a_ellipsoid}}, C{B{f}=0} represents a
122 sphere, negative B{C{f}} a prolate ellipsoid.
123 @kwarg lon00: An arbitrary, I{"polar"} longitude (C{degrees}), see the
124 C{reverse} method.
125 @kwarg name: Optional C{B{name}=NN} (C{str}).
127 @raise EcefError: If B{C{a_ellipsoid}} not L{Ellipsoid}, L{Ellipsoid2},
128 L{Datum} or L{a_f2Tuple} or C{scalar} or B{C{f}} not
129 C{scalar} or if C{scalar} B{C{a_ellipsoid}} not positive
130 or B{C{f}} not less than 1.0.
131 '''
132 try:
133 E = a_ellipsoid
134 if f is None:
135 pass
136 elif _isRadius(E) and isscalar(f):
137 E = a_f2Tuple(E, f)
138 else:
139 raise ValueError() # _invalid_
141 if E not in (_EWGS84, _WGS84):
142 d = _ellipsoidal_datum(E, **name)
143 E = d.ellipsoid
144 if E.a < EPS or E.f > EPS1:
145 raise ValueError() # _invalid_
146 self._datum = d
147 self._E = E
149 except (TypeError, ValueError) as x:
150 t = unstr(self.classname, a=a_ellipsoid, f=f)
151 raise EcefError(_SPACE_(t, _ellipsoid_), cause=x)
153 if name:
154 self.name = name
155 if lon00 is not INT0:
156 self.lon00 = lon00
158 def __eq__(self, other):
159 '''Compare this and an other Ecef.
161 @arg other: The other ecef (C{Ecef*}).
163 @return: C{True} if equal, C{False} otherwise.
164 '''
165 return other is self or (isinstance(other, self.__class__) and
166 other.ellipsoid == self.ellipsoid)
168 @Property_RO
169 def datum(self):
170 '''Get the datum (L{Datum}).
171 '''
172 return self._datum
174 @Property_RO
175 def ellipsoid(self):
176 '''Get the ellipsoid (L{Ellipsoid} or L{Ellipsoid2}).
177 '''
178 return self._E
180 @Property_RO
181 def equatoradius(self):
182 '''Get the C{ellipsoid}'s equatorial radius, semi-axis (C{meter}).
183 '''
184 return self.ellipsoid.a
186 a = equatorialRadius = equatoradius # Karney property
188 @Property_RO
189 def flattening(self): # Karney property
190 '''Get the C{ellipsoid}'s flattening (C{scalar}), positive for
191 I{oblate}, negative for I{prolate} or C{0} for I{near-spherical}.
192 '''
193 return self.ellipsoid.f
195 f = flattening
197 def _forward(self, lat, lon, h, name, M=False, _philam=False): # in .ltp.LocalCartesian.forward and -.reset
198 '''(INTERNAL) Common for all C{Ecef*}.
199 '''
200 if _philam: # lat, lon in radians
201 sa, ca, sb, cb = sincos2_(lat, lon)
202 lat = Lat(degrees90( lat), Error=EcefError)
203 lon = Lon(degrees180(lon), Error=EcefError)
204 else:
205 sa, ca, sb, cb = sincos2d_(lat, lon)
207 E = self.ellipsoid
208 n = E.roc1_(sa, ca) if self._isYou else E.roc1_(sa)
209 z = (h + n * E.e21) * sa
210 x = (h + n) * ca
212 m = self._Matrix(sa, ca, sb, cb) if M else None
213 return Ecef9Tuple(x * cb, x * sb, z, lat, lon, h,
214 0, m, self.datum,
215 name=self._name__(name))
217 def forward(self, latlonh, lon=None, height=0, M=False, **name):
218 '''Convert from geodetic C{(lat, lon, height)} to geocentric C{(x, y, z)}.
220 @arg latlonh: Either a C{LatLon}, an L{Ecef9Tuple} or C{scalar}
221 latitude (C{degrees}).
222 @kwarg lon: Optional C{scalar} longitude for C{scalar} B{C{latlonh}}
223 (C{degrees}).
224 @kwarg height: Optional height (C{meter}), vertically above (or below)
225 the surface of the ellipsoid.
226 @kwarg M: Optionally, return the rotation L{EcefMatrix} (C{bool}).
227 @kwarg name: Optional C{B{name}=NN} (C{str}).
229 @return: An L{Ecef9Tuple}C{(x, y, z, lat, lon, height, C, M, datum)} with
230 geocentric C{(x, y, z)} coordinates for the given geodetic ones
231 C{(lat, lon, height)}, case C{C} 0, optional C{M} (L{EcefMatrix})
232 and C{datum} if available.
234 @raise EcefError: If B{C{latlonh}} not C{LatLon}, L{Ecef9Tuple} or
235 C{scalar} or B{C{lon}} not C{scalar} for C{scalar}
236 B{C{latlonh}} or C{abs(lat)} exceeds 90°.
238 @note: Use method C{.forward_} to specify C{lat} and C{lon} in C{radians}
239 and avoid double angle conversions.
240 '''
241 llhn = _llhn4(latlonh, lon, height, **name)
242 return self._forward(*llhn, M=M)
244 def forward_(self, phi, lam, height=0, M=False, **name):
245 '''Like method C{.forward} except with geodetic lat- and longitude given
246 in I{radians}.
248 @arg phi: Latitude in I{radians} (C{scalar}).
249 @arg lam: Longitude in I{radians} (C{scalar}).
250 @kwarg height: Optional height (C{meter}), vertically above (or below)
251 the surface of the ellipsoid.
252 @kwarg M: Optionally, return the rotation L{EcefMatrix} (C{bool}).
253 @kwarg name: Optional C{B{name}=NN} (C{str}).
255 @return: An L{Ecef9Tuple}C{(x, y, z, lat, lon, height, C, M, datum)}
256 with C{lat} set to C{degrees90(B{phi})} and C{lon} to
257 C{degrees180(B{lam})}.
259 @raise EcefError: If B{C{phi}} or B{C{lam}} invalid or not C{scalar}.
260 '''
261 try: # like function C{_llhn4} below
262 plhn = Phi(phi), Lam(lam), Height(height), _name__(name)
263 except (TypeError, ValueError) as x:
264 raise EcefError(phi=phi, lam=lam, height=height, cause=x)
265 return self._forward(*plhn, M=M, _philam=True)
267 @property_ROver
268 def _Geocentrics(self):
269 '''(INTERNAL) Get the valid geocentric classes. I{once}.
270 '''
271 return (Ecef9Tuple, # overwrite property_ROver
272 _MODS.vector3d.Vector3d) # _MODS.cartesianBase.CartesianBase
274 @Property_RO
275 def _isYou(self):
276 '''(INTERNAL) Is this an C{EcefYou}?.
277 '''
278 return isinstance(self, EcefYou)
280 @property
281 def lon00(self):
282 '''Get the I{"polar"} longitude (C{degrees}), see method C{reverse}.
283 '''
284 return self._lon00
286 @lon00.setter # PYCHOK setter!
287 def lon00(self, lon00):
288 '''Set the I{"polar"} longitude (C{degrees}), see method C{reverse}.
289 '''
290 self._lon00 = Degrees(lon00=lon00)
292 def _Matrix(self, sa, ca, sb, cb):
293 '''Creation a rotation matrix.
295 @arg sa: C{sin(phi)} (C{float}).
296 @arg ca: C{cos(phi)} (C{float}).
297 @arg sb: C{sin(lambda)} (C{float}).
298 @arg cb: C{cos(lambda)} (C{float}).
300 @return: An L{EcefMatrix}.
301 '''
302 return self._xnamed(EcefMatrix(sa, ca, sb, cb))
304 def _polon(self, y, x, R, **lon00_name):
305 '''(INTERNAL) Handle I{"polar"} longitude.
306 '''
307 return atan2d(y, x) if R else _xkwds_get(lon00_name, lon00=self.lon00)
309 def reverse(self, xyz, y=None, z=None, M=False, **lon00_name): # PYCHOK no cover
310 '''I{Must be overloaded}.'''
311 self._notOverloaded(xyz, y=y, z=z, M=M, **lon00_name)
313 def toStr(self, prec=9, **unused): # PYCHOK signature
314 '''Return this C{Ecef*} as a string.
316 @kwarg prec: Precision, number of decimal digits (0..9).
318 @return: This C{Ecef*} (C{str}).
319 '''
320 return self.attrs(_a_, _f_, _datum_, _name_, prec=prec) # _ellipsoid_
323class EcefFarrell21(_EcefBase):
324 '''Conversion between geodetic and geocentric, I{Earth-Centered, Earth-Fixed} (ECEF)
325 coordinates based on I{Jay A. Farrell}'s U{Table 2.1<https://Books.Google.com/
326 books?id=fW4foWASY6wC>}, page 29.
327 '''
329 def reverse(self, xyz, y=None, z=None, M=None, **lon00_name): # PYCHOK unused M
330 '''Convert from geocentric C{(x, y, z)} to geodetic C{(lat, lon, height)} using
331 I{Farrell}'s U{Table 2.1<https://Books.Google.com/books?id=fW4foWASY6wC>},
332 page 29, aka the I{Heikkinen application} of U{Ferrari's solution
333 <https://WikiPedia.org/wiki/Geographic_coordinate_conversion>}.
335 @arg xyz: A geocentric (C{Cartesian}, L{Ecef9Tuple}) or C{scalar} ECEF C{x}
336 coordinate (C{meter}).
337 @kwarg y: ECEF C{y} coordinate for C{scalar} B{C{xyz}} and B{C{z}} (C{meter}).
338 @kwarg z: ECEF C{z} coordinate for C{scalar} B{C{xyz}} and B{C{y}} (C{meter}).
339 @kwarg M: I{Ignored}, rotation matrix C{M} not available.
340 @kwarg lon00_name: Optional C{B{name}=NN} (C{str}) and optional keyword argument
341 C{B{lon00}=INT0} (C{degrees}), an arbitrary I{"polar"} longitude
342 returned if C{B{x}=0} and C{B{y}=0}, see property C{lon00}.
344 @return: An L{Ecef9Tuple}C{(x, y, z, lat, lon, height, C, M, datum)} with
345 geodetic coordinates C{(lat, lon, height)} for the given geocentric
346 ones C{(x, y, z)}, case C{C=1}, C{M=None} always and C{datum}
347 if available.
349 @raise EcefError: Invalid B{C{xyz}} or C{scalar} C{x} or B{C{y}} and/or B{C{z}}
350 not C{scalar} for C{scalar} B{C{xyz}} or C{sqrt} domain or
351 zero division error.
353 @see: L{EcefFarrell22} and L{EcefVeness}.
354 '''
355 x, y, z, name = _xyzn4(xyz, y, z, self._Geocentrics, **lon00_name)
357 E = self.ellipsoid
358 a = E.a
359 a2 = E.a2
360 b2 = E.b2
361 e2 = E.e2
362 e2_ = E.e2abs * E.a2_b2 # (E.e * E.a_b)**2 = 0.0820944... WGS84
363 e4 = E.e4
365 try: # names as page 29
366 z2 = z**2
367 ez = z2 * (_1_0 - e2) # E.e2s2(z)
369 p = hypot(x, y)
370 p2 = p**2
371 G = p2 + ez - e2 * (a2 - b2) # p2 + ez - e4 * a2
372 F = b2 * z2 * 54
373 c = e4 * p2 * F / G**3
374 s = cbrt(sqrt(c * (c + _2_0)) + c + _1_0)
375 G *= fsumf_(s , _1_0, _1_0 / s) # k
376 P = F / (G**2 * _3_0)
377 Q = sqrt(_2_0 * e4 * P + _1_0)
378 Q1 = Q + _1_0
379 r0 = P * p * e2 / Q1 - sqrt(fsumf_(a2 * (Q1 / Q) * _0_5,
380 -P * ez / (Q * Q1),
381 -P * p2 * _0_5))
382 r = p + e2 * r0
383 v = b2 / (sqrt(r**2 + ez) * a) # z0 / z
385 h = hypot(r, z) * (_1_0 - v)
386 lat = atan1d((e2_ * v + _1_0) * z, p)
387 lon = self._polon(y, x, p, **lon00_name)
388 # note, phi and lam are swapped on page 29
390 except (ValueError, ZeroDivisionError) as X:
391 raise EcefError(x=x, y=y, z=z, cause=X)
393 return Ecef9Tuple(x, y, z, lat, lon, h,
394 1, None, self.datum,
395 name=self._name__(name))
398class EcefFarrell22(_EcefBase):
399 '''Conversion between geodetic and geocentric, I{Earth-Centered, Earth-Fixed} (ECEF)
400 coordinates based on I{Jay A. Farrell}'s U{Table 2.2<https://Books.Google.com/
401 books?id=fW4foWASY6wC>}, page 30.
402 '''
404 def reverse(self, xyz, y=None, z=None, M=None, **lon00_name): # PYCHOK unused M
405 '''Convert from geocentric C{(x, y, z)} to geodetic C{(lat, lon, height)} using
406 I{Farrell}'s U{Table 2.2<https://Books.Google.com/books?id=fW4foWASY6wC>},
407 page 30.
409 @arg xyz: A geocentric (C{Cartesian}, L{Ecef9Tuple}) or C{scalar} ECEF C{x}
410 coordinate (C{meter}).
411 @kwarg y: ECEF C{y} coordinate for C{scalar} B{C{xyz}} and B{C{z}} (C{meter}).
412 @kwarg z: ECEF C{z} coordinate for C{scalar} B{C{xyz}} and B{C{y}} (C{meter}).
413 @kwarg M: I{Ignored}, rotation matrix C{M} not available.
414 @kwarg lon00_name: Optional C{B{name}=NN} (C{str}) and optional keyword argument
415 C{B{lon00}=INT0} (C{degrees}), an arbitrary I{"polar"} longitude
416 returned if C{B{x}=0} and C{B{y}=0}, see property C{lon00}.
418 @return: An L{Ecef9Tuple}C{(x, y, z, lat, lon, height, C, M, datum)} with
419 geodetic coordinates C{(lat, lon, height)} for the given geocentric
420 ones C{(x, y, z)}, case C{C=1}, C{M=None} always and C{datum}
421 if available.
423 @raise EcefError: Invalid B{C{xyz}} or C{scalar} C{x} or B{C{y}} and/or B{C{z}}
424 not C{scalar} for C{scalar} B{C{xyz}} or C{sqrt} domain or
425 zero division error.
427 @see: L{EcefFarrell21} and L{EcefVeness}.
428 '''
429 x, y, z, name = _xyzn4(xyz, y, z, self._Geocentrics, **lon00_name)
431 E = self.ellipsoid
432 a = E.a
433 b = E.b
435 try: # see EcefVeness.reverse
436 p = hypot(x, y)
437 lon = self._polon(y, x, p, **lon00_name)
439 s, c = sincos2(atan2(z * a, p * b)) # == _norm3
440 lat = atan1d(z + s**3 * b * E.e22,
441 p - c**3 * a * E.e2)
443 s, c = sincos2d(lat)
444 if c: # E.roc1_(s) = E.a / sqrt(1 - E.e2 * s**2)
445 h = p / c - (E.roc1_(s) if s else a)
446 else: # polar
447 h = fabs(z) - b
448 # note, phi and lam are swapped on page 30
450 except (ValueError, ZeroDivisionError) as e:
451 raise EcefError(x=x, y=y, z=z, cause=e)
453 return Ecef9Tuple(x, y, z, lat, lon, h,
454 1, None, self.datum,
455 name=self._name__(name))
458class EcefKarney(_EcefBase):
459 '''Conversion between geodetic and geocentric, I{Earth-Centered, Earth-Fixed} (ECEF)
460 coordinates transcoded from I{Karney}'s C++ U{Geocentric<https://GeographicLib.SourceForge.io/
461 C++/doc/classGeographicLib_1_1Geocentric.html>} methods.
463 @note: On methods C{.forward} and C{.forwar_}, let C{v} be a unit vector located
464 at C{(lat, lon, h)}. We can express C{v} as column vectors in one of two
465 ways, C{v1} in East, North, Up (ENU) coordinates (where the components are
466 relative to a local coordinate system at C{C(lat0, lon0, h0)}) or as C{v0}
467 in geocentric C{x, y, z} coordinates. Then, M{v0 = M ⋅ v1} where C{M} is
468 the rotation matrix.
469 '''
471 @Property_RO
472 def hmax(self):
473 '''Get the distance or height limit (C{meter}, conventionally).
474 '''
475 return self.equatoradius / EPS_2 # self.equatoradius * _2_EPS, 12M lighyears
477 def reverse(self, xyz, y=None, z=None, M=False, **lon00_name):
478 '''Convert from geocentric C{(x, y, z)} to geodetic C{(lat, lon, height)}.
480 @arg xyz: A geocentric (C{Cartesian}, L{Ecef9Tuple}) or C{scalar} ECEF C{x}
481 coordinate (C{meter}).
482 @kwarg y: ECEF C{y} coordinate for C{scalar} B{C{xyz}} and B{C{z}} (C{meter}).
483 @kwarg z: ECEF C{z} coordinate for C{scalar} B{C{xyz}} and B{C{y}} (C{meter}).
484 @kwarg M: Optionally, return the rotation L{EcefMatrix} (C{bool}).
485 @kwarg lon00_name: Optional C{B{name}=NN} (C{str}) and optional keyword argument
486 C{B{lon00}=INT0} (C{degrees}), an arbitrary I{"polar"} longitude
487 returned if C{B{x}=0} and C{B{y}=0}, see property C{lon00}.
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.
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}}.
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)
511 x, y, z, name = _xyzn4(xyz, y, z, self._Geocentrics, **lon00_name)
513 E = self.ellipsoid
514 f = E.f
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
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
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
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
605 # lon00 <https://GitHub.com/mrJean1/PyGeodesy/issues/77>
606 lon = self._polon(sb, cb, R, **lon00_name)
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, name=self._name__(name))
612class EcefSudano(_EcefBase):
613 '''Conversion between geodetic and geocentric, I{Earth-Centered, Earth-Fixed} (ECEF) coordinates
614 based on I{John J. Sudano}'s U{paper<https://www.ResearchGate.net/publication/3709199>}.
615 '''
616 _tol = EPS2
618 def reverse(self, xyz, y=None, z=None, M=None, **lon00_name): # PYCHOK unused M
619 '''Convert from geocentric C{(x, y, z)} to geodetic C{(lat, lon, height)} using
620 I{Sudano}'s U{iterative method<https://www.ResearchGate.net/publication/3709199>}.
622 @arg xyz: A geocentric (C{Cartesian}, L{Ecef9Tuple}) or C{scalar} ECEF C{x}
623 coordinate (C{meter}).
624 @kwarg y: ECEF C{y} coordinate for C{scalar} B{C{xyz}} and B{C{z}} (C{meter}).
625 @kwarg z: ECEF C{z} coordinate for C{scalar} B{C{xyz}} and B{C{y}} (C{meter}).
626 @kwarg M: I{Ignored}, rotation matrix C{M} not available.
627 @kwarg lon00_name: Optional C{B{name}=NN} (C{str}) and optional keyword argument
628 C{B{lon00}=INT0} (C{degrees}), an arbitrary I{"polar"} longitude
629 returned if C{B{x}=0} and C{B{y}=0}, see property C{lon00}.
631 @return: An L{Ecef9Tuple}C{(x, y, z, lat, lon, height, C, M, datum)} with geodetic
632 coordinates C{(lat, lon, height)} for the given geocentric ones C{(x, y, z)},
633 iteration C{C}, C{M=None} always and C{datum} if available.
635 @raise EcefError: Invalid B{C{xyz}} or C{scalar} C{x} or B{C{y}} and/or B{C{z}}
636 not C{scalar} for C{scalar} B{C{xyz}} or no convergence.
637 '''
638 x, y, z, name = _xyzn4(xyz, y, z, self._Geocentrics, **lon00_name)
640 E = self.ellipsoid
641 e = E.e2 * E.a
642 R = hypot(x, y) # Rh
643 d = e - R
645 lat = atan1d(z, R * E.e21)
646 sa, ca = sincos2d(fabs(lat))
647 # Sudano's Eq (A-6) and (A-7) refactored/reduced,
648 # replacing Rn from Eq (A-4) with n = E.a / ca:
649 # N = ca**2 * ((z + E.e2 * n * sa) * ca - R * sa)
650 # = ca**2 * (z * ca + E.e2 * E.a * sa - R * sa)
651 # = ca**2 * (z * ca + (E.e2 * E.a - R) * sa)
652 # D = ca**3 * (E.e2 * n / E.e2s2(sa)) - R
653 # = ca**2 * (E.e2 * E.a / E.e2s2(sa) - R / ca**2)
654 # N / D = (z * ca + (E.e2 * E.a - R) * sa) /
655 # (E.e2 * E.a / E.e2s2(sa) - R / ca**2)
656 tol = self.tolerance
657 _S2 = Fsum(sa).fsum2f_
658 for i in range(1, _TRIPS):
659 ca2 = _1_0 - sa**2
660 if ca2 < EPS_2: # PYCHOK no cover
661 ca = _0_0
662 break
663 ca = sqrt(ca2)
664 r = e / E.e2s2(sa) - R / ca2
665 if fabs(r) < EPS_2:
666 break
667 lat = None
668 sa, t = _S2(-z * ca / r, -d * sa / r)
669 if fabs(t) < tol:
670 break
671 else:
672 t = unstr(self.reverse, x=x, y=y, z=z)
673 raise EcefError(t, txt=Fmt.no_convergence(r, tol))
675 if lat is None:
676 lat = copysign0(atan1d(fabs(sa), ca), z)
677 lon = self._polon(y, x, R, **lon00_name)
679 h = fsumf_(R * ca, fabs(z * sa), -E.a * E.e2s(sa)) # use Veness'
680 # because Sudano's Eq (7) doesn't produce the correct height
681 # h = (fabs(z) + R - E.a * cos(a + E.e21) * sa / ca) / (ca + sa)
682 return Ecef9Tuple(x, y, z, lat, lon, h,
683 i, None, self.datum, # C=i, M=None
684 iteration=i, name=self._name__(name))
686 @property_doc_(''' the convergence tolerance (C{float}).''')
687 def tolerance(self):
688 '''Get the convergence tolerance (C{scalar}).
689 '''
690 return self._tol
692 @tolerance.setter # PYCHOK setter!
693 def tolerance(self, tol):
694 '''Set the convergence tolerance (C{scalar}).
696 @raise EcefError: Non-scalar or invalid B{C{tol}}.
697 '''
698 self._tol = Scalar_(tolerance=tol, low=EPS, Error=EcefError)
701class EcefVeness(_EcefBase):
702 '''Conversion between geodetic and geocentric, I{Earth-Centered, Earth-Fixed} (ECEF) coordinates
703 transcoded from I{Chris Veness}' JavaScript classes U{LatLonEllipsoidal, Cartesian<https://
704 www.Movable-Type.co.UK/scripts/geodesy/docs/latlon-ellipsoidal.js.html>}.
706 @see: U{I{A Guide to Coordinate Systems in Great Britain}<https://www.OrdnanceSurvey.co.UK/
707 documents/resources/guide-coordinate-systems-great-britain.pdf>}, section I{B) Converting
708 between 3D Cartesian and ellipsoidal latitude, longitude and height coordinates}.
709 '''
711 def reverse(self, xyz, y=None, z=None, M=None, **lon00_name): # PYCHOK unused M
712 '''Conversion from geocentric C{(x, y, z)} to geodetic C{(lat, lon, height)}
713 transcoded from I{Chris Veness}' U{JavaScript<https://www.Movable-Type.co.UK/
714 scripts/geodesy/docs/latlon-ellipsoidal.js.html>}.
716 Uses B. R. Bowring’s formulation for μm precision in concise form U{I{The accuracy
717 of geodetic latitude and height equations}<https://www.ResearchGate.net/publication/
718 233668213>}, Survey Review, Vol 28, 218, Oct 1985.
720 @arg xyz: A geocentric (C{Cartesian}, L{Ecef9Tuple}) or C{scalar} ECEF C{x}
721 coordinate (C{meter}).
722 @kwarg y: ECEF C{y} coordinate for C{scalar} B{C{xyz}} and B{C{z}} (C{meter}).
723 @kwarg z: ECEF C{z} coordinate for C{scalar} B{C{xyz}} and B{C{y}} (C{meter}).
724 @kwarg M: I{Ignored}, rotation matrix C{M} not available.
725 @kwarg lon00_name: Optional C{B{name}=NN} (C{str}) and optional keyword argument
726 C{B{lon00}=INT0} (C{degrees}), an arbitrary I{"polar"} longitude
727 returned if C{B{x}=0} and C{B{y}=0}, see property C{lon00}.
729 @return: An L{Ecef9Tuple}C{(x, y, z, lat, lon, height, C, M, datum)} with
730 geodetic coordinates C{(lat, lon, height)} for the given geocentric
731 ones C{(x, y, z)}, case C{C}, C{M=None} always and C{datum} if available.
733 @raise EcefError: Invalid B{C{xyz}} or C{scalar} C{x} or B{C{y}} and/or B{C{z}}
734 not C{scalar} for C{scalar} B{C{xyz}}.
736 @see: Toms, Ralph M. U{I{An Efficient Algorithm for Geocentric to Geodetic
737 Coordinate Conversion}<https://www.OSTI.gov/scitech/biblio/110235>},
738 Sept 1995 and U{I{An Improved Algorithm for Geocentric to Geodetic
739 Coordinate Conversion}<https://www.OSTI.gov/scitech/servlets/purl/231228>},
740 Apr 1996, both from Lawrence Livermore National Laboratory (LLNL) and
741 Sudano, John J, U{I{An exact conversion from an Earth-centered coordinate
742 system to latitude longitude and altitude}<https://www.ResearchGate.net/
743 publication/3709199>}.
744 '''
745 x, y, z, name = _xyzn4(xyz, y, z, self._Geocentrics, **lon00_name)
747 E = self.ellipsoid
748 a = E.a
750 p = hypot(x, y) # distance from minor axis
751 r = hypot(p, z) # polar radius
752 if min(p, r) > EPS0:
753 b = E.b * E.e22
754 # parametric latitude (Bowring eqn 17, replaced)
755 t = (E.b * z) / (a * p) * (_1_0 + b / r)
756 c = _1_0 / hypot1(t)
757 s = c * t
758 # geodetic latitude (Bowring eqn 18)
759 lat = atan1d(z + s**3 * b,
760 p - c**3 * a * E.e2)
762 # height above ellipsoid (Bowring eqn 7)
763 sa, ca = sincos2d(lat)
764# r = a / E.e2s(sa) # length of normal terminated by minor axis
765# h = p * ca + z * sa - (a * a / r)
766 h = fsumf_(p * ca, z * sa, -a * E.e2s(sa))
767 C = 1
769 # see <https://GIS.StackExchange.com/questions/28446>
770 elif p > EPS: # lat arbitrarily zero, equatorial lon
771 C, lat, h = 2, _0_0, (p - a)
773 else: # polar lat, lon arbitrarily lon00
774 C, lat, h = 3, (_N_90_0 if z < 0 else _90_0), (fabs(z) - E.b)
776 lon = self._polon(y, x, p, **lon00_name)
777 return Ecef9Tuple(x, y, z, lat, lon, h,
778 C, None, self.datum, # M=None
779 name=self._name__(name))
782class EcefYou(_EcefBase):
783 '''Conversion between geodetic and geocentric, I{Earth-Centered, Earth-Fixed} (ECEF) coordinates
784 using I{Rey-Jer You}'s U{transformation<https://www.ResearchGate.net/publication/240359424>}
785 for I{non-prolate} ellipsoids.
787 @see: Featherstone, W.E., Claessens, S.J. U{I{Closed-form transformation between geodetic and
788 ellipsoidal coordinates}<https://Espace.Curtin.edu.AU/bitstream/handle/20.500.11937/
789 11589/115114_9021_geod2ellip_final.pdf>} Studia Geophysica et Geodaetica, 2008, 52,
790 pages 1-18 and U{PyMap3D <https://PyPI.org/project/pymap3d>}.
791 '''
793 def __init__(self, a_ellipsoid=_EWGS84, f=None, **lon00_name): # PYCHOK signature
794 _EcefBase.__init__(self, a_ellipsoid, f=f, **lon00_name) # inherited documentation
795 self._ee2 = EcefYou._ee2(self.ellipsoid)
797 @staticmethod
798 def _ee2(E):
799 e2 = E.a2 - E.b2
800 if e2 < 0 or E.f < 0:
801 raise EcefError(ellipsoid=E, txt=_prolate_)
802 return sqrt0(e2), e2
804 def reverse(self, xyz, y=None, z=None, M=None, **lon00_name): # PYCHOK unused M
805 '''Convert geocentric C{(x, y, z)} to geodetic C{(lat, lon, height)}
806 using I{Rey-Jer You}'s transformation.
808 @arg xyz: A geocentric (C{Cartesian}, L{Ecef9Tuple}) or C{scalar} ECEF C{x}
809 coordinate (C{meter}).
810 @kwarg y: ECEF C{y} coordinate for C{scalar} B{C{xyz}} and B{C{z}} (C{meter}).
811 @kwarg z: ECEF C{z} coordinate for C{scalar} B{C{xyz}} and B{C{y}} (C{meter}).
812 @kwarg M: I{Ignored}, rotation matrix C{M} not available.
813 @kwarg lon00_name: Optional C{B{name}=NN} (C{str}) and optional keyword argument
814 C{B{lon00}=INT0} (C{degrees}), an arbitrary I{"polar"} longitude
815 returned if C{B{x}=0} and C{B{y}=0}, see property C{lon00}.
817 @return: An L{Ecef9Tuple}C{(x, y, z, lat, lon, height, C, M, datum)} with
818 geodetic coordinates C{(lat, lon, height)} for the given geocentric
819 ones C{(x, y, z)}, case C{C=1}, C{M=None} always and C{datum} if
820 available.
822 @raise EcefError: Invalid B{C{xyz}} or C{scalar} C{x} or B{C{y}} and/or
823 B{C{z}} not C{scalar} for C{scalar} B{C{xyz}} or the
824 ellipsoid is I{prolate}.
825 '''
826 x, y, z, name = _xyzn4(xyz, y, z, self._Geocentrics, **lon00_name)
828 E = self.ellipsoid
829 e, e2 = self._ee2
831 q = hypot(x, y) # R
832 u = Fpowers(2, x, y, z) - e2
833 u = u.fadd_(hypot(u, e * z * _2_0)).fover(_2_0)
834 if u > EPS02:
835 u = sqrt(u)
836 p = hypot(u, e)
837 B = atan1(p * z, u * q) # beta0 = atan(p / u * z / q)
838 sB, cB = sincos2(B)
839 if cB and sB:
840 p *= E.a
841 d = (p / cB - e2 * cB) / sB
842 if isnon0(d):
843 B += fsumf_(u * E.b, -p, e2) / d
844 sB, cB = sincos2(B)
845 elif u < (-EPS2):
846 raise EcefError(x=x, y=y, z=z, u=u, txt=_singular_)
847 else:
848 sB, cB = _copysign_1_0(z), _0_0
850 lat = atan1d(E.a * sB, E.b * cB) # atan(E.a_b * tan(B))
851 lon = self._polon(y, x, q, **lon00_name)
853 h = hypot(z - E.b * sB, q - E.a * cB)
854 if hypot2_(x, y, z * E.a_b) < E.a2:
855 h = neg(h) # inside ellipsoid
856 return Ecef9Tuple(x, y, z, lat, lon, h,
857 1, None, self.datum, # C=1, M=None
858 name=self._name__(name))
861class EcefMatrix(_NamedTuple):
862 '''A rotation matrix known as I{East-North-Up (ENU) to ECEF}.
864 @see: U{From ENU to ECEF<https://WikiPedia.org/wiki/
865 Geographic_coordinate_conversion#From_ECEF_to_ENU>} and
866 U{Issue #74<https://Github.com/mrJean1/PyGeodesy/issues/74>}.
867 '''
868 _Names_ = ('_0_0_', '_0_1_', '_0_2_', # row-order
869 '_1_0_', '_1_1_', '_1_2_',
870 '_2_0_', '_2_1_', '_2_2_')
871 _Units_ = (Scalar,) * len(_Names_)
873 def _validate(self, **unused): # PYCHOK unused
874 '''(INTERNAL) Allow C{_Names_} with leading underscore.
875 '''
876 _NamedTuple._validate(self, underOK=True)
878 def __new__(cls, sa, ca, sb, cb, *_more):
879 '''New L{EcefMatrix} matrix.
881 @arg sa: C{sin(phi)} (C{float}).
882 @arg ca: C{cos(phi)} (C{float}).
883 @arg sb: C{sin(lambda)} (C{float}).
884 @arg cb: C{cos(lambda)} (C{float}).
885 @arg _more: (INTERNAL) from C{.multiply}.
887 @raise EcefError: If B{C{sa}}, B{C{ca}}, B{C{sb}} or
888 B{C{cb}} outside M{[-1.0, +1.0]}.
889 '''
890 t = sa, ca, sb, cb
891 if _more: # all 9 matrix elements ...
892 t += _more # ... from .multiply
894 elif max(map(fabs, t)) > _1_0:
895 raise EcefError(unstr(EcefMatrix, *t))
897 else: # build matrix from the following quaternion operations
898 # qrot(lam, [0,0,1]) * qrot(phi, [0,-1,0]) * [1,1,1,1]/2
899 # or
900 # qrot(pi/2 + lam, [0,0,1]) * qrot(-pi/2 + phi, [-1,0,0])
901 # where
902 # qrot(t,v) = [cos(t/2), sin(t/2)*v[1], sin(t/2)*v[2], sin(t/2)*v[3]]
904 # Local X axis (East) in geocentric coords
905 # M[0] = -slam; M[3] = clam; M[6] = 0;
906 # Local Y axis (North) in geocentric coords
907 # M[1] = -clam * sphi; M[4] = -slam * sphi; M[7] = cphi;
908 # Local Z axis (Up) in geocentric coords
909 # M[2] = clam * cphi; M[5] = slam * cphi; M[8] = sphi;
910 t = (-sb, -cb * sa, cb * ca,
911 cb, -sb * sa, sb * ca,
912 _0_0, ca, sa)
914 return _NamedTuple.__new__(cls, *t)
916 def column(self, column):
917 '''Get this matrix' B{C{column}} 0, 1 or 2 as C{3-tuple}.
918 '''
919 if 0 <= column < 3:
920 return self[column::3]
921 raise _IndexError(column=column)
923 def copy(self, **unused): # PYCHOK signature
924 '''Make a shallow or deep copy of this instance.
926 @return: The copy (C{This class} or subclass thereof).
927 '''
928 return self.classof(*self)
930 __copy__ = __deepcopy__ = copy
932 @Property_RO
933 def matrix3(self):
934 '''Get this matrix' rows (C{3-tuple} of 3 C{3-tuple}s).
935 '''
936 return tuple(map(self.row, range(3)))
938 @Property_RO
939 def matrixTransposed3(self):
940 '''Get this matrix' I{Transposed} rows (C{3-tuple} of 3 C{3-tuple}s).
941 '''
942 return tuple(map(self.column, range(3)))
944 def multiply(self, other):
945 '''Matrix multiply M{M0' ⋅ M} this matrix I{Transposed}
946 with an other matrix.
948 @arg other: The other matrix (L{EcefMatrix}).
950 @return: The matrix product (L{EcefMatrix}).
952 @raise TypeError: If B{C{other}} is not an L{EcefMatrix}.
953 '''
954 _xinstanceof(EcefMatrix, other=other)
955 # like LocalCartesian.MatrixMultiply, C{self.matrixTransposed3 X other.matrix3}
956 # <https://GeographicLib.SourceForge.io/C++/doc/LocalCartesian_8cpp_source.html>
957 # X = (fdot(self.column(r), *other.column(c)) for r in (0,1,2) for c in (0,1,2))
958 X = (fdot(self[r::3], *other[c::3]) for r in range(3) for c in range(3))
959 return _xnamed(EcefMatrix(*X), EcefMatrix.multiply.__name__)
961 def rotate(self, xyz, *xyz0):
962 '''Forward rotation M{M0' ⋅ ([x, y, z] - [x0, y0, z0])'}.
964 @arg xyz: Local C{(x, y, z)} coordinates (C{3-tuple}).
965 @arg xyz0: Optional, local C{(x0, y0, z0)} origin (C{3-tuple}).
967 @return: Rotated C{(x, y, z)} location (C{3-tuple}).
969 @raise LenError: Unequal C{len(B{xyz})} and C{len(B{xyz0})}.
970 '''
971 if xyz0:
972 if len(xyz0) != len(xyz):
973 raise LenError(self.rotate, xyz0=len(xyz0), xyz=len(xyz))
974 xyz = tuple(c - c0 for c, c0 in zip(xyz, xyz0))
976 # x' = M[0] * x + M[3] * y + M[6] * z
977 # y' = M[1] * x + M[4] * y + M[7] * z
978 # z' = M[2] * x + M[5] * y + M[8] * z
979 return (fdot(xyz, *self[0::3]), # .column(0)
980 fdot(xyz, *self[1::3]), # .column(1)
981 fdot(xyz, *self[2::3])) # .column(2)
983 def row(self, row):
984 '''Get this matrix' B{C{row}} 0, 1 or 2 as C{3-tuple}.
985 '''
986 if 0 <= row < 3:
987 r = row * 3
988 return self[r:r+3]
989 raise _IndexError(row=row)
991 def unrotate(self, xyz, *xyz0):
992 '''Inverse rotation M{[x0, y0, z0] + M0 ⋅ [x,y,z]'}.
994 @arg xyz: Local C{(x, y, z)} coordinates (C{3-tuple}).
995 @arg xyz0: Optional, local C{(x0, y0, z0)} origin (C{3-tuple}).
997 @return: Unrotated C{(x, y, z)} location (C{3-tuple}).
999 @raise LenError: Unequal C{len(B{xyz})} and C{len(B{xyz0})}.
1000 '''
1001 if xyz0:
1002 if len(xyz0) != len(xyz):
1003 raise LenError(self.unrotate, xyz0=len(xyz0), xyz=len(xyz))
1004 _xyz = _1_0_1T + xyz
1005 # x' = x0 + M[0] * x + M[1] * y + M[2] * z
1006 # y' = y0 + M[3] * x + M[4] * y + M[5] * z
1007 # z' = z0 + M[6] * x + M[7] * y + M[8] * z
1008 xyz_ = (fdot(_xyz, xyz0[0], *self[0:3]), # .row(0)
1009 fdot(_xyz, xyz0[1], *self[3:6]), # .row(1)
1010 fdot(_xyz, xyz0[2], *self[6:9])) # .row(2)
1011 else:
1012 # x' = M[0] * x + M[1] * y + M[2] * z
1013 # y' = M[3] * x + M[4] * y + M[5] * z
1014 # z' = M[6] * x + M[7] * y + M[8] * z
1015 xyz_ = (fdot(xyz, *self[0:3]), # .row(0)
1016 fdot(xyz, *self[3:6]), # .row(1)
1017 fdot(xyz, *self[6:9])) # .row(2)
1018 return xyz_
1021class Ecef9Tuple(_NamedTuple, _NamedLocal):
1022 '''9-Tuple C{(x, y, z, lat, lon, height, C, M, datum)} with I{geocentric} C{x},
1023 C{y} and C{z} plus I{geodetic} C{lat}, C{lon} and C{height}, case C{C} (see
1024 the C{Ecef*.reverse} methods) and optionally, rotation matrix C{M} (L{EcefMatrix})
1025 and C{datum}, with C{lat} and C{lon} in C{degrees} and C{x}, C{y}, C{z} and
1026 C{height} in C{meter}, conventionally.
1027 '''
1028 _Names_ = (_x_, _y_, _z_, _lat_, _lon_, _height_, _C_, _M_, _datum_)
1029 _Units_ = ( Meter, Meter, Meter, Lat, Lon, Height, Int, _Pass, _Pass)
1031 @property_ROver
1032 def _CartesianBase(self):
1033 '''(INTERNAL) Get class C{CartesianBase}, I{once}.
1034 '''
1035 return _MODS.cartesianBase.CartesianBase # overwrite property_ROver
1037 @deprecated_method
1038 def convertDatum(self, datum2): # for backward compatibility
1039 '''DEPRECATED, use method L{toDatum}.'''
1040 return self.toDatum(datum2)
1042 @property_RO
1043 def _ecef9(self):
1044 return self
1046 @Property_RO
1047 def lam(self):
1048 '''Get the longitude in C{radians} (C{float}).
1049 '''
1050 return self.philam.lam
1052 @Property_RO
1053 def lamVermeille(self):
1054 '''Get the longitude in C{radians} M{[-PI*3/2..+PI*3/2]} after U{Vermeille
1055 <https://Search.ProQuest.com/docview/639493848>} (2004), page 95.
1057 @see: U{Karney<https://GeographicLib.SourceForge.io/C++/doc/geocentric.html>},
1058 U{Vermeille<https://Search.ProQuest.com/docview/847292978>} 2011, pp 112-113, 116
1059 and U{Featherstone, et.al.<https://Search.ProQuest.com/docview/872827242>}, page 7.
1060 '''
1061 x, y = self.x, self.y
1062 if y > EPS0:
1063 r = atan2(x, hypot(y, x) + y) * _N_2_0 + PI_2
1064 elif y < -EPS0:
1065 r = atan2(x, hypot(y, x) - y) * _2_0 - PI_2
1066 else: # y == 0
1067 r = PI if x < 0 else _0_0
1068 return Lam(Vermeille=r)
1070 @Property_RO
1071 def latlon(self):
1072 '''Get the lat-, longitude in C{degrees} (L{LatLon2Tuple}C{(lat, lon)}).
1073 '''
1074 return LatLon2Tuple(self.lat, self.lon, name=self.name)
1076 @Property_RO
1077 def latlonheight(self):
1078 '''Get the lat-, longitude in C{degrees} and height (L{LatLon3Tuple}C{(lat, lon, height)}).
1079 '''
1080 return self.latlon.to3Tuple(self.height)
1082 @Property_RO
1083 def latlonheightdatum(self):
1084 '''Get the lat-, longitude in C{degrees} with height and datum (L{LatLon4Tuple}C{(lat, lon, height, datum)}).
1085 '''
1086 return self.latlonheight.to4Tuple(self.datum)
1088 @Property_RO
1089 def latlonVermeille(self):
1090 '''Get the latitude and I{Vermeille} longitude in C{degrees [-225..+225]} (L{LatLon2Tuple}C{(lat, lon)}).
1092 @see: Property C{lonVermeille}.
1093 '''
1094 return LatLon2Tuple(self.lat, self.lonVermeille, name=self.name)
1096 @Property_RO
1097 def lonVermeille(self):
1098 '''Get the longitude in C{degrees [-225..+225]} after U{Vermeille
1099 <https://Search.ProQuest.com/docview/639493848>} 2004, p 95.
1101 @see: Property C{lamVermeille}.
1102 '''
1103 return Lon(Vermeille=degrees(self.lamVermeille))
1105 def _ltp_toLocal(self, ltp, Xyz_kwds, **Xyz): # overloads C{_NamedLocal}'s
1106 '''(INTERNAL) Invoke C{ltp._xLtp(ltp)._ecef2local}.
1107 '''
1108 Xyz_ = self._ltp_toLocal2(Xyz_kwds, **Xyz) # in ._NamedLocal
1109 ltp = self._ltp._xLtp(ltp, self._Ltp) # both in ._NamedLocal
1110 return ltp._ecef2local(self, *Xyz_)
1112 @Property_RO
1113 def phi(self):
1114 '''Get the latitude in C{radians} (C{float}).
1115 '''
1116 return self.philam.phi
1118 @Property_RO
1119 def philam(self):
1120 '''Get the lat-, longitude in C{radians} (L{PhiLam2Tuple}C{(phi, lam)}).
1121 '''
1122 return PhiLam2Tuple(radians(self.lat), radians(self.lon), name=self.name)
1124 @Property_RO
1125 def philamheight(self):
1126 '''Get the lat-, longitude in C{radians} and height (L{PhiLam3Tuple}C{(phi, lam, height)}).
1127 '''
1128 return self.philam.to3Tuple(self.height)
1130 @Property_RO
1131 def philamheightdatum(self):
1132 '''Get the lat-, longitude in C{radians} with height and datum (L{PhiLam4Tuple}C{(phi, lam, height, datum)}).
1133 '''
1134 return self.philamheight.to4Tuple(self.datum)
1136 @Property_RO
1137 def philamVermeille(self):
1138 '''Get the latitude and I{Vermeille} longitude in C{radians [-PI*3/2..+PI*3/2]} (L{PhiLam2Tuple}C{(phi, lam)}).
1140 @see: Property C{lamVermeille}.
1141 '''
1142 return PhiLam2Tuple(radians(self.lat), self.lamVermeille, name=self.name)
1144 def toCartesian(self, Cartesian=None, **Cartesian_kwds):
1145 '''Return the geocentric C{(x, y, z)} coordinates as an ellipsoidal or spherical
1146 C{Cartesian}.
1148 @kwarg Cartesian: Optional class to return C{(x, y, z)} (L{ellipsoidalKarney.Cartesian},
1149 L{ellipsoidalNvector.Cartesian}, L{ellipsoidalVincenty.Cartesian},
1150 L{sphericalNvector.Cartesian} or L{sphericalTrigonometry.Cartesian})
1151 or C{None}.
1152 @kwarg Cartesian_kwds: Optionally, additional B{C{Cartesian}} keyword arguments, ignored
1153 if C{B{Cartesian} is None}.
1155 @return: A B{C{Cartesian}} instance or a L{Vector4Tuple}C{(x, y, z, h)} if C{B{Cartesian}
1156 is None}.
1158 @raise TypeError: Invalid B{C{Cartesian}} or B{C{Cartesian_kwds}} item.
1159 '''
1160 if Cartesian in (None, Vector4Tuple):
1161 r = self.xyzh
1162 elif Cartesian is Vector3Tuple:
1163 r = self.xyz
1164 else:
1165 _xsubclassof(self._CartesianBase, Cartesian=Cartesian)
1166 r = Cartesian(self, **_name1__(Cartesian_kwds, _or_nameof=self))
1167 return r
1169 def toDatum(self, datum2, **name):
1170 '''Convert this C{Ecef9Tuple} to an other datum.
1172 @arg datum2: Datum to convert I{to} (L{Datum}).
1173 @kwarg name: Optional C{B{name}=NN} (C{str}).
1175 @return: The converted 9-Tuple (C{Ecef9Tuple}).
1177 @raise TypeError: The B{C{datum2}} is not a L{Datum}.
1178 '''
1179 n = _name__(name, _or_nameof=self)
1180 if self.datum in (None, datum2): # PYCHOK _Names_
1181 r = self.copy(name=n)
1182 else:
1183 c = self._CartesianBase(self, datum=self.datum, name=n) # PYCHOK _Names_
1184 # c.toLatLon converts datum, x, y, z, lat, lon, etc.
1185 # and returns another Ecef9Tuple iff LatLon is None
1186 r = c.toLatLon(datum=datum2, LatLon=None)
1187 return r
1189 def toLatLon(self, LatLon=None, **LatLon_kwds):
1190 '''Return the geodetic C{(lat, lon, height[, datum])} coordinates.
1192 @kwarg LatLon: Optional class to return C{(lat, lon, height[, datum])} or C{None}.
1193 @kwarg LatLon_kwds: Optional B{C{height}}, B{C{datum}} and other B{C{LatLon}}
1194 keyword arguments.
1196 @return: A B{C{LatLon}} instance or if C{B{LatLon} is None}, a L{LatLon4Tuple}C{(lat,
1197 lon, height, datum)} or L{LatLon3Tuple}C{(lat, lon, height)} if C{datum} is
1198 specified or not.
1200 @raise TypeError: Invalid B{C{LatLon}} or B{C{LatLon_kwds}} item.
1201 '''
1202 lat, lon, D = self.lat, self.lon, self.datum # PYCHOK Ecef9Tuple
1203 kwds = _name1__(LatLon_kwds, _or_nameof=self)
1204 kwds = _xkwds(kwds, height=self.height, datum=D) # PYCHOK Ecef9Tuple
1205 d = kwds.get(_datum_, LatLon)
1206 if LatLon is None:
1207 r = LatLon3Tuple(lat, lon, kwds[_height_], name=kwds[_name_])
1208 if d is not None:
1209 # assert d is not LatLon
1210 r = r.to4Tuple(d) # checks type(d)
1211 else:
1212 if d is None:
1213 _ = kwds.pop(_datum_) # remove None datum
1214 r = LatLon(lat, lon, **kwds)
1215 _xdatum(_xattr(r, datum=D), D)
1216 return r
1218 def toVector(self, Vector=None, **Vector_kwds):
1219 '''Return these geocentric C{(x, y, z)} coordinates as vector.
1221 @kwarg Vector: Optional vector class to return C{(x, y, z)} or C{None}.
1222 @kwarg Vector_kwds: Optional, additional B{C{Vector}} keyword arguments,
1223 ignored if C{B{Vector} is None}.
1225 @return: A B{C{Vector}} instance or a L{Vector3Tuple}C{(x, y, z)} if
1226 C{B{Vector} is None}.
1228 @raise TypeError: Invalid B{C{Vector}} or B{C{Vector_kwds}} item.
1230 @see: Propertes C{xyz} and C{xyzh}
1231 '''
1232 return self.xyz if Vector is None else Vector(
1233 *self.xyz, **_name1__(Vector_kwds, _or_nameof=self)) # PYCHOK Ecef9Tuple
1235# def _T_x_M(self, T):
1236# '''(INTERNAL) Update M{self.M = T.multiply(self.M)}.
1237# '''
1238# return self.dup(M=T.multiply(self.M))
1240 @Property_RO
1241 def xyz(self):
1242 '''Get the geocentric C{(x, y, z)} coordinates (L{Vector3Tuple}C{(x, y, z)}).
1243 '''
1244 return Vector3Tuple(self.x, self.y, self.z, name=self.name)
1246 @Property_RO
1247 def xyzh(self):
1248 '''Get the geocentric C{(x, y, z)} coordinates and C{height} (L{Vector4Tuple}C{(x, y, z, h)})
1249 '''
1250 return self.xyz.to4Tuple(self.height)
1253def _4Ecef(this, Ecef): # in .datums.Datum.ecef, .ellipsoids.Ellipsoid.ecef
1254 '''Return an ECEF converter for C{this} L{Datum} or L{Ellipsoid}.
1255 '''
1256 if Ecef is None:
1257 Ecef = EcefKarney
1258 else:
1259 _xinstanceof(*_Ecefs, Ecef=Ecef)
1260 return Ecef(this, name=this.name)
1263def _llhn4(latlonh, lon, height, suffix=NN, Error=EcefError, **name): # in .ltp
1264 '''(INTERNAL) Get a C{(lat, lon, h, name)} 4-tuple.
1265 '''
1266 try:
1267 lat, lon = latlonh.lat, latlonh.lon
1268 h = _xattr(latlonh, height=_xattr(latlonh, h=height))
1269 n = _name__(name, _or_nameof=latlonh) # == latlonh._name__(name)
1270 except AttributeError:
1271 lat, h, n = latlonh, height, _name__(**name)
1272 try:
1273 return Lat(lat), Lon(lon), Height(h), n
1274 except (TypeError, ValueError) as x:
1275 t = _lat_, _lon_, _height_
1276 if suffix:
1277 t = (_ + suffix for _ in t)
1278 d = dict(zip(t, (lat, lon, h)))
1279 raise Error(cause=x, **d)
1282def _xEcef(Ecef): # PYCHOK .latlonBase
1283 '''(INTERNAL) Validate B{C{Ecef}} I{class}.
1284 '''
1285 if issubclassof(Ecef, _EcefBase):
1286 return Ecef
1287 raise _TypesError(_Ecef_, Ecef, *_Ecefs)
1290# kwd lon00 unused but will throw a TypeError if misspelled, etc.
1291def _xyzn4(xyz, y, z, Types, Error=EcefError, lon00=0, # PYCHOK unused
1292 _xyz_y_z_names=_xyz_y_z, **name): # in .ltp
1293 '''(INTERNAL) Get an C{(x, y, z, name)} 4-tuple.
1294 '''
1295 try:
1296 n = _name__(name, _or_nameof=xyz) # == xyz._name__(name)
1297 try:
1298 t = xyz.x, xyz.y, xyz.z, n
1299 if not isinstance(xyz, Types):
1300 raise _TypesError(_xyz_y_z_names[0], xyz, *Types)
1301 except AttributeError:
1302 t = map1(float, xyz, y, z) + (n,)
1303 except (TypeError, ValueError) as x:
1304 d = dict(zip(_xyz_y_z_names, (xyz, y, z)))
1305 raise Error(cause=x, **d)
1306 return t
1307# assert _xyz_y_z == _args_kwds_names(_xyzn4)[:3]
1310_Ecefs = (EcefKarney, EcefSudano, EcefVeness, EcefYou,
1311 EcefFarrell21, EcefFarrell22)
1312__all__ += _ALL_DOCS(_EcefBase)
1314# **) MIT License
1315#
1316# Copyright (C) 2016-2025 -- mrJean1 at Gmail -- All Rights Reserved.
1317#
1318# Permission is hereby granted, free of charge, to any person obtaining a
1319# copy of this software and associated documentation files (the "Software"),
1320# to deal in the Software without restriction, including without limitation
1321# the rights to use, copy, modify, merge, publish, distribute, sublicense,
1322# and/or sell copies of the Software, and to permit persons to whom the
1323# Software is furnished to do so, subject to the following conditions:
1324#
1325# The above copyright notice and this permission notice shall be included
1326# in all copies or substantial portions of the Software.
1327#
1328# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
1329# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
1330# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
1331# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
1332# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
1333# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
1334# OTHER DEALINGS IN THE SOFTWARE.