pygeodesy 24.3.24__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.
Files changed (115) hide show
  1. PyGeodesy-24.3.24.dist-info/METADATA +272 -0
  2. PyGeodesy-24.3.24.dist-info/RECORD +115 -0
  3. PyGeodesy-24.3.24.dist-info/WHEEL +6 -0
  4. PyGeodesy-24.3.24.dist-info/top_level.txt +1 -0
  5. pygeodesy/LICENSE +21 -0
  6. pygeodesy/__init__.py +615 -0
  7. pygeodesy/__main__.py +103 -0
  8. pygeodesy/albers.py +867 -0
  9. pygeodesy/auxilats/_CX_4.py +218 -0
  10. pygeodesy/auxilats/_CX_6.py +314 -0
  11. pygeodesy/auxilats/_CX_8.py +475 -0
  12. pygeodesy/auxilats/__init__.py +54 -0
  13. pygeodesy/auxilats/__main__.py +86 -0
  14. pygeodesy/auxilats/auxAngle.py +548 -0
  15. pygeodesy/auxilats/auxDLat.py +302 -0
  16. pygeodesy/auxilats/auxDST.py +296 -0
  17. pygeodesy/auxilats/auxLat.py +848 -0
  18. pygeodesy/auxilats/auxily.py +272 -0
  19. pygeodesy/azimuthal.py +1150 -0
  20. pygeodesy/basics.py +892 -0
  21. pygeodesy/booleans.py +2031 -0
  22. pygeodesy/cartesianBase.py +1062 -0
  23. pygeodesy/clipy.py +704 -0
  24. pygeodesy/constants.py +516 -0
  25. pygeodesy/css.py +660 -0
  26. pygeodesy/datums.py +752 -0
  27. pygeodesy/deprecated/__init__.py +61 -0
  28. pygeodesy/deprecated/bases.py +40 -0
  29. pygeodesy/deprecated/classes.py +262 -0
  30. pygeodesy/deprecated/consterns.py +54 -0
  31. pygeodesy/deprecated/datum.py +40 -0
  32. pygeodesy/deprecated/functions.py +375 -0
  33. pygeodesy/deprecated/nvector.py +48 -0
  34. pygeodesy/deprecated/rhumbBase.py +32 -0
  35. pygeodesy/deprecated/rhumbaux.py +33 -0
  36. pygeodesy/deprecated/rhumbsolve.py +33 -0
  37. pygeodesy/deprecated/rhumbx.py +33 -0
  38. pygeodesy/dms.py +986 -0
  39. pygeodesy/ecef.py +1348 -0
  40. pygeodesy/elevations.py +279 -0
  41. pygeodesy/ellipsoidalBase.py +1224 -0
  42. pygeodesy/ellipsoidalBaseDI.py +913 -0
  43. pygeodesy/ellipsoidalExact.py +343 -0
  44. pygeodesy/ellipsoidalGeodSolve.py +343 -0
  45. pygeodesy/ellipsoidalKarney.py +403 -0
  46. pygeodesy/ellipsoidalNvector.py +685 -0
  47. pygeodesy/ellipsoidalVincenty.py +590 -0
  48. pygeodesy/ellipsoids.py +2476 -0
  49. pygeodesy/elliptic.py +1198 -0
  50. pygeodesy/epsg.py +243 -0
  51. pygeodesy/errors.py +804 -0
  52. pygeodesy/etm.py +1190 -0
  53. pygeodesy/fmath.py +1013 -0
  54. pygeodesy/formy.py +1818 -0
  55. pygeodesy/frechet.py +865 -0
  56. pygeodesy/fstats.py +760 -0
  57. pygeodesy/fsums.py +1898 -0
  58. pygeodesy/gars.py +358 -0
  59. pygeodesy/geodesicw.py +581 -0
  60. pygeodesy/geodesicx/_C4_24.py +1699 -0
  61. pygeodesy/geodesicx/_C4_27.py +2395 -0
  62. pygeodesy/geodesicx/_C4_30.py +3301 -0
  63. pygeodesy/geodesicx/__init__.py +48 -0
  64. pygeodesy/geodesicx/__main__.py +91 -0
  65. pygeodesy/geodesicx/gx.py +1382 -0
  66. pygeodesy/geodesicx/gxarea.py +535 -0
  67. pygeodesy/geodesicx/gxbases.py +154 -0
  68. pygeodesy/geodesicx/gxline.py +669 -0
  69. pygeodesy/geodsolve.py +426 -0
  70. pygeodesy/geohash.py +914 -0
  71. pygeodesy/geoids.py +1884 -0
  72. pygeodesy/hausdorff.py +892 -0
  73. pygeodesy/heights.py +1155 -0
  74. pygeodesy/interns.py +687 -0
  75. pygeodesy/iters.py +545 -0
  76. pygeodesy/karney.py +919 -0
  77. pygeodesy/ktm.py +633 -0
  78. pygeodesy/latlonBase.py +1766 -0
  79. pygeodesy/lazily.py +960 -0
  80. pygeodesy/lcc.py +684 -0
  81. pygeodesy/ltp.py +1107 -0
  82. pygeodesy/ltpTuples.py +1563 -0
  83. pygeodesy/mgrs.py +721 -0
  84. pygeodesy/named.py +1324 -0
  85. pygeodesy/namedTuples.py +683 -0
  86. pygeodesy/nvectorBase.py +695 -0
  87. pygeodesy/osgr.py +781 -0
  88. pygeodesy/points.py +1686 -0
  89. pygeodesy/props.py +628 -0
  90. pygeodesy/resections.py +1048 -0
  91. pygeodesy/rhumb/__init__.py +46 -0
  92. pygeodesy/rhumb/aux_.py +397 -0
  93. pygeodesy/rhumb/bases.py +1148 -0
  94. pygeodesy/rhumb/ekx.py +563 -0
  95. pygeodesy/rhumb/solve.py +572 -0
  96. pygeodesy/simplify.py +647 -0
  97. pygeodesy/solveBase.py +472 -0
  98. pygeodesy/sphericalBase.py +724 -0
  99. pygeodesy/sphericalNvector.py +1264 -0
  100. pygeodesy/sphericalTrigonometry.py +1447 -0
  101. pygeodesy/streprs.py +627 -0
  102. pygeodesy/trf.py +2079 -0
  103. pygeodesy/triaxials.py +1484 -0
  104. pygeodesy/units.py +969 -0
  105. pygeodesy/unitsBase.py +349 -0
  106. pygeodesy/ups.py +538 -0
  107. pygeodesy/utily.py +1231 -0
  108. pygeodesy/utm.py +762 -0
  109. pygeodesy/utmups.py +318 -0
  110. pygeodesy/utmupsBase.py +517 -0
  111. pygeodesy/vector2d.py +785 -0
  112. pygeodesy/vector3d.py +968 -0
  113. pygeodesy/vector3dBase.py +1049 -0
  114. pygeodesy/webmercator.py +383 -0
  115. pygeodesy/wgrs.py +439 -0
pygeodesy/triaxials.py ADDED
@@ -0,0 +1,1484 @@
1
+
2
+ # -*- coding: utf-8 -*-
3
+
4
+ u'''Triaxal ellipsoid classes I{ordered} L{Triaxial} and I{unordered} L{Triaxial_} and Jacobi
5
+ conformal projections L{JacobiConformal} and L{JacobiConformalSpherical}, transcoded from
6
+ I{Charles Karney}'s C++ class U{JacobiConformal<https://GeographicLib.SourceForge.io/C++/doc/
7
+ classGeographicLib_1_1JacobiConformal.html#details>} to pure Python and miscellaneous classes
8
+ L{BetaOmega2Tuple}, L{BetaOmega3Tuple}, L{Jacobi2Tuple} and L{TriaxialError}.
9
+
10
+ Copyright (C) U{Charles Karney<mailto:Karney@Alum.MIT.edu>} (2008-2023). For more information,
11
+ see the U{GeographicLib<https://GeographicLib.SourceForge.io>} documentation.
12
+
13
+ @see: U{Geodesics on a triaxial ellipsoid<https://WikiPedia.org/wiki/Geodesics_on_an_ellipsoid#
14
+ Geodesics_on_a_triaxial_ellipsoid>} and U{Triaxial coordinate systems and their geometrical
15
+ interpretation<https://www.Topo.Auth.GR/wp-content/uploads/sites/111/2021/12/09_Panou.pdf>}.
16
+
17
+ @var Triaxials.Amalthea: Triaxial(name='Amalthea', a=125000, b=73000, c=64000, e2ab=0.658944, e2bc=0.231375493, e2ac=0.737856, volume=2446253479595252, area=93239507787.490371704, area_p=93212299402.670425415)
18
+ @var Triaxials.Ariel: Triaxial(name='Ariel', a=581100, b=577900, c=577700, e2ab=0.01098327, e2bc=0.000692042, e2ac=0.011667711, volume=812633172614203904, area=4211301462766.580078125, area_p=4211301574065.829589844)
19
+ @var Triaxials.Earth: Triaxial(name='Earth', a=6378173.435, b=6378103.9, c=6356754.399999999, e2ab=0.000021804, e2bc=0.006683418, e2ac=0.006705077, volume=1083208241574987694080, area=510065911057441.0625, area_p=510065915922713.6875)
20
+ @var Triaxials.Enceladus: Triaxial(name='Enceladus', a=256600, b=251400, c=248300, e2ab=0.040119337, e2bc=0.024509841, e2ac=0.06364586, volume=67094551514082248, area=798618496278.596679688, area_p=798619018175.109863281)
21
+ @var Triaxials.Europa: Triaxial(name='Europa', a=1564130, b=1561230, c=1560930, e2ab=0.003704694, e2bc=0.000384275, e2ac=0.004087546, volume=15966575194402123776, area=30663773697323.51953125, area_p=30663773794562.45703125)
22
+ @var Triaxials.Io: Triaxial(name='Io', a=1829400, b=1819300, c=1815700, e2ab=0.011011391, e2bc=0.003953651, e2ac=0.014921506, volume=25313121117889765376, area=41691875849096.7421875, area_p=41691877397441.2109375)
23
+ @var Triaxials.Mars: Triaxial(name='Mars', a=3394600, b=3393300, c=3376300, e2ab=0.000765776, e2bc=0.009994646, e2ac=0.010752768, volume=162907283585817247744, area=144249140795107.4375, area_p=144249144150662.15625)
24
+ @var Triaxials.Mimas: Triaxial(name='Mimas', a=207400, b=196800, c=190600, e2ab=0.09960581, e2bc=0.062015624, e2ac=0.155444317, volume=32587072869017956, area=493855762247.691894531, area_p=493857714107.9375)
25
+ @var Triaxials.Miranda: Triaxial(name='Miranda', a=240400, b=234200, c=232900, e2ab=0.050915557, e2bc=0.011070811, e2ac=0.061422691, volume=54926187094835456, area=698880863325.756958008, area_p=698881306767.950317383)
26
+ @var Triaxials.Moon: Triaxial(name='Moon', a=1735550, b=1735324, c=1734898, e2ab=0.000260419, e2bc=0.000490914, e2ac=0.000751206, volume=21886698675223740416, area=37838824729886.09375, area_p=37838824733332.2265625)
27
+ @var Triaxials.Tethys: Triaxial(name='Tethys', a=535600, b=528200, c=525800, e2ab=0.027441672, e2bc=0.009066821, e2ac=0.036259685, volume=623086233855821440, area=3528073490771.394042969, area_p=3528074261832.738769531)
28
+ @var Triaxials.WGS84_35: Triaxial(name='WGS84_35', a=6378172, b=6378102, c=6356752.314245179, e2ab=0.00002195, e2bc=0.006683478, e2ac=0.006705281, volume=1083207319768789942272, area=510065621722018.125, area_p=510065626587483.3125)
29
+ '''
30
+ # make sure int/int division yields float quotient, see .basics
31
+ from __future__ import division as _; del _ # PYCHOK semicolon
32
+
33
+ from pygeodesy.basics import isLatLon, isscalar, map2, _zip, _ValueError
34
+ from pygeodesy.constants import EPS, EPS0, EPS02, EPS4, INT0, PI2, PI_3, PI4, \
35
+ _EPS2e4, float0_, isfinite, isnear1, _0_0, _0_5, \
36
+ _1_0, _N_1_0, _N_2_0, _4_0 # PYCHOK used!
37
+ from pygeodesy.datums import Datum, _spherical_datum, _WGS84, Ellipsoid, _EWGS84, Fmt
38
+ # from pygeodesy.dms import toDMS # _MODS
39
+ # from pygeodesy.ellipsoids import Ellipsoid, _EWGS84 # from .datums
40
+ # from pygeodesy.elliptic import Elliptic # _MODS
41
+ # from pygeodesy.errors import _ValueError # from .basics
42
+ from pygeodesy.fmath import Fdot, fdot, fmean_, hypot, hypot_, norm2, sqrt0
43
+ from pygeodesy.fsums import Fsum, fsumf_, fsum1f_
44
+ from pygeodesy.interns import NN, _a_, _b_, _beta_, _c_, _distant_, _finite_, \
45
+ _height_, _inside_, _near_, _negative_, _not_, \
46
+ _NOTEQUAL_, _null_, _opposite_, _outside_, _SPACE_, \
47
+ _spherical_, _too_, _x_, _y_
48
+ # from pygeodesy.lazily import _ALL_LAZY, _ALL_MODS as _MODS # from .vector3d
49
+ from pygeodesy.named import _NamedEnum, _NamedEnumItem, _NamedTuple, _Pass, \
50
+ _lazyNamedEnumItem as _lazy
51
+ from pygeodesy.namedTuples import LatLon3Tuple, Vector3Tuple, Vector4Tuple
52
+ from pygeodesy.props import Property_RO, property_RO
53
+ # from pygeodesy.streprs import Fmt # from .datums
54
+ from pygeodesy.units import Float, Height_, Meter, Meter2, Meter3, Radians, \
55
+ Radius, Scalar_, _toDegrees, _toRadians
56
+ from pygeodesy.utily import asin1, atan2d, km2m, m2km, SinCos2, sincos2d_
57
+ from pygeodesy.vector3d import _otherV3d, Vector3d, _ALL_LAZY, _MODS
58
+
59
+ from math import atan2, fabs, sqrt
60
+
61
+ __all__ = _ALL_LAZY.triaxials
62
+ __version__ = '24.01.21'
63
+
64
+ _not_ordered_ = _not_('ordered')
65
+ _omega_ = 'omega'
66
+ _TRIPS = 269 # 48-55, Eberly 1074?
67
+
68
+
69
+ class _NamedTupleTo(_NamedTuple): # in .testNamedTuples, like .cartesianBase.RadiusThetaPhi3Tuple
70
+ '''(INTERNAL) Base for C{-.toDegrees}, C{-.toRadians}.
71
+ '''
72
+ def _toDegrees(self, a, b, *c, **toDMS_kwds):
73
+ a, b, _ = _toDegrees(self, a, b, **toDMS_kwds)
74
+ return _ or self.classof(a, b, *c, name=self.name)
75
+
76
+ def _toRadians(self, a, b, *c):
77
+ a, b, _ = _toRadians(self, a, b)
78
+ return _ or self.classof(a, b, *c, name=self.name)
79
+
80
+
81
+ class BetaOmega2Tuple(_NamedTupleTo):
82
+ '''2-Tuple C{(beta, omega)} with I{ellipsoidal} lat- and
83
+ longitude C{beta} and C{omega} both in L{Radians} (or
84
+ L{Degrees}).
85
+ '''
86
+ _Names_ = (_beta_, _omega_)
87
+ _Units_ = (_Pass, _Pass)
88
+
89
+ def toDegrees(self, **toDMS_kwds):
90
+ '''Convert this L{BetaOmega2Tuple} to L{Degrees} or C{toDMS}.
91
+
92
+ @return: L{BetaOmega2Tuple}C{(beta, omega)} with
93
+ C{beta} and C{omega} both in L{Degrees}
94
+ or as a L{toDMS} string provided some
95
+ B{C{toDMS_kwds}} keyword arguments are
96
+ specified.
97
+ '''
98
+ return _NamedTupleTo._toDegrees(self, *self, **toDMS_kwds)
99
+
100
+ def toRadians(self):
101
+ '''Convert this L{BetaOmega2Tuple} to L{Radians}.
102
+
103
+ @return: L{BetaOmega2Tuple}C{(beta, omega)} with
104
+ C{beta} and C{omega} both in L{Radians}.
105
+ '''
106
+ return _NamedTupleTo._toRadians(self, *self)
107
+
108
+
109
+ class BetaOmega3Tuple(_NamedTupleTo):
110
+ '''3-Tuple C{(beta, omega, height)} with I{ellipsoidal} lat- and
111
+ longitude C{beta} and C{omega} both in L{Radians} (or L{Degrees})
112
+ and the C{height}, rather the (signed) I{distance} to the triaxial's
113
+ surface (measured along the radial line to the triaxial's center)
114
+ in C{meter}, conventionally.
115
+ '''
116
+ _Names_ = BetaOmega2Tuple._Names_ + (_height_,)
117
+ _Units_ = BetaOmega2Tuple._Units_ + ( Meter,)
118
+
119
+ def toDegrees(self, **toDMS_kwds):
120
+ '''Convert this L{BetaOmega3Tuple} to L{Degrees} or C{toDMS}.
121
+
122
+ @return: L{BetaOmega3Tuple}C{(beta, omega, height)} with
123
+ C{beta} and C{omega} both in L{Degrees} or as a
124
+ L{toDMS} string provided some B{C{toDMS_kwds}}
125
+ keyword arguments are specified.
126
+ '''
127
+ return _NamedTupleTo._toDegrees(self, *self, **toDMS_kwds)
128
+
129
+ def toRadians(self):
130
+ '''Convert this L{BetaOmega3Tuple} to L{Radians}.
131
+
132
+ @return: L{BetaOmega3Tuple}C{(beta, omega, height)} with
133
+ C{beta} and C{omega} both in L{Radians}.
134
+ '''
135
+ return _NamedTupleTo._toRadians(self, *self)
136
+
137
+ def to2Tuple(self):
138
+ '''Reduce this L{BetaOmega3Tuple} to a L{BetaOmega2Tuple}.
139
+ '''
140
+ return BetaOmega2Tuple(*self[:2])
141
+
142
+
143
+ class Jacobi2Tuple(_NamedTupleTo):
144
+ '''2-Tuple C{(x, y)} with a Jacobi Conformal C{x} and C{y}
145
+ projection, both in L{Radians} (or L{Degrees}).
146
+ '''
147
+ _Names_ = (_x_, _y_)
148
+ _Units_ = (_Pass, _Pass)
149
+
150
+ def toDegrees(self, **toDMS_kwds):
151
+ '''Convert this L{Jacobi2Tuple} to L{Degrees} or C{toDMS}.
152
+
153
+ @return: L{Jacobi2Tuple}C{(x, y)} with C{x} and C{y}
154
+ both in L{Degrees} or as a L{toDMS} string
155
+ provided some B{C{toDMS_kwds}} keyword
156
+ arguments are specified.
157
+ '''
158
+ return _NamedTupleTo._toDegrees(self, *self, **toDMS_kwds)
159
+
160
+ def toRadians(self):
161
+ '''Convert this L{Jacobi2Tuple} to L{Radians}.
162
+
163
+ @return: L{Jacobi2Tuple}C{(x, y)} with C{x}
164
+ and C{y} both in L{Radians}.
165
+ '''
166
+ return _NamedTupleTo._toRadians(self, *self)
167
+
168
+
169
+ class Triaxial_(_NamedEnumItem):
170
+ '''I{Unordered} triaxial ellipsoid and base class.
171
+
172
+ Triaxial ellipsoids with right-handed semi-axes C{a}, C{b} and C{c}, oriented
173
+ such that the large principal ellipse C{ab} is the equator I{Z}=0, I{beta}=0,
174
+ while the small principal ellipse C{ac} is the prime meridian, plane I{Y}=0,
175
+ I{omega}=0.
176
+
177
+ The four umbilic points, C{abs}(I{omega}) = C{abs}(I{beta}) = C{PI/2}, lie on
178
+ the middle principal ellipse C{bc} in plane I{X}=0, I{omega}=C{PI/2}.
179
+
180
+ @note: I{Geodetic} C{lat}- and C{lon}gitudes are in C{degrees}, I{geodetic}
181
+ C{phi} and C{lam}bda are in C{radians}, but I{ellipsoidal} lat- and
182
+ longitude C{beta} and C{omega} are in L{Radians} by default (or in
183
+ L{Degrees} if converted).
184
+ '''
185
+ _ijk = _kji = None
186
+ _unordered = True
187
+
188
+ def __init__(self, a_triaxial, b=None, c=None, name=NN):
189
+ '''New I{unordered} L{Triaxial_}.
190
+
191
+ @arg a_triaxial: Large, C{X} semi-axis (C{scalar}, conventionally in
192
+ C{meter}) or an other L{Triaxial} or L{Triaxial_} instance.
193
+ @kwarg b: Middle, C{Y} semi-axis (C{meter}, same units as B{C{a}}), required
194
+ if C{B{a_triaxial} is scalar}, ignored otherwise.
195
+ @kwarg c: Small, C{Z} semi-axis (C{meter}, same units as B{C{a}}), required
196
+ if C{B{a_triaxial} is scalar}, ignored otherwise.
197
+ @kwarg name: Optional name (C{str}).
198
+
199
+ @raise TriaxialError: Invalid semi-axis or -axes.
200
+ '''
201
+ try:
202
+ a = a_triaxial
203
+ t = a._abc3 if isinstance(a, Triaxial_) else (
204
+ Radius(a=a), Radius(b=b), Radius(c=c))
205
+ except (TypeError, ValueError) as x:
206
+ raise TriaxialError(a=a, b=b, c=c, cause=x)
207
+ if name:
208
+ self.name = name
209
+
210
+ a, b, c = self._abc3 = t
211
+ if self._unordered: # == not isinstance(self, Triaxial)
212
+ s, _, t = sorted(t)
213
+ if not (isfinite(t) and s > 0):
214
+ raise TriaxialError(a=a, b=b, c=c) # txt=_invalid_
215
+ elif not (isfinite(a) and a >= b >= c > 0):
216
+ raise TriaxialError(a=a, b=b, c=c, txt=_not_ordered_)
217
+ elif not (a > c and self._a2c2 > 0 and self.e2ac > 0):
218
+ raise TriaxialError(a=a, c=c, e2ac=self.e2ac, txt=_spherical_)
219
+
220
+ def __str__(self):
221
+ return self.toStr()
222
+
223
+ @Property_RO
224
+ def a(self):
225
+ '''Get the (largest) C{x} semi-axis (C{meter}, conventionally).
226
+ '''
227
+ a, _, _ = self._abc3
228
+ return a
229
+
230
+ @Property_RO
231
+ def _a2b2(self):
232
+ '''(INTERNAL) Get C{a**2 - b**2} == E_sub_e**2.
233
+ '''
234
+ a, b, _ = self._abc3
235
+ return ((a - b) * (a + b)) if a != b else _0_0
236
+
237
+ @Property_RO
238
+ def _a2_b2(self):
239
+ '''(INTERNAL) Get C{(a/b)**2}.
240
+ '''
241
+ a, b, _ = self._abc3
242
+ return (a / b)**2 if a != b else _1_0
243
+
244
+ @Property_RO
245
+ def _a2c2(self):
246
+ '''(INTERNAL) Get C{a**2 - c**2} == E_sub_x**2.
247
+ '''
248
+ a, _, c = self._abc3
249
+ return ((a - c) * (a + c)) if a != c else _0_0
250
+
251
+ @Property_RO
252
+ def area(self):
253
+ '''Get the surface area (C{meter} I{squared}).
254
+ '''
255
+ c, b, a = sorted(self._abc3)
256
+ if a > c:
257
+ a = Triaxial(a, b, c).area if a > b else \
258
+ Ellipsoid(a, b=c).areax # a == b
259
+ else: # a == c == b
260
+ a = Meter2(area=a**2 * PI4)
261
+ return a
262
+
263
+ def area_p(self, p=1.6075):
264
+ '''I{Approximate} the surface area (C{meter} I{squared}).
265
+
266
+ @kwarg p: Exponent (C{scalar} > 0), 1.6 for near-spherical or 1.5849625007
267
+ for "near-flat" triaxials.
268
+
269
+ @see: U{Surface area<https://WikiPedia.org/wiki/Ellipsoid#Approximate_formula>}.
270
+ '''
271
+ a, b, c = self._abc3
272
+ if a == b == c:
273
+ a *= a
274
+ else:
275
+ _p = pow
276
+ a = _p(fmean_(_p(a * b, p), _p(a * c, p), _p(b * c, p)), _1_0 / p)
277
+ return Meter2(area_p=a * PI4)
278
+
279
+ @Property_RO
280
+ def b(self):
281
+ '''Get the (middle) C{y} semi-axis (C{meter}, same units as B{C{a}}).
282
+ '''
283
+ _, b, _ = self._abc3
284
+ return b
285
+
286
+ @Property_RO
287
+ def _b2c2(self):
288
+ '''(INTERNAL) Get C{b**2 - c**2} == E_sub_y**2.
289
+ '''
290
+ _, b, c = self._abc3
291
+ return ((b - c) * (b + c)) if b != c else _0_0
292
+
293
+ @Property_RO
294
+ def c(self):
295
+ '''Get the (smallest) C{z} semi-axis (C{meter}, same units as B{C{a}}).
296
+ '''
297
+ _, _, c = self._abc3
298
+ return c
299
+
300
+ @Property_RO
301
+ def _c2_b2(self):
302
+ '''(INTERNAL) Get C{(c/b)**2}.
303
+ '''
304
+ _, b, c = self._abc3
305
+ return (c / b)**2 if b != c else _1_0
306
+
307
+ @Property_RO
308
+ def e2ab(self):
309
+ '''Get the C{ab} ellipse' I{(1st) eccentricity squared} (C{scalar}), M{1 - (b/a)**2}.
310
+ '''
311
+ return Float(e2ab=(_1_0 - self._1e2ab) or _0_0)
312
+
313
+ @Property_RO
314
+ def _1e2ab(self):
315
+ '''(INTERNAL) Get C{1 - e2ab} == C{(b/a)**2}.
316
+ '''
317
+ a, b, _ = self._abc3
318
+ return (b / a)**2 if a != b else _1_0
319
+
320
+ @Property_RO
321
+ def e2ac(self):
322
+ '''Get the C{ac} ellipse' I{(1st) eccentricity squared} (C{scalar}), M{1 - (c/a)**2}.
323
+ '''
324
+ return Float(e2ac=(_1_0 - self._1e2ac) or _0_0)
325
+
326
+ @Property_RO
327
+ def _1e2ac(self):
328
+ '''(INTERNAL) Get C{1 - e2ac} == C{(c/a)**2}.
329
+ '''
330
+ a, _, c = self._abc3
331
+ return (c / a)**2 if a != c else _1_0
332
+
333
+ @Property_RO
334
+ def e2bc(self):
335
+ '''Get the C{bc} ellipse' I{(1st) eccentricity squared} (C{scalar}), M{1 - (c/b)**2}.
336
+ '''
337
+ return Float(e2bc=(_1_0 - self._1e2bc) or _0_0)
338
+
339
+ _1e2bc = _c2_b2 # C{1 - e2bc} == C{(c/b)**2}
340
+
341
+ @property_RO
342
+ def _Elliptic(self):
343
+ '''(INTERNAL) Get class L{Elliptic}, I{once}.
344
+ '''
345
+ Triaxial_._Elliptic = E = _MODS.elliptic.Elliptic # overwrite property_RO
346
+ return E
347
+
348
+ def hartzell4(self, pov, los=False, name=NN):
349
+ '''Compute the intersection of this triaxial's surface with a Line-Of-Sight
350
+ from a Point-Of-View in space.
351
+
352
+ @see: Function L{hartzell4<triaxials.hartzell4>} for further details.
353
+ '''
354
+ return hartzell4(pov, los=los, tri_biax=self, name=name)
355
+
356
+ def height4(self, x_xyz, y=None, z=None, normal=True, eps=EPS):
357
+ '''Compute the projection on and the height above or below this triaxial's surface.
358
+
359
+ @arg x_xyz: X component (C{scalar}) or a cartesian (C{Cartesian}, L{Ecef9Tuple},
360
+ L{Vector3d}, L{Vector3Tuple} or L{Vector4Tuple}).
361
+ @kwarg y: Y component (C{scalar}), required if B{C{x_xyz}} if C{scalar}.
362
+ @kwarg z: Z component (C{scalar}), required if B{C{x_xyz}} if C{scalar}.
363
+ @kwarg normal: If C{True} the projection is the I{normal, plumb} to the surface of,
364
+ otherwise the C{radial} line to the center of this triaxial (C{bool}).
365
+ @kwarg eps: Tolerance for root finding and validation (C{scalar}), use a negative
366
+ value to skip validation.
367
+
368
+ @return: L{Vector4Tuple}C{(x, y, z, h)} with the cartesian coordinates C{x}, C{y}
369
+ and C{z} of the projection on or the intersection with and with the height
370
+ C{h} above or below the triaxial's surface in C{meter}, conventionally.
371
+
372
+ @raise TriaxialError: Non-cartesian B{C{xyz}}, invalid B{C{eps}}, no convergence in
373
+ root finding or validation failed.
374
+
375
+ @see: Method L{Ellipsoid.height4} and I{Eberly}'s U{Distance from a Point to ...
376
+ <https://www.GeometricTools.com/Documentation/DistancePointEllipseEllipsoid.pdf>}.
377
+ '''
378
+ v, r = _otherV3d_(x_xyz, y, z), self.isSpherical
379
+
380
+ i, h = None, v.length
381
+ if h < EPS0: # EPS
382
+ x = y = z = _0_0
383
+ h -= min(self._abc3) # nearest
384
+ elif r: # .isSpherical
385
+ x, y, z = v.times(r / h).xyz
386
+ h -= r
387
+ else:
388
+ x, y, z = v.xyz
389
+ try:
390
+ if normal: # plumb to surface
391
+ x, y, z, h, i = _normalTo5(x, y, z, self, eps=eps)
392
+ else: # radial to center
393
+ x, y, z = self._radialTo3(z, hypot(x, y), y, x)
394
+ h = v.minus_(x, y, z).length
395
+ except Exception as e:
396
+ raise TriaxialError(x=x, y=y, z=z, cause=e)
397
+ if h > 0 and self.sideOf(v, eps=EPS0) < 0:
398
+ h = -h # below the surface
399
+ return Vector4Tuple(x, y, z, h, iteration=i, name=self.height4.__name__)
400
+
401
+ @Property_RO
402
+ def isOrdered(self):
403
+ '''Is this triaxial I{ordered} and I{not spherical} (C{bool})?
404
+ '''
405
+ a, b, c = self._abc3
406
+ return bool(a >= b > c) # b > c!
407
+
408
+ @Property_RO
409
+ def isSpherical(self):
410
+ '''Is this triaxial I{spherical} (C{Radius} or INT0)?
411
+ '''
412
+ a, b, c = self._abc3
413
+ return a if a == b == c else INT0
414
+
415
+ def _norm2(self, s, c, *a):
416
+ '''(INTERNAL) Normalize C{s} and C{c} iff not already.
417
+ '''
418
+ if fabs(_hypot21(s, c)) > EPS02:
419
+ s, c = norm2(s, c)
420
+ if a:
421
+ s, c = norm2(s * self.b, c * a[0])
422
+ return float0_(s, c)
423
+
424
+ def normal3d(self, x_xyz, y=None, z=None, length=_1_0):
425
+ '''Get a 3-D vector at a cartesian on and perpendicular to this triaxial's surface.
426
+
427
+ @arg x_xyz: X component (C{scalar}) or a cartesian (C{Cartesian},
428
+ L{Ecef9Tuple}, L{Vector3d}, L{Vector3Tuple} or L{Vector4Tuple}).
429
+ @kwarg y: Y component (C{scalar}), required if B{C{x_xyz}} if C{scalar}.
430
+ @kwarg z: Z component (C{scalar}), required if B{C{x_xyz}} if C{scalar}.
431
+ @kwarg length: Optional length and in-/outward direction (C{scalar}).
432
+
433
+ @return: A C{Vector3d(x_, y_, z_)} normalized to B{C{length}}, pointing
434
+ in- or outward for neg- respectively positive B{C{length}}.
435
+
436
+ @raise TriaxialError: Zero length cartesian or vector.
437
+
438
+ @note: Cartesian location C{(B{x}, B{y}, B{z})} must be on this triaxial's
439
+ surface, use method L{Triaxial.sideOf} to validate.
440
+ '''
441
+ # n = 2 * (x / a2, y / b2, z / c2)
442
+ # == 2 * (x, y * a2 / b2, z * a2 / c2) / a2 # iff ordered
443
+ # == 2 * (x, y / _1e2ab, z / _1e2ac) / a2
444
+ # == unit(x, y / _1e2ab, z / _1e2ac).times(length)
445
+ n = self._normal3d.times_(*_otherV3d_(x_xyz, y, z).xyz)
446
+ if n.length < EPS0:
447
+ raise TriaxialError(x=x_xyz, y=y, z=z, txt=_null_)
448
+ return n.times(length / n.length)
449
+
450
+ @Property_RO
451
+ def _normal3d(self):
452
+ '''(INTERNAL) Get M{Vector3d((d/a)**2, (d/b)**2, (d/c)**2)}, M{d = max(a, b, c)}.
453
+ '''
454
+ d = max(self._abc3)
455
+ t = tuple(((d / x)**2 if x != d else _1_0) for x in self._abc3)
456
+ return Vector3d(*t, name=self.normal3d.__name__)
457
+
458
+ def _order3(self, *abc, **reverse): # reverse=False
459
+ '''(INTERNAL) Un-/Order C{a}, C{b} and C{c}.
460
+
461
+ @return: 3-Tuple C{(a, b, c)} ordered by or un-ordered
462
+ (reverse-ordered) C{ijk} if C{B{reverse}=True}.
463
+ '''
464
+ ijk = self._order_ijk(**reverse)
465
+ return _getitems(abc, *ijk) if ijk else abc
466
+
467
+ def _order3d(self, v, **reverse): # reverse=False
468
+ '''(INTERNAL) Un-/Order a C{Vector3d}.
469
+
470
+ @return: Vector3d(x, y, z) un-/ordered.
471
+ '''
472
+ ijk = self._order_ijk(**reverse)
473
+ return v.classof(*_getitems(v.xyz, *ijk)) if ijk else v
474
+
475
+ @Property_RO
476
+ def _ordered4(self):
477
+ '''(INTERNAL) Helper for C{_hartzell3} and C{_normalTo5}.
478
+ '''
479
+ def _order2(reverse, a, b, c):
480
+ '''(INTERNAL) Un-Order C{a}, C{b} and C{c}.
481
+
482
+ @return: 2-Tuple C{((a, b, c), ijk)} with C{a} >= C{b} >= C{c}
483
+ and C{ijk} a 3-tuple with the initial indices.
484
+ '''
485
+ i, j, k = 0, 1, 2 # range(3)
486
+ if a < b:
487
+ a, b, i, j = b, a, j, i
488
+ if a < c:
489
+ a, c, i, k = c, a, k, i
490
+ if b < c:
491
+ b, c, j, k = c, b, k, j
492
+ # reverse (k, j, i) since (a, b, c) is reversed-sorted
493
+ ijk = (k, j, i) if reverse else (None if i < j < k else (i, j, k))
494
+ return (a, b, c), ijk
495
+
496
+ abc, T = self._abc3, self
497
+ if not self.isOrdered:
498
+ abc, ijk = _order2(False, *abc)
499
+ if ijk:
500
+ _, kji = _order2(True, *ijk)
501
+ T = Triaxial_(*abc)
502
+ T._ijk, T._kji = ijk, kji
503
+ return abc + (T,)
504
+
505
+ def _order_ijk(self, reverse=False):
506
+ '''(INTERNAL) Get the un-/order indices.
507
+ '''
508
+ return self._kji if reverse else self._ijk
509
+
510
+ def _radialTo3(self, sbeta, cbeta, somega, comega):
511
+ '''(INTERNAL) I{Unordered} helper for C{.height4}.
512
+ '''
513
+ def _rphi(a, b, sphi, cphi):
514
+ # <https://WikiPedia.org/wiki/Ellipse#Polar_form_relative_to_focus>
515
+ # polar form: radius(phi) = a * b / hypot(a * sphi, b * cphi)
516
+ return (b / hypot(sphi, b / a * cphi)) if a > b else (
517
+ (a / hypot(cphi, a / b * sphi)) if a < b else a)
518
+
519
+ sa, ca = self._norm2(sbeta, cbeta)
520
+ sb, cb = self._norm2(somega, comega)
521
+
522
+ a, b, c = self._abc3
523
+ if a != b:
524
+ a = _rphi(a, b, sb, cb)
525
+ if a != c:
526
+ c = _rphi(a, c, sa, ca)
527
+ z, r = c * sa, c * ca
528
+ x, y = r * cb, r * sb
529
+ return x, y, z
530
+
531
+ def sideOf(self, x_xyz, y=None, z=None, eps=EPS4):
532
+ '''Is a cartesian above, below or on the surface of this triaxial?
533
+
534
+ @arg x_xyz: X component (C{scalar}) or a cartesian (C{Cartesian},
535
+ L{Ecef9Tuple}, L{Vector3d}, L{Vector3Tuple} or L{Vector4Tuple}).
536
+ @kwarg y: Y component (C{scalar}), required if B{C{x_xyz}} if C{scalar}.
537
+ @kwarg z: Z component (C{scalar}), required if B{C{x_xyz}} if C{scalar}.
538
+ @kwarg eps: Near-surface tolerance (C{scalar}, distance I{squared}).
539
+
540
+ @return: C{INT0} if C{(B{x}, B{y}, B{z})} is near this triaxial's surface
541
+ within tolerance B{C{eps}}, otherwise a signed, radial, normalized
542
+ distance I{squared} (C{float}), negative or positive for in-
543
+ respectively outside this triaxial.
544
+
545
+ @see: Methods L{Triaxial.height4} and L{Triaxial.normal3d}.
546
+ '''
547
+ return _sideOf(_otherV3d_(x_xyz, y, z).xyz, self._abc3, eps=eps)
548
+
549
+ def toEllipsoid(self, name=NN):
550
+ '''Convert this triaxial to an L{Ellipsoid}, provided 2 axes match.
551
+
552
+ @return: An L{Ellipsoid} with north along this C{Z} axis if C{a == b},
553
+ this C{Y} axis if C{a == c} or this C{X} axis if C{b == c}.
554
+
555
+ @raise TriaxialError: This C{a != b}, C{b != c} and C{c != a}.
556
+
557
+ @see: Method L{Ellipsoid.toTriaxial}.
558
+ '''
559
+ a, b, c = self._abc3
560
+ if a == b:
561
+ b = c # N = c-Z
562
+ elif b == c: # N = a-X
563
+ a, b = b, a
564
+ elif a != c: # N = b-Y
565
+ t = _SPACE_(_a_, _NOTEQUAL_, _b_, _NOTEQUAL_, _c_)
566
+ raise TriaxialError(a=a, b=b, c=c, txt=t)
567
+ return Ellipsoid(a, b=b, name=name or self.name)
568
+
569
+ def toStr(self, prec=9, name=NN, **unused): # PYCHOK signature
570
+ '''Return this C{Triaxial} as a string.
571
+
572
+ @kwarg prec: Precision, number of decimal digits (0..9).
573
+ @kwarg name: Override name (C{str}) or C{None} to exclude
574
+ this triaxial's name.
575
+
576
+ @return: This C{Triaxial}'s attributes (C{str}).
577
+ '''
578
+ T = Triaxial_
579
+ t = T.a,
580
+ J = JacobiConformalSpherical
581
+ t += (J.ab, J.bc) if isinstance(self, J) else (T.b, T.c)
582
+ t += T.e2ab, T.e2bc, T.e2ac
583
+ J = JacobiConformal
584
+ if isinstance(self, J):
585
+ t += J.xyQ2,
586
+ t += T.volume, T.area
587
+ return self._instr(name, prec, props=t, area_p=self.area_p())
588
+
589
+ @Property_RO
590
+ def volume(self):
591
+ '''Get the volume (C{meter**3}), M{4 / 3 * PI * a * b * c}.
592
+ '''
593
+ a, b, c = self._abc3
594
+ return Meter3(volume=a * b * c * PI_3 * _4_0)
595
+
596
+
597
+ class Triaxial(Triaxial_):
598
+ '''I{Ordered} triaxial ellipsoid.
599
+
600
+ @see: L{Triaxial_} for more information.
601
+ '''
602
+ _unordered = False
603
+
604
+ def __init__(self, a_triaxial, b=None, c=None, name=NN):
605
+ '''New I{ordered} L{Triaxial}.
606
+
607
+ @arg a_triaxial: Largest semi-axis (C{scalar}, conventionally in C{meter})
608
+ or an other L{Triaxial} or L{Triaxial_} instance.
609
+ @kwarg b: Middle semi-axis (C{meter}, same units as B{C{a}}), required
610
+ if C{B{a_triaxial} is scalar}, ignored otherwise.
611
+ @kwarg c: Smallest semi-axis (C{meter}, same units as B{C{a}}), required
612
+ if C{B{a_triaxial} is scalar}, ignored otherwise.
613
+ @kwarg name: Optional name (C{str}).
614
+
615
+ @note: The semi-axes must be ordered as C{B{a} >= B{b} >= B{c} > 0} and
616
+ must be ellipsoidal, C{B{a} > B{c}}.
617
+
618
+ @raise TriaxialError: Semi-axes not ordered, spherical or invalid.
619
+ '''
620
+ Triaxial_.__init__(self, a_triaxial, b=b, c=c, name=name)
621
+
622
+ @Property_RO
623
+ def _a2b2_a2c2(self):
624
+ '''@see: Methods C{.forwardBetaOmega} and C{._k2_kp2}.
625
+ '''
626
+ return self._a2b2 / self._a2c2
627
+
628
+ @Property_RO
629
+ def area(self):
630
+ '''Get the surface area (C{meter} I{squared}).
631
+
632
+ @see: U{Surface area<https://WikiPedia.org/wiki/Ellipsoid#Surface_area>}.
633
+ '''
634
+ a, b, c = self._abc3
635
+ if a != b:
636
+ kp2, k2 = self._k2_kp2 # swapped!
637
+ aE = self._Elliptic(k2, _0_0, kp2, _1_0)
638
+ c2 = self._1e2ac # cos(phi)**2 = (c/a)**2
639
+ s = sqrt(self.e2ac) # sin(phi)**2 = 1 - c2
640
+ r = asin1(s) # phi = atan2(sqrt(c2), s)
641
+ b *= fsum1f_(aE.fE(r) * s, c / a * c / b,
642
+ aE.fF(r) * c2 / s)
643
+ a = Meter2(area=a * b * PI2)
644
+ else: # a == b > c
645
+ a = Ellipsoid(a, b=c).areax
646
+ return a
647
+
648
+ def _exyz3(self, u):
649
+ '''(INTERNAL) Helper for C{.forwardBetOmg}.
650
+ '''
651
+ if u > 0:
652
+ u2 = u**2
653
+ x = u * sqrt0(_1_0 + self._a2c2 / u2, Error=TriaxialError)
654
+ y = u * sqrt0(_1_0 + self._b2c2 / u2, Error=TriaxialError)
655
+ else:
656
+ x = y = u = _0_0
657
+ return x, y, u
658
+
659
+ def forwardBetaOmega(self, beta, omega, height=0, name=NN):
660
+ '''Convert I{ellipsoidal} lat- and longitude C{beta}, C{omega}
661
+ and height to cartesian.
662
+
663
+ @arg beta: Ellipsoidal latitude (C{radians} or L{Degrees}).
664
+ @arg omega: Ellipsoidal longitude (C{radians} or L{Degrees}).
665
+ @arg height: Height above or below the ellipsoid's surface (C{meter}, same
666
+ units as this triaxial's C{a}, C{b} and C{c} semi-axes).
667
+ @kwarg name: Optional name (C{str}).
668
+
669
+ @return: A L{Vector3Tuple}C{(x, y, z)}.
670
+
671
+ @see: Method L{Triaxial.reverseBetaOmega} and U{Expressions (23-25)<https://
672
+ www.Topo.Auth.GR/wp-content/uploads/sites/111/2021/12/09_Panou.pdf>}.
673
+ '''
674
+ if height:
675
+ h = self._Height(height)
676
+ x, y, z = self._exyz3(h + self.c)
677
+ else:
678
+ x, y, z = self._abc3 # == self._exyz3(self.c)
679
+ if z: # and x and y:
680
+ sa, ca = SinCos2(beta)
681
+ sb, cb = SinCos2(omega)
682
+
683
+ r = self._a2b2_a2c2
684
+ x *= cb * sqrt0(ca**2 + r * sa**2, Error=TriaxialError)
685
+ y *= ca * sb
686
+ z *= sa * sqrt0(_1_0 - r * cb**2, Error=TriaxialError)
687
+ return Vector3Tuple(x, y, z, name=name)
688
+
689
+ def forwardBetaOmega_(self, sbeta, cbeta, somega, comega, name=NN):
690
+ '''Convert I{ellipsoidal} lat- and longitude C{beta} and C{omega}
691
+ to cartesian coordinates I{on the triaxial's surface}.
692
+
693
+ @arg sbeta: Ellipsoidal latitude C{sin(beta)} (C{scalar}).
694
+ @arg cbeta: Ellipsoidal latitude C{cos(beta)} (C{scalar}).
695
+ @arg somega: Ellipsoidal longitude C{sin(omega)} (C{scalar}).
696
+ @arg comega: Ellipsoidal longitude C{cos(omega)} (C{scalar}).
697
+ @kwarg name: Optional name (C{str}).
698
+
699
+ @return: A L{Vector3Tuple}C{(x, y, z)} on the surface.
700
+
701
+ @raise TriaxialError: This triaxial is near-spherical.
702
+
703
+ @see: Method L{Triaxial.reverseBetaOmega}, U{Triaxial ellipsoid coordinate
704
+ system<https://WikiPedia.org/wiki/Geodesics_on_an_ellipsoid#
705
+ Triaxial_ellipsoid_coordinate_system>} and U{expressions (23-25)<https://
706
+ www.Topo.Auth.GR/wp-content/uploads/sites/111/2021/12/09_Panou.pdf>}.
707
+ '''
708
+ t = self._radialTo3(sbeta, cbeta, somega, comega)
709
+ return Vector3Tuple(*t, name=name)
710
+
711
+ def forwardCartesian(self, x_xyz, y=None, z=None, name=NN, **normal_eps):
712
+ '''Project a cartesian on this triaxial.
713
+
714
+ @arg x_xyz: X component (C{scalar}) or a cartesian (C{Cartesian},
715
+ L{Ecef9Tuple}, L{Vector3d}, L{Vector3Tuple} or L{Vector4Tuple}).
716
+ @kwarg y: Y component (C{scalar}), required if B{C{x_xyz}} if C{scalar}.
717
+ @kwarg z: Z component (C{scalar}), required if B{C{x_xyz}} if C{scalar}.
718
+ @kwarg name: Optional name (C{str}).
719
+ @kwarg normal_eps: Optional keyword arguments C{B{normal}=True} and
720
+ C{B{eps}=EPS}, see method L{Triaxial.height4}.
721
+
722
+ @see: Method L{Triaxial.height4} for further information and method
723
+ L{Triaxial.reverseCartesian} to reverse the projection.
724
+ '''
725
+ t = self.height4(x_xyz, y, z, **normal_eps)
726
+ _ = t.rename(name)
727
+ return t
728
+
729
+ def forwardLatLon(self, lat, lon, height=0, name=NN):
730
+ '''Convert I{geodetic} lat-, longitude and heigth to cartesian.
731
+
732
+ @arg lat: Geodetic latitude (C{degrees}).
733
+ @arg lon: Geodetic longitude (C{degrees}).
734
+ @arg height: Height above the ellipsoid (C{meter}, same units
735
+ as this triaxial's C{a}, C{b} and C{c} axes).
736
+ @kwarg name: Optional name (C{str}).
737
+
738
+ @return: A L{Vector3Tuple}C{(x, y, z)}.
739
+
740
+ @see: Method L{Triaxial.reverseLatLon} and U{Expressions (9-11)<https://
741
+ www.Topo.Auth.GR/wp-content/uploads/sites/111/2021/12/09_Panou.pdf>}.
742
+ '''
743
+ return self._forwardLatLon3(height, name, *sincos2d_(lat, lon))
744
+
745
+ def forwardLatLon_(self, slat, clat, slon, clon, height=0, name=NN):
746
+ '''Convert I{geodetic} lat-, longitude and heigth to cartesian.
747
+
748
+ @arg slat: Geodetic latitude C{sin(lat)} (C{scalar}).
749
+ @arg clat: Geodetic latitude C{cos(lat)} (C{scalar}).
750
+ @arg slon: Geodetic longitude C{sin(lon)} (C{scalar}).
751
+ @arg clon: Geodetic longitude C{cos(lon)} (C{scalar}).
752
+ @arg height: Height above the ellipsoid (C{meter}, same units
753
+ as this triaxial's axes C{a}, C{b} and C{c}).
754
+ @kwarg name: Optional name (C{str}).
755
+
756
+ @return: A L{Vector3Tuple}C{(x, y, z)}.
757
+
758
+ @see: Method L{Triaxial.reverseLatLon} and U{Expressions (9-11)<https://
759
+ www.Topo.Auth.GR/wp-content/uploads/sites/111/2021/12/09_Panou.pdf>}.
760
+ '''
761
+ sa, ca = self._norm2(slat, clat)
762
+ sb, cb = self._norm2(slon, clon)
763
+ return self._forwardLatLon3(height, name, sa, ca, sb, cb)
764
+
765
+ def _forwardLatLon3(self, height, name, sa, ca, sb, cb):
766
+ '''(INTERNAL) Helper for C{.forwardLatLon} and C{.forwardLatLon_}.
767
+ '''
768
+ ca_x_sb = ca * sb
769
+ h = self._Height(height)
770
+ # 1 - (1 - (c/a)**2) * sa**2 - (1 - (b/a)**2) * ca**2 * sb**2
771
+ t = fsumf_(_1_0, -self.e2ac * sa**2, -self.e2ab * ca_x_sb**2)
772
+ n = self.a / sqrt0(t, Error=TriaxialError) # prime vertical
773
+ x = (h + n) * ca * cb
774
+ y = (h + n * self._1e2ab) * ca_x_sb
775
+ z = (h + n * self._1e2ac) * sa
776
+ return Vector3Tuple(x, y, z, name=name)
777
+
778
+ def _Height(self, height):
779
+ '''(INTERNAL) Validate a C{height}.
780
+ '''
781
+ return Height_(height=height, low=-self.c, Error=TriaxialError)
782
+
783
+ @Property_RO
784
+ def _k2_kp2(self):
785
+ '''(INTERNAL) Get C{k2} and C{kp2} for C{._xE}, C{._yE} and C{.area}.
786
+ '''
787
+ # k2 = a2b2 / a2c2 * c2_b2
788
+ # kp2 = b2c2 / a2c2 * a2_b2
789
+ # b2 = b**2
790
+ # xE = Elliptic(k2, -a2b2 / b2, kp2, a2_b2)
791
+ # yE = Elliptic(kp2, +b2c2 / b2, k2, c2_b2)
792
+ # aE = Elliptic(kp2, 0, k2, 1)
793
+ return (self._a2b2_a2c2 * self._c2_b2,
794
+ self._b2c2 / self._a2c2 * self._a2_b2)
795
+
796
+ def _radialTo3(self, sbeta, cbeta, somega, comega):
797
+ '''(INTERNAL) Convert I{ellipsoidal} lat- and longitude C{beta} and
798
+ C{omega} to cartesian coordinates I{on the triaxial's surface},
799
+ also I{ordered} helper for C{.height4}.
800
+ '''
801
+ sa, ca = self._norm2(sbeta, cbeta)
802
+ sb, cb = self._norm2(somega, comega)
803
+
804
+ b2_a2 = self._1e2ab # == (b/a)**2
805
+ c2_a2 = -self._1e2ac # == -(c/a)**2
806
+ a2c2_a2 = self. e2ac # (a**2 - c**2) / a**2 == 1 - (c/a)**2
807
+
808
+ x2 = Fsum(_1_0, -b2_a2 * sa**2, c2_a2 * ca**2).fover(a2c2_a2)
809
+ z2 = Fsum(c2_a2, sb**2, b2_a2 * cb**2).fover(a2c2_a2)
810
+
811
+ x, y, z = self._abc3
812
+ x *= cb * sqrt0(x2, Error=TriaxialError)
813
+ y *= ca * sb
814
+ z *= sa * sqrt0(z2, Error=TriaxialError)
815
+ return x, y, z
816
+
817
+ def reverseBetaOmega(self, x_xyz, y=None, z=None, name=NN):
818
+ '''Convert cartesian to I{ellipsoidal} lat- and longitude, C{beta}, C{omega}
819
+ and height.
820
+
821
+ @arg x_xyz: X component (C{scalar}) or a cartesian (C{Cartesian},
822
+ L{Ecef9Tuple}, L{Vector3d}, L{Vector3Tuple} or L{Vector4Tuple}).
823
+ @kwarg y: Y component (C{scalar}), required if B{C{x_xyz}} if C{scalar}.
824
+ @kwarg z: Z component (C{scalar}), required if B{C{x_xyz}} if C{scalar}.
825
+ @kwarg name: Optional name (C{str}).
826
+
827
+ @return: A L{BetaOmega3Tuple}C{(beta, omega, height)} with C{beta} and
828
+ C{omega} in L{Radians} and (radial) C{height} in C{meter}, same
829
+ units as this triaxial's axes.
830
+
831
+ @see: Methods L{Triaxial.forwardBetaOmega} and L{Triaxial.forwardBetaOmega_}
832
+ and U{Expressions (21-22)<https://www.Topo.Auth.GR/wp-content/uploads/
833
+ sites/111/2021/12/09_Panou.pdf>}.
834
+ '''
835
+ v = _otherV3d_(x_xyz, y, z)
836
+ a, b, h = self._reverseLatLon3(v, atan2, v, self.forwardBetaOmega_)
837
+ return BetaOmega3Tuple(Radians(beta=a), Radians(omega=b), h, name=name)
838
+
839
+ def reverseCartesian(self, x_xyz, y=None, z=None, h=0, normal=True, eps=_EPS2e4, name=NN):
840
+ '''"Unproject" a cartesian on to a cartesion I{off} this triaxial's surface.
841
+
842
+ @arg x_xyz: X component (C{scalar}) or a cartesian (C{Cartesian},
843
+ L{Ecef9Tuple}, L{Vector3d}, L{Vector3Tuple} or L{Vector4Tuple}).
844
+ @kwarg y: Y component (C{scalar}), required if B{C{x_xyz}} if C{scalar}.
845
+ @kwarg z: Z component (C{scalar}), required if B{C{x_xyz}} if C{scalar}.
846
+ @arg h: Height above or below this triaxial's surface (C{meter}, same units
847
+ as the axes).
848
+ @kwarg normal: If C{True} the height is C{normal} to the surface, otherwise
849
+ C{radially} to the center of this triaxial (C{bool}).
850
+ @kwarg eps: Tolerance for surface test (C{scalar}).
851
+ @kwarg name: Optional name (C{str}).
852
+
853
+ @return: A L{Vector3Tuple}C{(x, y, z)}.
854
+
855
+ @raise TrialError: Cartesian C{(x, y, z)} not on this triaxial's surface.
856
+
857
+ @see: Methods L{Triaxial.forwardCartesian} and L{Triaxial.height4}.
858
+ '''
859
+ v = _otherV3d_(x_xyz, y, z, name=name)
860
+ s = _sideOf(v.xyz, self._abc3, eps=eps)
861
+ if s: # PYCHOK no cover
862
+ t = _SPACE_((_inside_ if s < 0 else _outside_), self.toRepr())
863
+ raise TriaxialError(eps=eps, sideOf=s, x=v.x, y=v.y, z=v.z, txt=t)
864
+
865
+ if h:
866
+ if normal:
867
+ v = v.plus(self.normal3d(*v.xyz, length=h))
868
+ elif v.length > EPS0:
869
+ v = v.times(_1_0 + (h / v.length))
870
+ return v.xyz # Vector3Tuple
871
+
872
+ def reverseLatLon(self, x_xyz, y=None, z=None, name=NN):
873
+ '''Convert cartesian to I{geodetic} lat-, longitude and height.
874
+
875
+ @arg x_xyz: X component (C{scalar}) or a cartesian (C{Cartesian},
876
+ L{Ecef9Tuple}, L{Vector3d}, L{Vector3Tuple} or L{Vector4Tuple}).
877
+ @kwarg y: Y component (C{scalar}), required if B{C{x_xyz}} if C{scalar}.
878
+ @kwarg z: Z component (C{scalar}), required if B{C{x_xyz}} if C{scalar}.
879
+ @kwarg name: Optional name (C{str}).
880
+
881
+ @return: A L{LatLon3Tuple}C{(lat, lon, height)} with C{lat} and C{lon}
882
+ in C{degrees} and (radial) C{height} in C{meter}, same units
883
+ as this triaxial's axes.
884
+
885
+ @see: Methods L{Triaxial.forwardLatLon} and L{Triaxial.forwardLatLon_}
886
+ and U{Expressions (4-5)<https://www.Topo.Auth.GR/wp-content/uploads/
887
+ sites/111/2021/12/09_Panou.pdf>}.
888
+ '''
889
+ v = _otherV3d_(x_xyz, y, z)
890
+ s = v.times_(self._1e2ac, # == 1 - e_sub_x**2
891
+ self._1e2bc, # == 1 - e_sub_y**2
892
+ _1_0)
893
+ t = self._reverseLatLon3(s, atan2d, v, self.forwardLatLon_)
894
+ return LatLon3Tuple(*t, name=name)
895
+
896
+ def _reverseLatLon3(self, s, atan2_, v, forward_):
897
+ '''(INTERNAL) Helper for C{.reverseBetOmg} and C{.reverseLatLon}.
898
+ '''
899
+ x, y, z = s.xyz
900
+ d = hypot( x, y)
901
+ a = atan2_(z, d)
902
+ b = atan2_(y, x)
903
+ h = v.minus_(*forward_(z, d, y, x)).length
904
+ return a, b, h
905
+
906
+
907
+ class JacobiConformal(Triaxial):
908
+ '''This is a conformal projection of a triaxial ellipsoid to a plane in which the
909
+ C{X} and C{Y} grid lines are straight.
910
+
911
+ Ellipsoidal coordinates I{beta} and I{omega} are converted to Jacobi Conformal
912
+ I{y} respectively I{x} separately. Jacobi's coordinates have been multiplied
913
+ by C{sqrt(B{a}**2 - B{c}**2) / (2 * B{b})} so that the customary results are
914
+ returned in the case of an ellipsoid of revolution.
915
+
916
+ Copyright (C) U{Charles Karney<mailto:Karney@Alum.MIT.edu>} (2014-2023) and
917
+ licensed under the MIT/X11 License.
918
+
919
+ @note: This constructor can I{not be used to specify a sphere}, see alternate
920
+ L{JacobiConformalSpherical}.
921
+
922
+ @see: L{Triaxial}, C++ class U{JacobiConformal<https://GeographicLib.SourceForge.io/
923
+ C++/doc/classGeographicLib_1_1JacobiConformal.html#details>}, U{Jacobi's conformal
924
+ projection<https://GeographicLib.SourceForge.io/C++/doc/jacobi.html>} and Jacobi,
925
+ C. G. J. I{U{Vorlesungen über Dynamik<https://Books.Google.com/books?
926
+ id=ryEOAAAAQAAJ&pg=PA212>}}, page 212ff.
927
+ '''
928
+
929
+ @Property_RO
930
+ def _xE(self):
931
+ '''(INTERNAL) Get the x-elliptic function.
932
+ '''
933
+ k2, kp2 = self._k2_kp2
934
+ # -a2b2 / b2 == (b2 - a2) / b2 == 1 - a2 / b2 == 1 - a2_b2
935
+ return self._Elliptic(k2, _1_0 - self._a2_b2, kp2, self._a2_b2)
936
+
937
+ def xR(self, omega):
938
+ '''Compute a Jacobi Conformal C{x} projection.
939
+
940
+ @arg omega: Ellipsoidal longitude (C{radians} or L{Degrees}).
941
+
942
+ @return: The C{x} projection (L{Radians}).
943
+ '''
944
+ return self.xR_(*SinCos2(omega))
945
+
946
+ def xR_(self, somega, comega):
947
+ '''Compute a Jacobi Conformal C{x} projection.
948
+
949
+ @arg somega: Ellipsoidal longitude C{sin(omega)} (C{scalar}).
950
+ @arg comega: Ellipsoidal longitude C{cos(omega)} (C{scalar}).
951
+
952
+ @return: The C{x} projection (L{Radians}).
953
+ '''
954
+ s, c = self._norm2(somega, comega, self.a)
955
+ return Radians(x=self._xE.fPi(s, c) * self._a2_b2)
956
+
957
+ @Property_RO
958
+ def xyQ2(self):
959
+ '''Get the Jacobi Conformal quadrant size (L{Jacobi2Tuple}C{(x, y)}).
960
+ '''
961
+ return Jacobi2Tuple(Radians(x=self._a2_b2 * self._xE.cPi),
962
+ Radians(y=self._c2_b2 * self._yE.cPi),
963
+ name=JacobiConformal.xyQ2.name)
964
+
965
+ def xyR2(self, beta, omega, name=NN):
966
+ '''Compute a Jacobi Conformal C{x} and C{y} projection.
967
+
968
+ @arg beta: Ellipsoidal latitude (C{radians} or L{Degrees}).
969
+ @arg omega: Ellipsoidal longitude (C{radians} or L{Degrees}).
970
+ @kwarg name: Optional name (C{str}).
971
+
972
+ @return: A L{Jacobi2Tuple}C{(x, y)}.
973
+ '''
974
+ return self.xyR2_(*(SinCos2(beta) + SinCos2(omega)),
975
+ name=name or self.xyR2.__name__)
976
+
977
+ def xyR2_(self, sbeta, cbeta, somega, comega, name=NN):
978
+ '''Compute a Jacobi Conformal C{x} and C{y} projection.
979
+
980
+ @arg sbeta: Ellipsoidal latitude C{sin(beta)} (C{scalar}).
981
+ @arg cbeta: Ellipsoidal latitude C{cos(beta)} (C{scalar}).
982
+ @arg somega: Ellipsoidal longitude C{sin(omega)} (C{scalar}).
983
+ @arg comega: Ellipsoidal longitude C{cos(omega)} (C{scalar}).
984
+ @kwarg name: Optional name (C{str}).
985
+
986
+ @return: A L{Jacobi2Tuple}C{(x, y)}.
987
+ '''
988
+ return Jacobi2Tuple(self.xR_(somega, comega),
989
+ self.yR_(sbeta, cbeta),
990
+ name=name or self.xyR2_.__name__)
991
+
992
+ @Property_RO
993
+ def _yE(self):
994
+ '''(INTERNAL) Get the x-elliptic function.
995
+ '''
996
+ kp2, k2 = self._k2_kp2 # swapped!
997
+ # b2c2 / b2 == (b2 - c2) / b2 == 1 - c2 / b2 == e2bc
998
+ return self._Elliptic(k2, self.e2bc, kp2, self._c2_b2)
999
+
1000
+ def yR(self, beta):
1001
+ '''Compute a Jacobi Conformal C{y} projection.
1002
+
1003
+ @arg beta: Ellipsoidal latitude (C{radians} or L{Degrees}).
1004
+
1005
+ @return: The C{y} projection (L{Radians}).
1006
+ '''
1007
+ return self.yR_(*SinCos2(beta))
1008
+
1009
+ def yR_(self, sbeta, cbeta):
1010
+ '''Compute a Jacobi Conformal C{y} projection.
1011
+
1012
+ @arg sbeta: Ellipsoidal latitude C{sin(beta)} (C{scalar}).
1013
+ @arg cbeta: Ellipsoidal latitude C{cos(beta)} (C{scalar}).
1014
+
1015
+ @return: The C{y} projection (L{Radians}).
1016
+ '''
1017
+ s, c = self._norm2(sbeta, cbeta, self.c)
1018
+ return Radians(y=self._yE.fPi(s, c) * self._c2_b2)
1019
+
1020
+
1021
+ class JacobiConformalSpherical(JacobiConformal):
1022
+ '''An alternate, I{spherical} L{JacobiConformal} projection.
1023
+
1024
+ @see: L{JacobiConformal} for other and more details.
1025
+ '''
1026
+ _ab = _bc = 0
1027
+
1028
+ def __init__(self, radius_triaxial, ab=0, bc=0, name=NN):
1029
+ '''New L{JacobiConformalSpherical}.
1030
+
1031
+ @arg radius_triaxial: Radius (C{scalar}, conventionally in
1032
+ C{meter}) or an other L{JacobiConformalSpherical},
1033
+ L{JacobiConformal} or ordered L{Triaxial}.
1034
+ @kwarg ab: Relative magnitude of C{B{a} - B{b}} (C{meter},
1035
+ same units as C{scalar B{radius}}.
1036
+ @kwarg bc: Relative magnitude of C{B{b} - B{c}} (C{meter},
1037
+ same units as C{scalar B{radius}}.
1038
+ @kwarg name: Optional name (C{str}).
1039
+
1040
+ @raise TriaxialError: Invalid B{C{radius_triaxial}}, negative
1041
+ B{C{ab}}, negative B{C{bc}} or C{(B{ab}
1042
+ + B{bc})} not positive.
1043
+
1044
+ @note: If B{C{radius_triaxial}} is a L{JacobiConformalSpherical}
1045
+ and if B{C{ab}} and B{C{bc}} are both zero or C{None},
1046
+ the B{C{radius_triaxial}}'s C{ab}, C{bc}, C{a}, C{b}
1047
+ and C{c} are copied.
1048
+ '''
1049
+ try:
1050
+ r = radius_triaxial
1051
+ if isinstance(r, Triaxial): # ordered only
1052
+ t = r._abc3
1053
+ j = isinstance(r, JacobiConformalSpherical) and not bool(ab or bc)
1054
+ else:
1055
+ t = (Radius(radius=r),) * 3
1056
+ j = False
1057
+ self._ab = r.ab if j else Scalar_(ab=ab) # low=0
1058
+ self._bc = r.bc if j else Scalar_(bc=bc) # low=0
1059
+ if (self.ab + self.bc) <= 0:
1060
+ raise ValueError(_negative_)
1061
+ a, _, c = self._abc3 = t
1062
+ if not (a >= c and isfinite(self._a2b2)
1063
+ and isfinite(self._a2c2)):
1064
+ raise ValueError(_not_(_finite_))
1065
+ except (TypeError, ValueError) as x:
1066
+ raise TriaxialError(radius_triaxial=r, ab=ab, bc=bc, cause=x)
1067
+ if name:
1068
+ self.name = name
1069
+
1070
+ @Property_RO
1071
+ def ab(self):
1072
+ '''Get relative magnitude C{ab} (C{meter}, same units as B{C{a}}).
1073
+ '''
1074
+ return self._ab
1075
+
1076
+ @Property_RO
1077
+ def _a2b2(self):
1078
+ '''(INTERNAL) Get C{a**2 - b**2} == ab * (a + b).
1079
+ '''
1080
+ a, b, _ = self._abc3
1081
+ return self.ab * (a + b)
1082
+
1083
+ @Property_RO
1084
+ def _a2c2(self):
1085
+ '''(INTERNAL) Get C{a**2 - c**2} == a2b2 + b2c2.
1086
+ '''
1087
+ return self._a2b2 + self._b2c2
1088
+
1089
+ @Property_RO
1090
+ def bc(self):
1091
+ '''Get relative magnitude C{bc} (C{meter}, same units as B{C{a}}).
1092
+ '''
1093
+ return self._bc
1094
+
1095
+ @Property_RO
1096
+ def _b2c2(self):
1097
+ '''(INTERNAL) Get C{b**2 - c**2} == bc * (b + c).
1098
+ '''
1099
+ _, b, c = self._abc3
1100
+ return self.bc * (b + c)
1101
+
1102
+ @Property_RO
1103
+ def radius(self):
1104
+ '''Get radius (C{meter}, conventionally).
1105
+ '''
1106
+ return self.a
1107
+
1108
+
1109
+ class TriaxialError(_ValueError):
1110
+ '''Raised for L{Triaxial} issues.
1111
+ '''
1112
+ pass # ...
1113
+
1114
+
1115
+ class Triaxials(_NamedEnum):
1116
+ '''(INTERNAL) L{Triaxial} registry, I{must} be a sub-class
1117
+ to accommodate the L{_LazyNamedEnumItem} properties.
1118
+ '''
1119
+ def _Lazy(self, *abc, **name):
1120
+ '''(INTERNAL) Instantiate the C{Triaxial}.
1121
+ '''
1122
+ a, b, c = map(km2m, abc)
1123
+ return Triaxial(a, b, c, **name)
1124
+
1125
+ Triaxials = Triaxials(Triaxial, Triaxial_) # PYCHOK singleton
1126
+ '''Some pre-defined L{Triaxial}s, all I{lazily} instantiated.'''
1127
+ # <https://ArxIV.org/pdf/1909.06452.pdf> Table 1 Semi-axes in Km
1128
+ # <https://www.JPS.NASA.gov/education/images/pdf/ss-moons.pdf>
1129
+ # <https://link.Springer.com/article/10.1007/s00190-022-01650-9>
1130
+ _EWGS84_35 = _EWGS84.a + 35, _EWGS84.a - 35, _EWGS84.b
1131
+ Triaxials._assert( # a (Km) b (Km) c (Km) planet
1132
+ Amalthea = _lazy('Amalthea', 125.0, 73.0, 64), # Jupiter
1133
+ Ariel = _lazy('Ariel', 581.1, 577.9, 577.7), # Uranus
1134
+ Earth = _lazy('Earth', 6378.173435, 6378.1039, 6356.7544),
1135
+ Enceladus = _lazy('Enceladus', 256.6, 251.4, 248.3), # Saturn
1136
+ Europa = _lazy('Europa', 1564.13, 1561.23, 1560.93), # Jupiter
1137
+ Io = _lazy('Io', 1829.4, 1819.3, 1815.7), # Jupiter
1138
+ Mars = _lazy('Mars', 3394.6, 3393.3, 3376.3),
1139
+ Mimas = _lazy('Mimas', 207.4, 196.8, 190.6), # Saturn
1140
+ Miranda = _lazy('Miranda', 240.4, 234.2, 232.9), # Uranus
1141
+ Moon = _lazy('Moon', 1735.55, 1735.324, 1734.898), # Earth
1142
+ Tethys = _lazy('Tethys', 535.6, 528.2, 525.8), # Saturn
1143
+ WGS84_35 = _lazy('WGS84_35', *map2(m2km, _EWGS84_35)))
1144
+ del _EWGS84_35
1145
+
1146
+
1147
+ def _getitems(items, *indices):
1148
+ '''(INTERNAL) Get the C{items} at the given I{indices}.
1149
+
1150
+ @return: C{Type(items[i] for i in indices)} with
1151
+ C{Type = type(items)}, any C{type} having
1152
+ the special method C{__getitem__}.
1153
+ '''
1154
+ return type(items)(map(items.__getitem__, indices))
1155
+
1156
+
1157
+ def _hartzell3(pov, los, Tun): # in .ellipsoids.hartzell4, .formy.hartzell
1158
+ '''(INTERNAL) Hartzell's "Satellite Line-of-Sight Intersection ...",
1159
+ formula from a Point-Of-View to an I{un-/ordered} Triaxial.
1160
+ '''
1161
+ def _toUvwV3d(los, pov):
1162
+ try: # pov must be LatLon or Cartesian if los is a Los
1163
+ v = los.toUvw(pov)
1164
+ except (AttributeError, TypeError):
1165
+ v = _otherV3d(los=los)
1166
+ return v
1167
+
1168
+ p3 = _otherV3d(pov=pov.toCartesian() if isLatLon(pov) else pov)
1169
+ if los is True: # normal
1170
+ a, b, c, d, i = _normalTo5(p3.x, p3.y, p3.z, Tun)
1171
+ return type(p3)(a, b, c), d, i
1172
+
1173
+ u3 = p3.negate() if los is False or los is None else _toUvwV3d(los, pov)
1174
+
1175
+ a, b, c, T = Tun._ordered4
1176
+
1177
+ a2 = a**2 # largest, factored out
1178
+ b2, p2 = (b**2, T._1e2ab) if b != a else (a2, _1_0)
1179
+ c2, q2 = (c**2, T._1e2ac) if c != a else (a2, _1_0)
1180
+
1181
+ p3 = T._order3d(p3)
1182
+ u3 = T._order3d(u3).unit() # unit vector, opposing signs
1183
+
1184
+ x2, y2, z2 = p3.x2y2z2 # p3.times_(p3).xyz
1185
+ ux, vy, wz = u3.times_(p3).xyz
1186
+ u2, v2, w2 = u3.x2y2z2 # u3.times_(u3).xyz
1187
+
1188
+ t = (p2 * c2), c2, b2
1189
+ m = fdot(t, u2, v2, w2) # a2 factored out
1190
+ if m < EPS0: # zero or near-null LOS vector
1191
+ raise _ValueError(_near_(_null_))
1192
+
1193
+ r = fsumf_(b2 * w2, c2 * v2, -v2 * z2, vy * wz * 2,
1194
+ -w2 * y2, -u2 * y2 * q2, -u2 * z2 * p2, ux * wz * 2 * p2,
1195
+ -w2 * x2 * p2, b2 * u2 * q2, -v2 * x2 * q2, ux * vy * 2 * q2)
1196
+ if r > 0: # a2 factored out
1197
+ r = sqrt(r) * b * c # == a * a * b * c / a2
1198
+ elif r < 0: # LOS pointing away from or missing the triaxial
1199
+ raise _ValueError(_opposite_ if max(ux, vy, wz) > 0 else _outside_)
1200
+
1201
+ d = Fdot(t, ux, vy, wz).fadd_(r).fover(m) # -r for antipode, a2 factored out
1202
+ if d > 0: # POV inside or LOS outside or missing the triaxial
1203
+ s = fsumf_(_N_1_0, x2 / a2, y2 / b2, z2 / c2) # like _sideOf
1204
+ raise _ValueError(_outside_ if s > 0 else _inside_)
1205
+ elif fsum1f_(x2, y2, z2) < d**2: # d past triaxial's center
1206
+ raise _ValueError(_too_(_distant_))
1207
+
1208
+ v = p3.minus(u3.times(d)) # cartesian type(pov) or Vector3d
1209
+ h = p3.minus(v).length # distance to pov == -d
1210
+ return T._order3d(v, reverse=True), h, None
1211
+
1212
+
1213
+ def hartzell4(pov, los=False, tri_biax=_WGS84, name=NN):
1214
+ '''Compute the intersection of a tri-/biaxial ellipsoid and a Line-Of-Sight
1215
+ from a Point-Of-View outside.
1216
+
1217
+ @arg pov: Point-Of-View outside the tri-/biaxial (C{Cartesian}, L{Ecef9Tuple}
1218
+ C{LatLon} or L{Vector3d}).
1219
+ @kwarg los: Line-Of-Sight, I{direction} to the tri-/biaxial (L{Los}, L{Vector3d}),
1220
+ C{True} for the I{normal, plumb} onto the surface or C{False} or
1221
+ C{None} to point to the center of the tri-/biaxial.
1222
+ @kwarg tri_biax: A triaxial (L{Triaxial}, L{Triaxial_}, L{JacobiConformal} or
1223
+ L{JacobiConformalSpherical}) or biaxial ellipsoid (L{Datum},
1224
+ L{Ellipsoid}, L{Ellipsoid2}, L{a_f2Tuple} or C{scalar} radius,
1225
+ conventionally in C{meter}).
1226
+ @kwarg name: Optional name (C{str}).
1227
+
1228
+ @return: L{Vector4Tuple}C{(x, y, z, h)} on the tri-/biaxial's surface, with C{h}
1229
+ the distance from B{C{pov}} to C{(x, y, z)} I{along the} B{C{los}}, all
1230
+ in C{meter}, conventionally.
1231
+
1232
+ @raise TriaxialError: Invalid B{C{pov}} or B{C{pov}} inside the tri-/biaxial or
1233
+ invalid B{C{los}} or B{C{los}} points outside or away from
1234
+ the tri-/biaxial.
1235
+
1236
+ @raise TypeError: Invalid B{C{tri_biax}}, C{ellipsoid} or C{datum}.
1237
+
1238
+ @see: Class L{pygeodesy3.Los}, functions L{pygeodesy.tyr3d} and L{pygeodesy.hartzell}
1239
+ and U{lookAtSpheroid<https://PyPI.org/project/pymap3d>} and U{"Satellite
1240
+ Line-of-Sight Intersection with Earth"<https://StephenHartzell.Medium.com/
1241
+ satellite-line-of-sight-intersection-with-earth-d786b4a6a9b6>}.
1242
+ '''
1243
+ n = hartzell4.__name__
1244
+ if isinstance(tri_biax, Triaxial_):
1245
+ T = tri_biax
1246
+ else:
1247
+ D = tri_biax if isinstance(tri_biax, Datum) else \
1248
+ _spherical_datum(tri_biax, name=n)
1249
+ T = D.ellipsoid._triaxial
1250
+ try:
1251
+ v, h, i = _hartzell3(pov, los, T)
1252
+ except Exception as x:
1253
+ raise TriaxialError(pov=pov, los=los, tri_biax=tri_biax, cause=x)
1254
+ return Vector4Tuple(v.x, v.y, v.z, h, iteration=i, name=name or n)
1255
+
1256
+
1257
+ def _hypot21(x, y, z=0):
1258
+ '''(INTERNAL) Compute M{x**2 + y**2 + z**2 - 1} with C{max(fabs(x),
1259
+ fabs(y), fabs(z))} rarely greater than 1.0.
1260
+ '''
1261
+ return fsumf_(_1_0, x**2, y**2, (z**2 if z else _0_0), _N_2_0)
1262
+
1263
+
1264
+ def _normalTo4(x, y, a, b, eps=EPS):
1265
+ '''(INTERNAL) Nearest point on and distance to a 2-D ellipse, I{unordered}.
1266
+
1267
+ @see: Function C{pygeodesy.ellipsoids._normalTo3} and I{Eberly}'s U{Distance
1268
+ from a Point to ... an Ellipse ...<https://www.GeometricTools.com/
1269
+ Documentation/DistancePointEllipseEllipsoid.pdf>}.
1270
+ '''
1271
+ if b > a:
1272
+ b, a, d, i = _normalTo4(y, x, b, a, eps=eps)
1273
+ return a, b, d, i
1274
+
1275
+ if not (b > 0 and isfinite(a)):
1276
+ raise _ValueError(a=a, b=b)
1277
+
1278
+ i, _a = None, fabs
1279
+ if y:
1280
+ if x:
1281
+ u = _a(x / a)
1282
+ v = _a(y / b)
1283
+ g = _hypot21(u, v)
1284
+ if _a(g) < EPS02: # on the ellipse
1285
+ a, b, d = x, y, _0_0
1286
+ else:
1287
+ r = (b / a)**2
1288
+ t, i = _rootXd(_1_0 / r, 0, u, 0, v, g, eps)
1289
+ a = x / (t * r + _1_0)
1290
+ b = y / (t + _1_0)
1291
+ d = hypot(x - a, y - b)
1292
+ else: # x == 0
1293
+ if y < 0:
1294
+ b = -b
1295
+ a, d = x, _a(y - b)
1296
+
1297
+ else: # y == 0
1298
+ n = a * x
1299
+ d = (a + b) * (a - b)
1300
+ if d > _a(n): # PYCHOK no cover
1301
+ r = n / d
1302
+ a *= r
1303
+ r = _1_0 - r**2
1304
+ if r > EPS02:
1305
+ b *= sqrt(r)
1306
+ d = hypot(x - a, b)
1307
+ else:
1308
+ b = _0_0
1309
+ d = _a(x - a)
1310
+ else:
1311
+ if x < 0:
1312
+ a = -a
1313
+ b, d = y, _a(x - a)
1314
+ return a, b, d, i
1315
+
1316
+
1317
+ def _normalTo5(x, y, z, Tun, eps=EPS): # MCCABE 19
1318
+ '''(INTERNAL) Nearest point on and distance to an I{un-/ordered} triaxial.
1319
+
1320
+ @see: I{Eberly}'s U{Distance from a Point to ... an Ellipsoid ...<https://
1321
+ www.GeometricTools.com/Documentation/DistancePointEllipseEllipsoid.pdf>}.
1322
+ '''
1323
+ a, b, c, T = Tun._ordered4
1324
+ if Tun is not T: # T is ordered, Tun isn't
1325
+ t = T._order3(x, y, z) + (T,)
1326
+ a, b, c, d, i = _normalTo5(*t, eps=eps)
1327
+ return T._order3(a, b, c, reverse=True) + (d, i)
1328
+
1329
+ if not (c > 0 and isfinite(a)):
1330
+ raise _ValueError(a=a, b=b, c=c)
1331
+
1332
+ if eps > 0:
1333
+ val = max(eps * 1e8, EPS)
1334
+ else: # no validation
1335
+ val, eps = 0, max(EPS0, -eps)
1336
+
1337
+ i, _a = None, fabs
1338
+ if z:
1339
+ if y:
1340
+ if x:
1341
+ u = _a(x / a)
1342
+ v = _a(y / b)
1343
+ w = _a(z / c)
1344
+ g = _hypot21(u, v, w)
1345
+ if _a(g) < EPS02: # on the ellipsoid
1346
+ a, b, c, d = x, y, z, _0_0
1347
+ else:
1348
+ r = T._1e2ac # (c / a)**2
1349
+ s = T._1e2bc # (c / b)**2
1350
+ t, i = _rootXd(_1_0 / r, _1_0 / s, u, v, w, g, eps)
1351
+ a = x / (t * r + _1_0)
1352
+ b = y / (t * s + _1_0)
1353
+ c = z / (t + _1_0)
1354
+ d = hypot_(x - a, y - b, z - c)
1355
+ else: # x == 0
1356
+ a = x # 0
1357
+ b, c, d, i = _normalTo4(y, z, b, c, eps=eps)
1358
+ elif x: # y == 0
1359
+ b = y # 0
1360
+ a, c, d, i = _normalTo4(x, z, a, c, eps=eps)
1361
+ else: # x == y == 0
1362
+ if z < 0:
1363
+ c = -c
1364
+ a, b, d = x, y, _a(z - c)
1365
+
1366
+ else: # z == 0
1367
+ t = True
1368
+ d = T._a2c2 # (a + c) * (a - c)
1369
+ n = a * x
1370
+ if d > _a(n):
1371
+ u = n / d
1372
+ d = T._b2c2 # (b + c) * (b - c)
1373
+ n = b * y
1374
+ if d > _a(n):
1375
+ v = n / d
1376
+ n = _hypot21(u, v)
1377
+ if n < 0:
1378
+ a *= u
1379
+ b *= v
1380
+ c *= sqrt(-n)
1381
+ d = hypot_(x - a, y - b, c)
1382
+ t = False
1383
+ if t:
1384
+ c = z # signed-0
1385
+ a, b, d, i = _normalTo4(x, y, a, b, eps=eps)
1386
+
1387
+ if val > 0: # validate
1388
+ e = T.sideOf(a, b, c, eps=val)
1389
+ if e: # not near the ellipsoid's surface
1390
+ raise _ValueError(a=a, b=b, c=c, d=d,
1391
+ sideOf=e, eps=val)
1392
+ if d: # angle of delta and normal vector
1393
+ m = Vector3d(x, y, z).minus_(a, b, c)
1394
+ if m.euclid > val:
1395
+ m = m.unit()
1396
+ n = T.normal3d(a, b, c)
1397
+ e = n.dot(m) # n.negate().dot(m)
1398
+ if not isnear1(_a(e), eps1=val):
1399
+ raise _ValueError(n=n, m=m,
1400
+ dot=e, eps=val)
1401
+ return a, b, c, d, i
1402
+
1403
+
1404
+ def _otherV3d_(x_xyz, y, z, **name):
1405
+ '''(INTERNAL) Get a Vector3d from C{x_xyz}, C{y} and C{z}.
1406
+ '''
1407
+ return Vector3d(x_xyz, y, z, **name) if isscalar(x_xyz) else \
1408
+ _otherV3d(x_xyz=x_xyz)
1409
+
1410
+
1411
+ def _rootXd(r, s, u, v, w, g, eps):
1412
+ '''(INTERNAL) Robust 2d- or 3d-root finder: 2d- if C{s == v == 0} else 3d-root.
1413
+
1414
+ @see: I{Eberly}'s U{Robust Root Finders ...<https://www.GeometricTools.com/
1415
+ Documentation/DistancePointEllipseEllipsoid.pdf>}.
1416
+ '''
1417
+ _1, __2 = _1_0, _0_5
1418
+ _a, _h21 = fabs, _hypot21
1419
+
1420
+ u *= r
1421
+ v *= s # 0 for 2d-root
1422
+ t0 = w - _1
1423
+ t1 = _0_0 if g < 0 else (hypot_(u, w, v) - _1)
1424
+ # assert t0 <= t1
1425
+ for i in range(1, _TRIPS): # 48-55
1426
+ e = _a(t0 - t1)
1427
+ if e < eps:
1428
+ break
1429
+ t = (t0 + t1) * __2
1430
+ if t in (t0, t1):
1431
+ break
1432
+ g = _h21(u / (t + r), w / (t + _1),
1433
+ (v / (t + s)) if v else 0)
1434
+ if g > 0:
1435
+ t0 = t
1436
+ elif g < 0:
1437
+ t1 = t
1438
+ else:
1439
+ break
1440
+ else: # PYCHOK no cover
1441
+ t = Fmt.no_convergence(e, eps)
1442
+ raise _ValueError(t, txt=_rootXd.__name__)
1443
+ return t, i
1444
+
1445
+
1446
+ def _sideOf(xyz, abc, eps=EPS):
1447
+ '''(INTERNAL) Helper for C{_hartzell3}, M{.sideOf} and M{.reverseCartesian}.
1448
+
1449
+ @return: M{sum((x / a)**2 for x, a in zip(xyz, abc)) - 1} or C{INT0}.
1450
+ '''
1451
+ s = fsumf_(_N_1_0, *((x / a)**2 for x, a in _zip(xyz, abc) if a)) # strict=True
1452
+ return INT0 if fabs(s) < eps else s
1453
+
1454
+
1455
+ if __name__ == '__main__':
1456
+
1457
+ from pygeodesy import printf
1458
+ from pygeodesy.interns import _COMMA_, _NL_, _NLATvar_
1459
+
1460
+ # __doc__ of this file, force all into registery
1461
+ t = [NN] + Triaxials.toRepr(all=True, asorted=True).split(_NL_)
1462
+ printf(_NLATvar_.join(i.strip(_COMMA_) for i in t))
1463
+
1464
+ # **) MIT License
1465
+ #
1466
+ # Copyright (C) 2022-2024 -- mrJean1 at Gmail -- All Rights Reserved.
1467
+ #
1468
+ # Permission is hereby granted, free of charge, to any person obtaining a
1469
+ # copy of this software and associated documentation files (the "Software"),
1470
+ # to deal in the Software without restriction, including without limitation
1471
+ # the rights to use, copy, modify, merge, publish, distribute, sublicense,
1472
+ # and/or sell copies of the Software, and to permit persons to whom the
1473
+ # Software is furnished to do so, subject to the following conditions:
1474
+ #
1475
+ # The above copyright notice and this permission notice shall be included
1476
+ # in all copies or substantial portions of the Software.
1477
+ #
1478
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
1479
+ # OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
1480
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
1481
+ # THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
1482
+ # OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
1483
+ # ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
1484
+ # OTHER DEALINGS IN THE SOFTWARE.