pygeodesy 25.7.25__py2.py3-none-any.whl → 25.8.25__py2.py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
pygeodesy/ecef.py CHANGED
@@ -8,10 +8,12 @@ Geocentric conversions transcoded from I{Charles Karney}'s C++ class U{Geocentri
8
8
  into pure Python class L{EcefKarney}, class L{EcefSudano} based on I{John Sudano}'s
9
9
  U{paper<https://www.ResearchGate.net/publication/
10
10
  3709199_An_exact_conversion_from_an_Earth-centered_coordinate_system_to_latitude_longitude_and_altitude>},
11
- class L{EcefVeness} transcoded from I{Chris Veness}' JavaScript classes U{LatLonEllipsoidal,
12
- Cartesian<https://www.Movable-Type.co.UK/scripts/geodesy/docs/latlon-ellipsoidal.js.html>}, class L{EcefYou}
13
- implementing I{Rey-Jer You}'s U{transformations<https://www.ResearchGate.net/publication/240359424>} and
14
- classes L{EcefFarrell22} and L{EcefFarrell22} from I{Jay A. Farrell}'s U{Table 2.1 and 2.2
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
15
17
  <https://Books.Google.com/books?id=fW4foWASY6wC>}, page 29-30.
16
18
 
17
19
  Following is a copy of I{Karney}'s U{Detailed Description
@@ -59,9 +61,9 @@ plane} as opposed to I{geocentric} (ECEF) ones.
59
61
 
60
62
  from pygeodesy.basics import copysign0, _isin, isscalar, issubclassof, neg, map1, \
61
63
  _xinstanceof, _xsubclassof, typename # _args_kwds_names
62
- from pygeodesy.constants import EPS, EPS0, EPS02, EPS1, EPS2, EPS_2, INT0, PI, PI_2, \
63
- _0_0, _0_5, _1_0, _1_0_1T, _2_0, _N_2_0, _3_0, _4_0, \
64
- _6_0, _90_0, _N_90_0, _copysign_1_0, isnon0 # PYCHOK used!
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, \
66
+ _90_0, _copysign_1_0, isnon0 # PYCHOK used!
65
67
  from pygeodesy.datums import _ellipsoidal_datum, _WGS84, a_f2Tuple, _EWGS84
66
68
  from pygeodesy.ecefLocals import _EcefLocal
67
69
  # from pygeodesy.ellipsoids import a_f2Tuple, _EWGS84 # from .datums
@@ -77,23 +79,23 @@ from pygeodesy.lazily import _ALL_DOCS, _ALL_LAZY, _ALL_MODS as _MODS
77
79
  from pygeodesy.named import _name__, _name1__, _NamedBase, _NamedTuple, _Pass, _xnamed
78
80
  from pygeodesy.namedTuples import LatLon2Tuple, LatLon3Tuple, \
79
81
  PhiLam2Tuple, Vector3Tuple, Vector4Tuple
80
- from pygeodesy.props import deprecated_method, Property_RO, property_RO, \
81
- property_ROver, property_doc_
82
+ from pygeodesy.props import deprecated_method, deprecated_property, Property_RO, \
83
+ property_RO, property_ROver
82
84
  # from pygeodesy.streprs import Fmt, unstr # from .fsums
83
85
  from pygeodesy.units import _isRadius, Degrees, Height, Int, Lam, Lat, Lon, Meter, \
84
- Phi, Scalar, Scalar_
86
+ Phi, Radians_, Scalar, Scalar_
85
87
  from pygeodesy.utily import atan1, atan1d, atan2, atan2d, degrees90, degrees180, \
86
- sincos2, sincos2_, sincos2d, sincos2d_
88
+ sincos2, sincos2_, sincos2d_
87
89
  # from pygeodesy.vector3d import Vector3d # _MODS
88
90
 
89
91
  from math import cos, degrees, fabs, radians, sqrt
90
92
 
91
93
  __all__ = _ALL_LAZY.ecef
92
- __version__ = '25.05.07'
94
+ __version__ = '25.08.25'
93
95
 
94
96
  _Ecef_ = 'Ecef'
95
97
  _prolate_ = 'prolate'
96
- _TRIPS = 33 # 8..9 sufficient, EcefSudano.reverse
98
+ _TRIPS = 33 # 8..9 sufficient, EcefEDA.-/EcefSudano.reverse
97
99
  _xyz_y_z = _xyz_, _y_, _z_ # _args_kwds_names(_xyzn4)[:3]
98
100
 
99
101
 
@@ -104,8 +106,7 @@ class EcefError(_ValueError):
104
106
 
105
107
 
106
108
  class _EcefBase(_NamedBase):
107
- '''(INTERNAL) Base class for L{EcefFarrell21}, L{EcefFarrell22}, L{EcefKarney},
108
- L{EcefSudano}, L{EcefVeness} and L{EcefYou}.
109
+ '''(INTERNAL) Base class for C{Ecef*} conversion classes.
109
110
  '''
110
111
  _datum = _WGS84
111
112
  _E = _EWGS84
@@ -163,8 +164,8 @@ class _EcefBase(_NamedBase):
163
164
 
164
165
  @return: C{True} if equal, C{False} otherwise.
165
166
  '''
166
- return other is self or (isinstance(other, self.__class__) and
167
- other.ellipsoid == self.ellipsoid)
167
+ return other is self or (isinstance(other, type(self)) and
168
+ other.ellipsoid == self.ellipsoid)
168
169
 
169
170
  @Property_RO
170
171
  def datum(self):
@@ -207,13 +208,15 @@ class _EcefBase(_NamedBase):
207
208
 
208
209
  E = self.ellipsoid
209
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
210
214
  z = (h + n * E.e21) * sa
211
- x = (h + n) * ca
212
215
 
213
216
  m = self._Matrix(sa, ca, sb, cb) if M else None
214
- return Ecef9Tuple(x * cb, x * sb, z, lat, lon, h,
215
- 0, m, self.datum,
216
- name=self._name__(name))
217
+ return Ecef9Tuple(x, y, z, lat, lon, h,
218
+ 0, m, self.datum, # C=0, always
219
+ name=self._name__(name))
217
220
 
218
221
  def forward(self, latlonh, lon=None, height=0, M=False, **name):
219
222
  '''Convert from geodetic C{(lat, lon, height)} to geocentric C{(x, y, z)}.
@@ -296,14 +299,14 @@ class _EcefBase(_NamedBase):
296
299
  '''
297
300
  return self._xnamed(EcefMatrix(sa, ca, sb, cb))
298
301
 
299
- def _polon(self, y, x, R, **lon00_name):
302
+ def _polon(self, y, x, p, **lon00_name):
300
303
  '''(INTERNAL) Handle I{"polar"} longitude.
301
304
  '''
302
- return atan2d(y, x) if R else _xkwds_get(lon00_name, lon00=self.lon00)
305
+ return atan2d(y, x) if p else _xkwds_get(lon00_name, lon00=self.lon00)
303
306
 
304
- def reverse(self, xyz, y=None, z=None, M=False, **lon00_name): # PYCHOK no cover
307
+ def reverse(self, xyz, y=None, z=None, M=False, **tol_lon00_name): # PYCHOK no cover
305
308
  '''I{Must be overloaded}.'''
306
- self._notOverloaded(xyz, y=y, z=z, M=M, **lon00_name)
309
+ self._notOverloaded(xyz, y=y, z=z, M=M, **tol_lon00_name)
307
310
 
308
311
  def toStr(self, prec=9, **unused): # PYCHOK signature
309
312
  '''Return this C{Ecef*} as a string.
@@ -314,6 +317,33 @@ class _EcefBase(_NamedBase):
314
317
  '''
315
318
  return self.attrs(_a_, _f_, _datum_, _name_, prec=prec) # _ellipsoid_
316
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
+
317
347
 
318
348
  class EcefFarrell21(_EcefBase):
319
349
  '''Conversion between geodetic and geocentric, I{Earth-Centered, Earth-Fixed} (ECEF)
@@ -333,13 +363,12 @@ class EcefFarrell21(_EcefBase):
333
363
  @kwarg z: ECEF C{z} coordinate for C{scalar} B{C{xyz}} and B{C{y}} (C{meter}).
334
364
  @kwarg M: I{Ignored}, rotation matrix C{M} not available.
335
365
  @kwarg lon00_name: Optional C{B{name}=NN} (C{str}) and optional keyword argument
336
- C{B{lon00}=INT0} (C{degrees}), an arbitrary I{"polar"} longitude
337
- returned if C{B{x}=0} and C{B{y}=0}, see property C{lon00}.
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}.
338
368
 
339
369
  @return: An L{Ecef9Tuple}C{(x, y, z, lat, lon, height, C, M, datum)} with
340
370
  geodetic coordinates C{(lat, lon, height)} for the given geocentric
341
- ones C{(x, y, z)}, case C{C=1}, C{M=None} always and C{datum}
342
- if available.
371
+ ones C{(x, y, z)}, case C{C}, C{M=None} always and C{datum}.
343
372
 
344
373
  @raise EcefError: Invalid B{C{xyz}} or C{scalar} C{x} or B{C{y}} and/or B{C{z}}
345
374
  not C{scalar} for C{scalar} B{C{xyz}} or C{sqrt} domain or
@@ -347,47 +376,46 @@ class EcefFarrell21(_EcefBase):
347
376
 
348
377
  @see: L{EcefFarrell22} and L{EcefVeness}.
349
378
  '''
350
- x, y, z, name = _xyzn4(xyz, y, z, self._Geocentrics, **lon00_name)
351
-
352
- E = self.ellipsoid
353
- a = E.a
354
- a2 = E.a2
355
- b2 = E.b2
356
- e2 = E.e2
357
- e2_ = E.e2abs * E.a2_b2 # (E.e * E.a_b)**2 = 0.0820944... WGS84
358
- e4 = E.e4
359
-
360
- try: # names as page 29
361
- z2 = z**2
362
- ez = z2 * (_1_0 - e2) # E.e2s2(z)
363
-
364
- p = hypot(x, y)
365
- p2 = p**2
366
- G = p2 + ez - e2 * (a2 - b2) # p2 + ez - e4 * a2
367
- F = b2 * z2 * 54
368
- c = e4 * p2 * F / G**3
369
- s = cbrt(sqrt(c * (c + _2_0)) + c + _1_0)
370
- G *= fsumf_(s , _1_0, _1_0 / s) # k
371
- P = F / (G**2 * _3_0)
372
- Q = sqrt(_2_0 * e4 * P + _1_0)
373
- Q1 = Q + _1_0
374
- r0 = P * p * e2 / Q1 - sqrt(fsumf_(a2 * (Q1 / Q) * _0_5,
375
- -P * ez / (Q * Q1),
376
- -P * p2 * _0_5))
377
- r = p + e2 * r0
378
- v = b2 / (sqrt(r**2 + ez) * a) # z0 / z
379
-
380
- h = hypot(r, z) * (_1_0 - v)
381
- lat = atan1d((e2_ * v + _1_0) * z, p)
382
- lon = self._polon(y, x, p, **lon00_name)
383
- # note, phi and lam are swapped on page 29
384
-
385
- except (ValueError, ZeroDivisionError) as X:
386
- raise EcefError(x=x, y=y, z=z, cause=X)
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)
387
416
 
388
417
  return Ecef9Tuple(x, y, z, lat, lon, h,
389
- 1, None, self.datum,
390
- name=self._name__(name))
418
+ C, None, self.datum, name=name)
391
419
 
392
420
 
393
421
  class EcefFarrell22(_EcefBase):
@@ -407,13 +435,12 @@ class EcefFarrell22(_EcefBase):
407
435
  @kwarg z: ECEF C{z} coordinate for C{scalar} B{C{xyz}} and B{C{y}} (C{meter}).
408
436
  @kwarg M: I{Ignored}, rotation matrix C{M} not available.
409
437
  @kwarg lon00_name: Optional C{B{name}=NN} (C{str}) and optional keyword argument
410
- C{B{lon00}=INT0} (C{degrees}), an arbitrary I{"polar"} longitude
411
- returned if C{B{x}=0} and C{B{y}=0}, see property C{lon00}.
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}.
412
440
 
413
441
  @return: An L{Ecef9Tuple}C{(x, y, z, lat, lon, height, C, M, datum)} with
414
442
  geodetic coordinates C{(lat, lon, height)} for the given geocentric
415
- ones C{(x, y, z)}, case C{C=1}, C{M=None} always and C{datum}
416
- if available.
443
+ ones C{(x, y, z)}, case C{C}, C{M=None} always and C{datum}.
417
444
 
418
445
  @raise EcefError: Invalid B{C{xyz}} or C{scalar} C{x} or B{C{y}} and/or B{C{z}}
419
446
  not C{scalar} for C{scalar} B{C{xyz}} or C{sqrt} domain or
@@ -421,33 +448,19 @@ class EcefFarrell22(_EcefBase):
421
448
 
422
449
  @see: L{EcefFarrell21} and L{EcefVeness}.
423
450
  '''
424
- x, y, z, name = _xyzn4(xyz, y, z, self._Geocentrics, **lon00_name)
425
-
426
- E = self.ellipsoid
427
- a = E.a
428
- b = E.b
429
-
430
- try: # see EcefVeness.reverse
431
- p = hypot(x, y)
432
- lon = self._polon(y, x, p, **lon00_name)
433
-
434
- s, c = sincos2(atan2(z * a, p * b)) # == _norm3
435
- lat = atan1d(z + s**3 * b * E.e22,
436
- p - c**3 * a * E.e2)
437
-
438
- s, c = sincos2d(lat)
439
- if c: # E.roc1_(s) = E.a / sqrt(1 - E.e2 * s**2)
440
- h = p / c - (E.roc1_(s) if s else a)
441
- else: # polar
442
- h = fabs(z) - b
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)
443
460
  # note, phi and lam are swapped on page 30
444
461
 
445
- except (ValueError, ZeroDivisionError) as e:
446
- raise EcefError(x=x, y=y, z=z, cause=e)
447
-
448
462
  return Ecef9Tuple(x, y, z, lat, lon, h,
449
- 1, None, self.datum,
450
- name=self._name__(name))
463
+ C, None, self.datum, name=name)
451
464
 
452
465
 
453
466
  class EcefKarney(_EcefBase):
@@ -463,12 +476,6 @@ class EcefKarney(_EcefBase):
463
476
  the rotation matrix.
464
477
  '''
465
478
 
466
- @Property_RO
467
- def hmax(self):
468
- '''Get the distance or height limit (C{meter}, conventionally).
469
- '''
470
- return self.equatoradius / EPS_2 # self.equatoradius * _2_EPS, 12M lighyears
471
-
472
479
  def reverse(self, xyz, y=None, z=None, M=False, **lon00_name):
473
480
  '''Convert from geocentric C{(x, y, z)} to geodetic C{(lat, lon, height)}.
474
481
 
@@ -478,8 +485,8 @@ class EcefKarney(_EcefBase):
478
485
  @kwarg z: ECEF C{z} coordinate for C{scalar} B{C{xyz}} and B{C{y}} (C{meter}).
479
486
  @kwarg M: Optionally, return the rotation L{EcefMatrix} (C{bool}).
480
487
  @kwarg lon00_name: Optional C{B{name}=NN} (C{str}) and optional keyword argument
481
- C{B{lon00}=INT0} (C{degrees}), an arbitrary I{"polar"} longitude
482
- returned if C{B{x}=0} and C{B{y}=0}, see property C{lon00}.
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}.
483
490
 
484
491
  @return: An L{Ecef9Tuple}C{(x, y, z, lat, lon, height, C, M, datum)} with
485
492
  geodetic coordinates C{(lat, lon, height)} for the given geocentric
@@ -499,27 +506,14 @@ class EcefKarney(_EcefBase):
499
506
  is not below M{−E.a * (1 − E.e2) / sqrt(1 − E.e2 * sin(lat)**2)}. Like
500
507
  C{forward} above, M{v1 = Transpose(M) ⋅ v0}.
501
508
  '''
502
- def _norm3(y, x):
503
- h = hypot(y, x) # EPS0, EPS_2
504
- return (y / h, x / h, h) if h > 0 else (_0_0, _1_0, h)
505
-
506
509
  x, y, z, name = _xyzn4(xyz, y, z, self._Geocentrics, **lon00_name)
507
510
 
508
511
  E = self.ellipsoid
509
512
  f = E.f
510
513
 
511
- sb, cb, R = _norm3(y, x)
512
- h = hypot(R, z) # distance to earth center
513
- if h > self.hmax: # PYCHOK no cover
514
- # We are really far away (> 12M light years). Treat the earth
515
- # as a point and h above as an acceptable approximation to the
516
- # height. This avoids overflow, e.g., in the computation of d
517
- # below. It's possible that h has overflowed to INF, that's OK.
518
- # Treat finite x, y, but R overflows to +INF by scaling by 2.
519
- sb, cb, R = _norm3(y * _0_5, x * _0_5)
520
- sa, ca, _ = _norm3(z * _0_5, R)
521
- C = 1
522
-
514
+ sa, ca, sb, cb, R, h, C = _norm7(y, x, z, E)
515
+ if C: # PYCHOK no cover
516
+ pass # too high, far
523
517
  elif E.e4: # E.isEllipsoidal
524
518
  # Treat prolate spheroids by swapping R and Z here and by
525
519
  # switching the arguments to phi = atan2(...) at the end.
@@ -569,7 +563,7 @@ class EcefKarney(_EcefBase):
569
563
  k2 += E.e2
570
564
  sa, ca, h = _norm3(z / k1, R / k2)
571
565
  h *= k1 - E.e21
572
- C = 2
566
+ C = 1
573
567
 
574
568
  else: # e = E.e4 * q == 0 and r <= 0
575
569
  # This leads to k = 0 (oblate, equatorial plane) and k + E.e^2 = 0
@@ -595,7 +589,7 @@ class EcefKarney(_EcefBase):
595
589
  # difficult. Origin maps to North pole, same as with ellipsoid.
596
590
  sa, ca, _ = _norm3((z if h else _1_0), R)
597
591
  h -= E.a
598
- C = 4
592
+ C = 2
599
593
 
600
594
  # lon00 <https://GitHub.com/mrJean1/PyGeodesy/issues/77>
601
595
  lon = self._polon(sb, cb, R, **lon00_name)
@@ -608,9 +602,9 @@ class EcefSudano(_EcefBase):
608
602
  '''Conversion between geodetic and geocentric, I{Earth-Centered, Earth-Fixed} (ECEF) coordinates
609
603
  based on I{John J. Sudano}'s U{paper<https://www.ResearchGate.net/publication/3709199>}.
610
604
  '''
611
- _tol = EPS2
605
+ _tol = EPS0 # DEPRECATED
612
606
 
613
- def reverse(self, xyz, y=None, z=None, M=None, **lon00_name): # PYCHOK unused M
607
+ def reverse(self, xyz, y=None, z=None, M=None, tol=EPS, **lon00_name): # PYCHOK unused M
614
608
  '''Convert from geocentric C{(x, y, z)} to geodetic C{(lat, lon, height)} using
615
609
  I{Sudano}'s U{iterative method<https://www.ResearchGate.net/publication/3709199>}.
616
610
 
@@ -619,78 +613,129 @@ class EcefSudano(_EcefBase):
619
613
  @kwarg y: ECEF C{y} coordinate for C{scalar} B{C{xyz}} and B{C{z}} (C{meter}).
620
614
  @kwarg z: ECEF C{z} coordinate for C{scalar} B{C{xyz}} and B{C{y}} (C{meter}).
621
615
  @kwarg M: I{Ignored}, rotation matrix C{M} not available.
616
+ @kwarg tol: Convergence tolerance for C{sin}(latitude) (C{scalar}).
622
617
  @kwarg lon00_name: Optional C{B{name}=NN} (C{str}) and optional keyword argument
623
- C{B{lon00}=INT0} (C{degrees}), an arbitrary I{"polar"} longitude
624
- returned if C{B{x}=0} and C{B{y}=0}, see property C{lon00}.
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}.
625
620
 
626
621
  @return: An L{Ecef9Tuple}C{(x, y, z, lat, lon, height, C, M, datum)} with geodetic
627
622
  coordinates C{(lat, lon, height)} for the given geocentric ones C{(x, y, z)},
628
- iteration C{C}, C{M=None} always and C{datum} if available.
623
+ case C{C}, C{M=None} always and C{datum} if available.
629
624
 
630
625
  @raise EcefError: Invalid B{C{xyz}} or C{scalar} C{x} or B{C{y}} and/or B{C{z}}
631
626
  not C{scalar} for C{scalar} B{C{xyz}} or no convergence.
632
- '''
633
- x, y, z, name = _xyzn4(xyz, y, z, self._Geocentrics, **lon00_name)
634
-
635
- E = self.ellipsoid
636
- e = E.e2 * E.a
637
- R = hypot(x, y) # Rh
638
- d = e - R
639
-
640
- lat = atan1d(z, R * E.e21)
641
- sa, ca = sincos2d(fabs(lat))
642
- # Sudano's Eq (A-6) and (A-7) refactored/reduced,
643
- # replacing Rn from Eq (A-4) with n = E.a / ca:
644
- # N = ca**2 * ((z + E.e2 * n * sa) * ca - R * sa)
645
- # = ca**2 * (z * ca + E.e2 * E.a * sa - R * sa)
646
- # = ca**2 * (z * ca + (E.e2 * E.a - R) * sa)
647
- # D = ca**3 * (E.e2 * n / E.e2s2(sa)) - R
648
- # = ca**2 * (E.e2 * E.a / E.e2s2(sa) - R / ca**2)
649
- # N / D = (z * ca + (E.e2 * E.a - R) * sa) /
650
- # (E.e2 * E.a / E.e2s2(sa) - R / ca**2)
651
- tol = self.tolerance
652
- _S2 = Fsum(sa).fsum2f_
653
- for i in range(1, _TRIPS):
654
- ca2 = _1_0 - sa**2
655
- if ca2 < EPS_2: # PYCHOK no cover
656
- ca = _0_0
657
- break
658
- ca = sqrt(ca2)
659
- r = e / E.e2s2(sa) - R / ca2
660
- if fabs(r) < EPS_2:
661
- break
662
- lat = None
663
- sa, t = _S2(-z * ca / r, -d * sa / r)
664
- if fabs(t) < tol:
665
- break
666
- else:
667
- t = unstr(self.reverse, x=x, y=y, z=z)
668
- raise EcefError(t, txt=Fmt.no_convergence(r, tol))
669
627
 
628
+ @see: Class L{EcefUPC}.
629
+ '''
630
+ x, y, z, lat, lon, h, C, p, name = self._xyzllhCpn9(xyz, y, z, **lon00_name)
670
631
  if lat is None:
671
- lat = copysign0(atan1d(fabs(sa), ca), z)
672
- lon = self._polon(y, x, R, **lon00_name)
673
-
674
- h = fsumf_(R * ca, fabs(z * sa), -E.a * E.e2s(sa)) # use Veness'
675
- # because Sudano's Eq (7) doesn't produce the correct height
676
- # h = (fabs(z) + R - E.a * cos(a + E.e21) * sa / ca) / (ca + sa)
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
677
670
  return Ecef9Tuple(x, y, z, lat, lon, h,
678
- i, None, self.datum, # C=i, M=None
679
- iteration=i, name=self._name__(name))
671
+ C, None, self.datum, # M=None
672
+ iteration=i, name=name)
680
673
 
681
- @property_doc_(''' the convergence tolerance (C{float}).''')
674
+ @deprecated_property
682
675
  def tolerance(self):
683
- '''Get the convergence tolerance (C{scalar}).
684
- '''
676
+ '''DEPRECATED on 2025.08.22, use keyword argument C{tol}.'''
685
677
  return self._tol
686
678
 
687
679
  @tolerance.setter # PYCHOK setter!
688
680
  def tolerance(self, tol):
689
- '''Set the convergence tolerance (C{scalar}).
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
+ '''
690
688
 
691
- @raise EcefError: Non-scalar or invalid B{C{tol}}.
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}.
692
711
  '''
693
- self._tol = Scalar_(tolerance=tol, low=EPS, Error=EcefError)
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)
694
739
 
695
740
 
696
741
  class EcefVeness(_EcefBase):
@@ -718,8 +763,8 @@ class EcefVeness(_EcefBase):
718
763
  @kwarg z: ECEF C{z} coordinate for C{scalar} B{C{xyz}} and B{C{y}} (C{meter}).
719
764
  @kwarg M: I{Ignored}, rotation matrix C{M} not available.
720
765
  @kwarg lon00_name: Optional C{B{name}=NN} (C{str}) and optional keyword argument
721
- C{B{lon00}=INT0} (C{degrees}), an arbitrary I{"polar"} longitude
722
- returned if C{B{x}=0} and C{B{y}=0}, see property C{lon00}.
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}.
723
768
 
724
769
  @return: An L{Ecef9Tuple}C{(x, y, z, lat, lon, height, C, M, datum)} with
725
770
  geodetic coordinates C{(lat, lon, height)} for the given geocentric
@@ -737,41 +782,23 @@ class EcefVeness(_EcefBase):
737
782
  system to latitude longitude and altitude}<https://www.ResearchGate.net/
738
783
  publication/3709199>}.
739
784
  '''
740
- x, y, z, name = _xyzn4(xyz, y, z, self._Geocentrics, **lon00_name)
741
-
742
- E = self.ellipsoid
743
- a = E.a
744
-
745
- p = hypot(x, y) # distance from minor axis
746
- r = hypot(p, z) # polar radius
747
- if min(p, r) > EPS0:
748
- b = E.b * E.e22
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
749
790
  # parametric latitude (Bowring eqn 17, replaced)
750
- t = (E.b * z) / (a * p) * (_1_0 + b / r)
791
+ t = (E.b * z) / (a * p) * (_1_0 + B / h) # h = hypot(z, p)
751
792
  c = _1_0 / hypot1(t)
752
793
  s = c * t
753
794
  # geodetic latitude (Bowring eqn 18)
754
- lat = atan1d(z + s**3 * b,
755
- p - c**3 * a * E.e2)
756
-
757
- # height above ellipsoid (Bowring eqn 7)
758
- sa, ca = sincos2d(lat)
759
- # r = a / E.e2s(sa) # length of normal terminated by minor axis
760
- # h = p * ca + z * sa - (a * a / r)
761
- h = fsumf_(p * ca, z * sa, -a * E.e2s(sa))
762
- C = 1
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)
763
799
 
764
- # see <https://GIS.StackExchange.com/questions/28446>
765
- elif p > EPS: # lat arbitrarily zero, equatorial lon
766
- C, lat, h = 2, _0_0, (p - a)
767
-
768
- else: # polar lat, lon arbitrarily lon00
769
- C, lat, h = 3, (_N_90_0 if z < 0 else _90_0), (fabs(z) - E.b)
770
-
771
- lon = self._polon(y, x, p, **lon00_name)
772
800
  return Ecef9Tuple(x, y, z, lat, lon, h,
773
- C, None, self.datum, # M=None
774
- name=self._name__(name))
801
+ C, None, self.datum, name=name)
775
802
 
776
803
 
777
804
  class EcefYou(_EcefBase):
@@ -788,14 +815,12 @@ class EcefYou(_EcefBase):
788
815
 
789
816
  def __init__(self, a_ellipsoid=_EWGS84, f=None, **lon00_name): # PYCHOK signature
790
817
  _EcefBase.__init__(self, a_ellipsoid, f=f, **lon00_name) # inherited documentation
791
- self._ee2 = EcefYou._ee2(self.ellipsoid)
792
818
 
793
- @staticmethod
794
- def _ee2(E):
819
+ E = self.ellipsoid
795
820
  e2 = E.a2 - E.b2
796
821
  if e2 < 0 or E.f < 0:
797
822
  raise EcefError(ellipsoid=E, txt=_prolate_)
798
- return sqrt0(e2), e2
823
+ self._ee2 = sqrt0(e2), e2
799
824
 
800
825
  def reverse(self, xyz, y=None, z=None, M=None, **lon00_name): # PYCHOK unused M
801
826
  '''Convert geocentric C{(x, y, z)} to geodetic C{(lat, lon, height)}
@@ -812,47 +837,44 @@ class EcefYou(_EcefBase):
812
837
 
813
838
  @return: An L{Ecef9Tuple}C{(x, y, z, lat, lon, height, C, M, datum)} with
814
839
  geodetic coordinates C{(lat, lon, height)} for the given geocentric
815
- ones C{(x, y, z)}, case C{C=1}, C{M=None} always and C{datum} if
816
- available.
840
+ ones C{(x, y, z)}, case C{C}, C{M=None} always and C{datum} if available.
817
841
 
818
842
  @raise EcefError: Invalid B{C{xyz}} or C{scalar} C{x} or B{C{y}} and/or
819
843
  B{C{z}} not C{scalar} for C{scalar} B{C{xyz}} or the
820
844
  ellipsoid is I{prolate}.
821
845
  '''
822
- x, y, z, name = _xyzn4(xyz, y, z, self._Geocentrics, **lon00_name)
823
- q = hypot(x, y) # R
824
-
825
- E = self.ellipsoid
826
- e, e2 = self._ee2
827
-
828
- u = hypot2_(x, y, z) - e2
829
- u += hypot(u, e * z * _2_0)
830
- u *= _0_5
831
- if u > EPS02:
832
- u = sqrt(u)
833
- p = hypot(u, e)
834
- B = atan1(p * z, u * q) # beta0 = atan(p / u * z / q)
835
- sB, cB = sincos2(B)
836
- if cB and sB:
837
- p *= E.a
838
- d = (p / cB - e2 * cB) / sB
839
- if isnon0(d):
840
- B += fsumf_(u * E.b, -p, e2) / d
841
- sB, cB = sincos2(B)
842
- elif u < (-EPS2):
843
- raise EcefError(x=x, y=y, z=z, u=u, txt=_singular_)
844
- else:
845
- sB, cB = _copysign_1_0(z), _0_0
846
-
847
- lat = atan1d(E.a * sB, E.b * cB) # atan(E.a_b * tan(B))
848
- lon = self._polon(y, x, q, **lon00_name)
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
849
875
 
850
- h = hypot(z - E.b * sB, q - E.a * cB)
851
- if hypot2_(x, y, z * E.a_b) < E.a2:
852
- h = neg(h) # inside ellipsoid
853
876
  return Ecef9Tuple(x, y, z, lat, lon, h,
854
- 1, None, self.datum, # C=1, M=None
855
- name=self._name__(name))
877
+ C, None, self.datum, name=name)
856
878
 
857
879
 
858
880
  class EcefMatrix(_NamedTuple):
@@ -872,7 +894,7 @@ class EcefMatrix(_NamedTuple):
872
894
  '''
873
895
  _NamedTuple._validate(self, underOK=True)
874
896
 
875
- def __new__(cls, sa, ca, sb, cb, *_more):
897
+ def __new__(cls, sa, ca, sb, cb, *_more, **name):
876
898
  '''New L{EcefMatrix} matrix.
877
899
 
878
900
  @arg sa: C{sin(phi)} (C{float}).
@@ -908,7 +930,7 @@ class EcefMatrix(_NamedTuple):
908
930
  cb, -sb * sa, sb * ca,
909
931
  _0_0, ca, sa)
910
932
 
911
- return _NamedTuple.__new__(cls, *t)
933
+ return _NamedTuple.__new__(cls, *t, **name)
912
934
 
913
935
  def column(self, column):
914
936
  '''Get this matrix' B{C{column}} 0, 1 or 2 as C{3-tuple}.
@@ -1017,10 +1039,11 @@ class EcefMatrix(_NamedTuple):
1017
1039
 
1018
1040
  class Ecef9Tuple(_NamedTuple, _EcefLocal):
1019
1041
  '''9-Tuple C{(x, y, z, lat, lon, height, C, M, datum)} with I{geocentric} C{x},
1020
- C{y} and C{z} plus I{geodetic} C{lat}, C{lon} and C{height}, case C{C} (see
1021
- the C{Ecef*.reverse} methods) and optionally, rotation matrix C{M} (L{EcefMatrix})
1022
- and C{datum}, with C{lat} and C{lon} in C{degrees} and C{x}, C{y}, C{z} and
1023
- C{height} in C{meter}, conventionally.
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}.
1024
1047
  '''
1025
1048
  _Names_ = (_x_, _y_, _z_, _lat_, _lon_, _height_, _C_, _M_, _datum_)
1026
1049
  _Units_ = ( Meter, Meter, Meter, Lat, Lon, Height, Int, _Pass, _Pass)
@@ -1040,6 +1063,12 @@ class Ecef9Tuple(_NamedTuple, _EcefLocal):
1040
1063
  def _ecef9(self): # in ._EcefLocal._Ltp_ecef2local
1041
1064
  return self
1042
1065
 
1066
+ @property_RO
1067
+ def ellipsoid(self):
1068
+ '''Get the ellipsoid (L{Ellipsoid}).
1069
+ '''
1070
+ return (self.datum or _WGS84).ellipsoid
1071
+
1043
1072
  @Property_RO
1044
1073
  def lam(self):
1045
1074
  '''Get the longitude in C{radians} (C{float}).
@@ -1056,10 +1085,11 @@ class Ecef9Tuple(_NamedTuple, _EcefLocal):
1056
1085
  and U{Featherstone, et.al.<https://Search.ProQuest.com/docview/872827242>}, page 7.
1057
1086
  '''
1058
1087
  x, y = self.x, self.y
1059
- if y > EPS0:
1060
- r = atan2(x, hypot(y, x) + y) * _N_2_0 + PI_2
1061
- elif y < -EPS0:
1062
- r = atan2(x, hypot(y, x) - y) * _2_0 - PI_2
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
1063
1093
  else: # y == 0
1064
1094
  r = PI if x < 0 else _0_0
1065
1095
  return Lam(Vermeille=r)
@@ -1093,12 +1123,19 @@ class Ecef9Tuple(_NamedTuple, _EcefLocal):
1093
1123
  @Property_RO
1094
1124
  def lonVermeille(self):
1095
1125
  '''Get the longitude in C{degrees [-225..+225]} after U{Vermeille
1096
- <https://Search.ProQuest.com/docview/639493848>} 2004, p 95.
1126
+ <https://Search.ProQuest.com/docview/639493848>} 2004, page 95.
1097
1127
 
1098
1128
  @see: Property C{lamVermeille}.
1099
1129
  '''
1100
1130
  return Lon(Vermeille=degrees(self.lamVermeille))
1101
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
+
1102
1139
  @Property_RO
1103
1140
  def phi(self):
1104
1141
  '''Get the latitude in C{radians} (C{float}).
@@ -1271,6 +1308,32 @@ def _llhn4(latlonh, lon, height, suffix=NN, Error=EcefError, **name): # in .ltp
1271
1308
  raise Error(cause=x, **d)
1272
1309
 
1273
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
+
1274
1337
  def _xEcef(Ecef): # PYCHOK .latlonBase
1275
1338
  '''(INTERNAL) Validate B{C{Ecef}} I{class}.
1276
1339
  '''
@@ -1299,8 +1362,9 @@ def _xyzn4(xyz, y, z, Types, Error=EcefError, lon00=0, # PYCHOK unused
1299
1362
  # assert _xyz_y_z == _args_kwds_names(_xyzn4)[:3]
1300
1363
 
1301
1364
 
1302
- _Ecefs = (EcefKarney, EcefSudano, EcefVeness, EcefYou,
1303
- EcefFarrell21, EcefFarrell22)
1365
+ _Ecefs = tuple(_ for _ in locals().values()
1366
+ if issubclassof(_, _EcefBase) and
1367
+ _ is not _EcefBase)
1304
1368
  __all__ += _ALL_DOCS(_EcefBase)
1305
1369
 
1306
1370
  # **) MIT License