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/ltp.py ADDED
@@ -0,0 +1,1107 @@
1
+
2
+ # -*- coding: utf-8 -*-
3
+
4
+ u'''I{Local Tangent Plane} (LTP) and I{local} cartesian coordinates.
5
+
6
+ I{Local cartesian} and I{local tangent plane} classes L{LocalCartesian}, approximations L{ChLVa}
7
+ and L{ChLVe} and L{Ltp}, L{ChLV}, L{LocalError}, L{Attitude} and L{Frustum}.
8
+
9
+ @see: U{Local tangent plane coordinates<https://WikiPedia.org/wiki/Local_tangent_plane_coordinates>}
10
+ and class L{LocalCartesian}, transcoded from I{Charles Karney}'s C++ classU{LocalCartesian
11
+ <https://GeographicLib.SourceForge.io/C++/doc/classGeographicLib_1_1LocalCartesian.html>}.
12
+ '''
13
+ # make sure int/int division yields float quotient, see .basics
14
+ from __future__ import division as _; del _ # PYCHOK semicolon
15
+
16
+ from pygeodesy.basics import _args_kwds_names, issubclassof, map1, map2 # .datums
17
+ from pygeodesy.constants import EPS, INT0, _umod_360, _0_0, _0_01, _0_5, _1_0, \
18
+ _2_0, _60_0, _90_0, _100_0, _180_0, _3600_0, \
19
+ _N_1_0 # PYCHOK used!
20
+ from pygeodesy.datums import _WGS84, _xinstanceof
21
+ from pygeodesy.ecef import _EcefBase, EcefKarney, _llhn4, _xyzn4
22
+ from pygeodesy.errors import _NotImplementedError, _TypesError, _ValueError, \
23
+ _xattr, _xkwds, _xkwds_get
24
+ from pygeodesy.fmath import fabs, fdot, Fhorner
25
+ from pygeodesy.fsums import _floor, Fsum, fsumf_, fsum1f_
26
+ from pygeodesy.interns import NN, _0_, _COMMASPACE_, _DOT_, _ecef_, _height_, \
27
+ _invalid_, _lat0_, _lon0_, _ltp_, _M_, _name_, _too_
28
+ # from pygeodesy.lazily import _ALL_LAZY # from vector3d
29
+ from pygeodesy.ltpTuples import Attitude4Tuple, ChLVEN2Tuple, ChLV9Tuple, \
30
+ ChLVYX2Tuple, Footprint5Tuple, Local9Tuple, \
31
+ ChLVyx2Tuple, _XyzLocals4, _XyzLocals5, Xyz4Tuple
32
+ from pygeodesy.named import _NamedBase, notOverloaded
33
+ from pygeodesy.namedTuples import LatLon3Tuple, LatLon4Tuple, Vector3Tuple
34
+ from pygeodesy.props import Property, Property_RO, property_doc_, property_RO, \
35
+ _update_all
36
+ from pygeodesy.streprs import Fmt, strs, unstr
37
+ from pygeodesy.units import Bearing, Degrees, _isHeight, Meter
38
+ from pygeodesy.utily import cotd, _loneg, sincos2d, sincos2d_, tand, tand_, \
39
+ wrap180, wrap360
40
+ from pygeodesy.vector3d import _ALL_LAZY, Vector3d
41
+
42
+ # from math import fabs, floor as _floor # from .fmath, .fsums
43
+
44
+ __all__ = _ALL_LAZY.ltp
45
+ __version__ = '24.02.16'
46
+
47
+ _height0_ = _height_ + _0_
48
+ _narrow_ = 'narrow'
49
+ _wide_ = 'wide'
50
+ _Xyz_ = 'Xyz'
51
+
52
+
53
+ def _fov_2(**fov):
54
+ # Half a field-of-view angle in C{degrees}.
55
+ f = Degrees(Error=LocalError, **fov) * _0_5
56
+ if EPS < f < _90_0:
57
+ return f
58
+ t = _invalid_ if f < 0 else _too_(_wide_ if f > EPS else _narrow_)
59
+ raise LocalError(txt=t, **fov)
60
+
61
+
62
+ class Attitude(_NamedBase):
63
+ '''The orientation of a plane or camera in space.
64
+ '''
65
+ _alt = Meter( alt =_0_0)
66
+ _roll = Degrees(roll=_0_0)
67
+ _tilt = Degrees(tilt=_0_0)
68
+ _yaw = Bearing(yaw =_0_0)
69
+
70
+ def __init__(self, alt_attitude=INT0, tilt=INT0, yaw=INT0, roll=INT0, name=NN):
71
+ '''New L{Attitude}.
72
+
73
+ @kwarg alt_attitude: An altitude (C{meter}) above earth or an attitude
74
+ (L{Attitude} or L{Attitude4Tuple}) with the
75
+ C{B{alt}itude}, B{C{tilt}}, B{C{yaw}} and B{C{roll}}.
76
+ @kwarg tilt: Pitch, elevation from horizontal (C{degrees180}), negative down
77
+ (clockwise rotation along and around the x- or East axis).
78
+ @kwarg yaw: Bearing, heading (compass C{degrees360}), clockwise from North
79
+ (counter-clockwise rotation along and around the z- or Up axis).
80
+ @kwarg roll: Roll, bank (C{degrees180}), positive to the right and down
81
+ (clockwise rotation along and around the y- or North axis).
82
+ @kwarg name: Optional name C{str}).
83
+
84
+ @raise AttitudeError: Invalid B{C{alt_attitude}}, B{C{tilt}}, B{C{yaw}} or
85
+ B{C{roll}}.
86
+
87
+ @see: U{Principal axes<https://WikiPedia.org/wiki/Aircraft_principal_axes>} and
88
+ U{Yaw, pitch, and roll rotations<http://MSL.CS.UIUC.edu/planning/node102.html>}.
89
+ '''
90
+ if _isHeight(alt_attitude):
91
+ t = Attitude4Tuple(alt_attitude, tilt, yaw, roll)
92
+ else:
93
+ try:
94
+ t = alt_attitude.atyr
95
+ except AttributeError:
96
+ raise AttitudeError(alt=alt_attitude, tilt=tilt, yaw=yaw, rol=roll)
97
+ for n, v in t.items():
98
+ if v:
99
+ setattr(self, n, v)
100
+ n = name or t.name
101
+ if n:
102
+ self.name = n
103
+
104
+ @property_doc_(' altitude above earth in C{meter}.')
105
+ def alt(self):
106
+ return self._alt
107
+
108
+ @alt.setter # PYCHOK setter!
109
+ def alt(self, alt): # PYCHOK no cover
110
+ a = Meter(alt=alt, Error=AttitudeError)
111
+ if self._alt != a:
112
+ _update_all(self)
113
+ self._alt = a
114
+
115
+ altitude = alt
116
+
117
+ @Property_RO
118
+ def atyr(self):
119
+ '''Return this attitude's alt[itude], tilt, yaw and roll as an L{Attitude4Tuple}.
120
+ '''
121
+ return Attitude4Tuple(self.alt, self.tilt, self.yaw, self.roll, name=self.name)
122
+
123
+ @Property_RO
124
+ def matrix(self):
125
+ '''Get the 3x3 rotation matrix C{R(yaw)·R(tilt)·R(roll)}, aka I{ZYX} (C{float}, row-order).
126
+
127
+ @see: Matrix M of case 10 in U{Appendix A
128
+ <https://ntrs.NASA.gov/api/citations/19770019231/downloads/19770019231.pdf>}.
129
+ '''
130
+ _f = fsum1f_
131
+ # to follow the definitions of rotation angles alpha, beta and gamma:
132
+ # negate yaw since yaw is counter-clockwise around the z-axis, swap
133
+ # tilt and roll since tilt is around the x- and roll around the y-axis
134
+ sa, ca, sb, cb, sg, cg = sincos2d_(-self.yaw, self.roll, self.tilt)
135
+ return ((ca * cb, _f(ca * sb * sg, -sa * cg), _f(ca * sb * cg, sa * sg)),
136
+ (sa * cb, _f(sa * sb * sg, ca * cg), _f(sa * sb * cg, -ca * sg)),
137
+ ( -sb, cb * sg, cb * cg))
138
+
139
+ @property_doc_(' roll/bank in C{degrees180}, positive to the right and down.')
140
+ def roll(self):
141
+ return self._roll
142
+
143
+ @roll.setter # PYCHOK setter!
144
+ def roll(self, roll):
145
+ r = Degrees(roll=roll, wrap=wrap180, Error=AttitudeError)
146
+ if self._roll != r:
147
+ _update_all(self)
148
+ self._roll = r
149
+
150
+ bank = roll
151
+
152
+ def rotate(self, x_xyz, y=None, z=None, Vector=None, **Vector_kwds):
153
+ '''Transform a (local) cartesian by this attitude's matrix.
154
+
155
+ @arg x_xyz: X component of vector (C{scalar}) or (3-D) vector
156
+ (C{Cartesian}, L{Vector3d} or L{Vector3Tuple}).
157
+ @kwarg y: Y component of vector (C{scalar}), same units as B{C{x}}.
158
+ @kwarg z: Z component of vector (C{scalar}), same units as B{C{x}}.
159
+ @kwarg Vector: Class to return transformed point (C{Cartesian},
160
+ L{Vector3d} or C{Vector3Tuple}) or C{None}.
161
+ @kwarg Vector_kwds: Optional, additional B{C{Vector}} keyword arguments,
162
+ ignored if C{B{Vector} is None}.
163
+
164
+ @return: A B{C{Vector}} instance or a L{Vector3Tuple}C{(x, y, z)} if
165
+ C{B{Vector}=None}.
166
+
167
+ @raise AttitudeError: Invalid B{C{x_xyz}}, B{C{y}} or B{C{z}}.
168
+
169
+ @see: U{Yaw, pitch, and roll rotations<http://MSL.CS.UIUC.edu/planning/node102.html>}.
170
+ '''
171
+ try:
172
+ try:
173
+ xyz = map2(float, x_xyz.xyz)
174
+ except AttributeError:
175
+ xyz = map1(float, x_xyz, y, z)
176
+ except (TypeError, ValueError) as x:
177
+ raise AttitudeError(x_xyz=x_xyz, y=y, z=z, cause=x)
178
+
179
+ x, y, z = (fdot(r, *xyz) for r in self.matrix)
180
+ return Vector3Tuple(x, y, z, name=self.name) if Vector is None else \
181
+ Vector(x, y, z, **_xkwds(Vector_kwds, name=self.name))
182
+
183
+ @property_doc_(' tilt/pitch/elevation from horizontal in C{degrees180}, negative down.')
184
+ def tilt(self):
185
+ return self._tilt
186
+
187
+ @tilt.setter # PYCHOK setter!
188
+ def tilt(self, tilt):
189
+ t = Degrees(tilt=tilt, wrap=wrap180, Error=AttitudeError)
190
+ if self._tilt != t:
191
+ _update_all(self)
192
+ self._tilt = t
193
+
194
+ elevation = pitch = tilt
195
+
196
+ def toStr(self, prec=6, sep=_COMMASPACE_, **unused): # PYCHOK signature
197
+ '''Format this attitude as string.
198
+
199
+ @kwarg prec: The C{float} precision, number of decimal digits (0..9).
200
+ Trailing zero decimals are stripped for B{C{prec}} values
201
+ of 1 and above, but kept for negative B{C{prec}} values.
202
+ @kwarg sep: Separator to join (C{str}).
203
+
204
+ @return: This attitude (C{str}).
205
+ '''
206
+ return self.atyr.toStr(prec=prec, sep=sep)
207
+
208
+ @Property_RO
209
+ def tyr3d(self):
210
+ '''Get this attitude's (3-D) directional vector (L{Vector3d}).
211
+
212
+ @see: U{Yaw, pitch, and roll rotations<http://MSL.CS.UIUC.edu/planning/node102.html>}.
213
+ '''
214
+ def _r2d(r):
215
+ return fsumf_(_N_1_0, *r)
216
+
217
+ return Vector3d(*map(_r2d, self.matrix), name=tyr3d.__name__)
218
+
219
+ @property_doc_(' yaw/bearing/heading in compass C{degrees360}, clockwise from North.')
220
+ def yaw(self):
221
+ return self._yaw
222
+
223
+ @yaw.setter # PYCHOK setter!
224
+ def yaw(self, yaw):
225
+ y = Bearing(yaw=yaw, Error=AttitudeError)
226
+ if self._yaw != y:
227
+ _update_all(self)
228
+ self._yaw = y
229
+
230
+ bearing = heading = yaw
231
+
232
+
233
+ class AttitudeError(_ValueError):
234
+ '''An L{Attitude} or L{Attitude4Tuple} issue.
235
+ '''
236
+ pass
237
+
238
+
239
+ class Frustum(_NamedBase):
240
+ '''A rectangular pyramid, typically representing a camera's I{field-of-view}
241
+ (fov) and the intersection with (or projection to) a I{local tangent plane}.
242
+
243
+ @see: U{Viewing frustum<https://WikiPedia.org/wiki/Viewing_frustum>}.
244
+ '''
245
+ _h_2 = _0_0 # half hfov in degrees
246
+ _ltp = None # local tangent plane
247
+ _tan_h_2 = _0_0 # tan(_h_2)
248
+ _v_2 = _0_0 # half vfov in degrees
249
+
250
+ def __init__(self, hfov, vfov, ltp=None):
251
+ '''New L{Frustum}.
252
+
253
+ @arg hfov: Horizontal field-of-view (C{degrees180}).
254
+ @arg vfov: Vertical field-of-view (C{degrees180}).
255
+ @kwarg ltp: Optional I{local tangent plane} (L{Ltp}).
256
+
257
+ @raise LocalError: Invalid B{C{hfov}} or B{C{vfov}}.
258
+ '''
259
+ self._h_2 = h = _fov_2(hfov=hfov)
260
+ self._v_2 = _fov_2(vfov=vfov)
261
+
262
+ self._tan_h_2 = tand(h, hfov_2=h)
263
+
264
+ if ltp:
265
+ self._ltp = _xLtp(ltp)
266
+
267
+ def footprint5(self, alt_attitude, tilt=0, yaw=0, roll=0, z=_0_0, ltp=None): # MCCABE 15
268
+ '''Compute the center and corners of the intersection with (or projection
269
+ to) the I{local tangent plane} (LTP).
270
+
271
+ @arg alt_attitude: An altitude (C{meter}) above I{local tangent plane} or
272
+ an attitude (L{Attitude} or L{Attitude4Tuple}) with the
273
+ C{B{alt}itude}, B{C{tilt}}, B{C{yaw}} and B{C{roll}}.
274
+ @kwarg tilt: Pitch, elevation from horizontal (C{degrees}), negative down
275
+ (clockwise rotation along and around the x- or East axis).
276
+ @kwarg yaw: Bearing, heading (compass C{degrees}), clockwise from North
277
+ (counter-clockwise rotation along and around the z- or Up axis).
278
+ @kwarg roll: Roll, bank (C{degrees}), positive to the right and down
279
+ (clockwise rotation along and around the y- or North axis).
280
+ @kwarg z: Optional height of the footprint (C{meter}) above I{local tangent plane}.
281
+ @kwarg ltp: The I{local tangent plane} (L{Ltp}), overriding this
282
+ frustum's C{ltp}.
283
+
284
+ @return: A L{Footprint5Tuple}C{(center, upperleft, upperight, loweright,
285
+ lowerleft)} with the C{center} and 4 corners, each an L{Xyz4Tuple}.
286
+
287
+ @raise TypeError: Invalid B{C{ltp}}.
288
+
289
+ @raise UnitError: Invalid B{C{altitude}}, B{C{tilt}}, B{C{roll}} or B{C{z}}.
290
+
291
+ @raise ValueError: If B{C{altitude}} too low, B{C{z}} too high or B{C{tilt}}
292
+ or B{C{roll}} -including B{C{vfov}} respectively B{C{hfov}}-
293
+ over the horizon.
294
+
295
+ @see: U{Principal axes<https://WikiPedia.org/wiki/Aircraft_principal_axes>}.
296
+ '''
297
+ def _xy2(a, e, h_2, tan_h_2, r):
298
+ # left and right corners, or swapped
299
+ if r < EPS: # no roll
300
+ r = a * tan_h_2
301
+ l = -r # PYCHOK l is ell
302
+ else: # roll
303
+ r, l = tand_(r - h_2, r + h_2, roll_hfov=r) # PYCHOK l is ell
304
+ r *= -a # negate right positive
305
+ l *= -a # PYCHOK l is ell
306
+ y = a * cotd(e, tilt_vfov=e)
307
+ return (l, y), (r, y)
308
+
309
+ def _xyz5(b, xy5, z, ltp):
310
+ # rotate (x, y)'s by bearing, clockwise
311
+ s, c = sincos2d(b)
312
+ _f = fsum1f_
313
+ for x, y in xy5:
314
+ yield Xyz4Tuple(_f(x * c, y * s),
315
+ _f(y * c, -x * s), z, ltp)
316
+
317
+ try:
318
+ a, t, y, r = alt_attitude.atyr
319
+ except AttributeError:
320
+ a, t, y, r = alt_attitude, tilt, yaw, roll
321
+
322
+ a = Meter(altitude=a)
323
+ if a < EPS: # too low
324
+ raise _ValueError(altitude=a)
325
+ if z: # PYCHOK no cover
326
+ z = Meter(z=z)
327
+ a -= z
328
+ if a < EPS: # z above a
329
+ raise _ValueError(altitude_z=a)
330
+ else:
331
+ z = _0_0
332
+
333
+ b = Degrees(yaw=y, wrap=wrap360) # bearing
334
+ e = -Degrees(tilt=t, wrap=wrap180) # elevation, pitch
335
+ if not EPS < e < _180_0:
336
+ raise _ValueError(tilt=t)
337
+ if e > _90_0:
338
+ e = _loneg(e)
339
+ b = _umod_360(b + _180_0)
340
+
341
+ r = Degrees(roll=r, wrap=wrap180) # roll center
342
+ x = (-a * tand(r, roll=r)) if r else _0_0
343
+ y = a * cotd(e, tilt=t) # ground range
344
+ if fabs(y) < EPS:
345
+ y = _0_0
346
+
347
+ v, h, t = self._v_2, self._h_2, self._tan_h_2
348
+ # center and corners, clockwise from upperleft, rolled
349
+ xy5 = ((x, y),) + _xy2(a, e - v, h, t, r) \
350
+ + _xy2(a, e + v, -h, -t, r) # swapped
351
+ # turn center and corners by yaw, clockwise
352
+ p = self.ltp if ltp is None else ltp # None OK
353
+ return Footprint5Tuple(_xyz5(b, xy5, z, p)) # *_xyz5
354
+
355
+ @Property_RO
356
+ def hfov(self):
357
+ '''Get the horizontal C{fov} (C{degrees}).
358
+ '''
359
+ return Degrees(hfov=self._h_2 * _2_0)
360
+
361
+ @Property_RO
362
+ def ltp(self):
363
+ '''Get the I{local tangent plane} (L{Ltp}) or C{None}.
364
+ '''
365
+ return self._ltp
366
+
367
+ def toStr(self, prec=3, fmt=Fmt.F, sep=_COMMASPACE_): # PYCHOK signature
368
+ '''Convert this frustum to a "hfov, vfov, ltp" string.
369
+
370
+ @kwarg prec: Number of (decimal) digits, unstripped (0..8 or C{None}).
371
+ @kwarg fmt: Optional, C{float} format (C{letter}).
372
+ @kwarg sep: Separator to join (C{str}).
373
+
374
+ @return: Frustum in the specified form (C{str}).
375
+ '''
376
+ t = self.hfov, self.vfov
377
+ if self.ltp:
378
+ t += self.ltp,
379
+ t = strs(t, prec=prec, fmt=fmt)
380
+ return sep.join(t) if sep else t
381
+
382
+ @Property_RO
383
+ def vfov(self):
384
+ '''Get the vertical C{fov} (C{degrees}).
385
+ '''
386
+ return Degrees(vfov=self._v_2 * _2_0)
387
+
388
+
389
+ class LocalError(_ValueError):
390
+ '''A L{LocalCartesian} or L{Ltp} related issue.
391
+ '''
392
+ pass
393
+
394
+
395
+ class LocalCartesian(_NamedBase):
396
+ '''Conversion between geodetic C{(lat, lon, height)} and I{local
397
+ cartesian} C{(x, y, z)} coordinates with I{geodetic} origin
398
+ C{(lat0, lon0, height0)}, transcoded from I{Karney}'s C++ class
399
+ U{LocalCartesian<https://GeographicLib.SourceForge.io/C++/doc/
400
+ classGeographicLib_1_1LocalCartesian.html>}.
401
+
402
+ The C{z} axis is normal to the ellipsoid, the C{y} axis points due
403
+ North. The plane C{z = -height0} is tangent to the ellipsoid.
404
+
405
+ The conversions all take place via geocentric coordinates using a
406
+ geocentric L{EcefKarney}, by default the WGS84 datum/ellipsoid.
407
+
408
+ @see: Class L{Ltp}.
409
+ '''
410
+ _ecef = EcefKarney(_WGS84)
411
+ _Ecef = EcefKarney
412
+ _lon00 = INT0 # self.lon0
413
+ _t0 = None # origin (..., lat0, lon0, height0, ...) L{Ecef9Tuple}
414
+ _9Tuple = Local9Tuple
415
+
416
+ def __init__(self, latlonh0=INT0, lon0=INT0, height0=INT0, ecef=None, name=NN, **lon00):
417
+ '''New L{LocalCartesian} converter.
418
+
419
+ @kwarg latlonh0: The (geodetic) origin (C{LatLon}, L{LatLon4Tuple}, L{Ltp}
420
+ L{LocalCartesian} or L{Ecef9Tuple}) or the C{scalar}
421
+ latitude of the (goedetic) origin (C{degrees}).
422
+ @kwarg lon0: Longitude of the (goedetic) origin (C{degrees}) for C{scalar}
423
+ B{C{latlonh0}}, ignored otherwise.
424
+ @kwarg height0: Optional height (C{meter}, conventionally) at the (goedetic)
425
+ origin perpendicular to and above (or below) the ellipsoid's
426
+ surface and for C{scalar} B{C{latlonh0}}, ignored otherwise.
427
+ @kwarg ecef: An ECEF converter (L{EcefKarney} I{only}) for C{scalar}
428
+ B{C{latlonh0}}, ignored otherwise.
429
+ @kwarg name: Optional name (C{str}).
430
+ @kwarg lon00: An arbitrary, I{polar} longitude (C{degrees}), overriding
431
+ the default C{B{lon00}=B{lon0}}, see method C{reverse}.
432
+
433
+ @raise LocalError: If B{C{latlonh0}} not C{LatLon}, L{LatLon4Tuple}, L{Ltp},
434
+ L{LocalCartesian} or L{Ecef9Tuple} or B{C{latlonh0}},
435
+ B{C{lon0}}, B{C{height0}} or B{C{lon00}} invalid.
436
+
437
+ @raise TypeError: Invalid B{C{ecef}} or not L{EcefKarney}.
438
+
439
+ @note: If BC{latlonh0} is an L{Ltp} or L{LocalCartesian}, only C{lat0}, C{lon0},
440
+ C{height0} and I{polar} C{lon00} are copied, I{not} the ECEF converter.
441
+ '''
442
+ self.reset(latlonh0, lon0=lon0, height0=height0, ecef=ecef, name=name, **lon00)
443
+
444
+ def __eq__(self, other):
445
+ '''Compare this and an other instance.
446
+
447
+ @arg other: The other ellipsoid (L{LocalCartesian} or L{Ltp}).
448
+
449
+ @return: C{True} if equal, C{False} otherwise.
450
+ '''
451
+ return other is self or (isinstance(other, self.__class__) and
452
+ other.ecef == self.ecef and
453
+ other._t0 == self._t0)
454
+
455
+ @Property_RO
456
+ def datum(self):
457
+ '''Get the ECEF converter's datum (L{Datum}).
458
+ '''
459
+ return self.ecef.datum
460
+
461
+ @Property_RO
462
+ def ecef(self):
463
+ '''Get the ECEF converter (L{EcefKarney}).
464
+ '''
465
+ return self._ecef
466
+
467
+ def _ecef2local(self, ecef, Xyz, Xyz_kwds):
468
+ '''(INTERNAL) Convert geocentric/geodetic to local, like I{forward}.
469
+
470
+ @arg ecef: Geocentric (and geodetic) (L{Ecef9Tuple}).
471
+ @arg Xyz: An L{XyzLocal}, L{Enu} or L{Ned} I{class} or C{None}.
472
+ @arg Xyz_kwds: B{C{Xyz}} keyword arguments, ignored if C{B{Xyz} is None}.
473
+
474
+ @return: An C{B{Xyz}(x, y, z, ltp, **B{Xyz_kwds}} instance or if
475
+ C{B{Xyz} is None}, a L{Local9Tuple}C{(x, y, z, lat, lon,
476
+ height, ltp, ecef, M)} with this C{ltp}, B{C{ecef}}
477
+ (L{Ecef9Tuple}) converted to this C{datum} and C{M=None},
478
+ always.
479
+ '''
480
+ ltp = self
481
+ if ecef.datum != ltp.datum:
482
+ ecef = ecef.toDatum(ltp.datum)
483
+ x, y, z = self.M.rotate(ecef.xyz, *ltp._t0_xyz)
484
+ r = Local9Tuple(x, y, z, ecef.lat, ecef.lon, ecef.height,
485
+ ltp, ecef, None, name=ecef.name)
486
+ if Xyz:
487
+ if not issubclassof(Xyz, *_XyzLocals4): # Vector3d
488
+ raise _TypesError(_Xyz_, Xyz, *_XyzLocals4)
489
+ r = r.toXyz(Xyz=Xyz, **Xyz_kwds)
490
+ return r
491
+
492
+ @Property_RO
493
+ def ellipsoid(self):
494
+ '''Get the ECEF converter's ellipsoid (L{Ellipsoid}).
495
+ '''
496
+ return self.ecef.datum.ellipsoid
497
+
498
+ def forward(self, latlonh, lon=None, height=0, M=False, name=NN):
499
+ '''Convert I{geodetic} C{(lat, lon, height)} to I{local} cartesian
500
+ C{(x, y, z)}.
501
+
502
+ @arg latlonh: Either a C{LatLon}, L{Ltp}, L{Ecef9Tuple} or C{scalar}
503
+ (geodetic) latitude (C{degrees}).
504
+ @kwarg lon: Optional C{scalar} (geodetic) longitude for C{scalar}
505
+ B{C{latlonh}} (C{degrees}).
506
+ @kwarg height: Optional height (C{meter}, conventionally) perpendicular
507
+ to and above (or below) the ellipsoid's surface.
508
+ @kwarg M: Optionally, return the I{concatenated} rotation L{EcefMatrix},
509
+ iff available (C{bool}).
510
+ @kwarg name: Optional name (C{str}).
511
+
512
+ @return: A L{Local9Tuple}C{(x, y, z, lat, lon, height, ltp, ecef, M)}
513
+ with I{local} C{x}, C{y}, C{z}, I{geodetic} C{(lat}, C{lon},
514
+ C{height}, this C{ltp}, C{ecef} (L{Ecef9Tuple}) with
515
+ I{geocentric} C{x}, C{y}, C{z} (and I{geodetic} C{lat},
516
+ C{lon}, C{height}) and the I{concatenated} rotation matrix
517
+ C{M} (L{EcefMatrix}) if requested.
518
+
519
+ @raise LocalError: If B{C{latlonh}} not C{scalar}, C{LatLon}, L{Ltp},
520
+ L{Ecef9Tuple} or invalid or if B{C{lon}} not
521
+ C{scalar} for C{scalar} B{C{latlonh}} or invalid
522
+ or if B{C{height}} invalid.
523
+ '''
524
+ lat, lon, h, n = _llhn4(latlonh, lon, height, Error=LocalError, name=name)
525
+ t = self.ecef._forward(lat, lon, h, n, M=M)
526
+ x, y, z = self.M.rotate(t.xyz, *self._t0_xyz)
527
+ m = self.M.multiply(t.M) if M else None
528
+ return self._9Tuple(x, y, z, lat, lon, h, self, t, m, name=n or self.name)
529
+
530
+ @Property_RO
531
+ def height0(self):
532
+ '''Get the origin's height (C{meter}).
533
+ '''
534
+ return self._t0.height
535
+
536
+ @Property_RO
537
+ def lat0(self):
538
+ '''Get the origin's latitude (C{degrees}).
539
+ '''
540
+ return self._t0.lat
541
+
542
+ @Property_RO
543
+ def latlonheight0(self):
544
+ '''Get the origin's lat-, longitude and height (L{LatLon3Tuple}C{(lat, lon, height)}).
545
+ '''
546
+ return LatLon3Tuple(self.lat0, self.lon0, self.height0, name=self.name)
547
+
548
+ def _local2ecef(self, local, nine=False, M=False):
549
+ '''(INTERNAL) Convert I{local} to geocentric/geodetic, like I{.reverse}.
550
+
551
+ @arg local: Local (L{XyzLocal}, L{Enu}, L{Ned}, L{Aer} or L{Local9Tuple}).
552
+ @kwarg nine: Return 3- or 9-tuple (C{bool}).
553
+ @kwarg M: Include the rotation matrix (C{bool}).
554
+
555
+ @return: A I{geocentric} 3-tuple C{(x, y, z)} or if C{B{nine}=True},
556
+ an L{Ecef9Tuple}C{(x, y, z, lat, lon, height, C, M, datum)},
557
+ optionally including rotation matrix C{M} or C{None}.
558
+ '''
559
+ t = self.M.unrotate(local.xyz, *self._t0_xyz)
560
+ if nine:
561
+ t = self.ecef.reverse(*t, M=M)
562
+ return t
563
+
564
+ @Property_RO
565
+ def lon0(self):
566
+ '''Get the origin's longitude (C{degrees}).
567
+ '''
568
+ return self._t0.lon
569
+
570
+ @Property
571
+ def lon00(self):
572
+ '''Get the arbitrary, I{polar} longitude (C{degrees}).
573
+ '''
574
+ return self._lon00
575
+
576
+ @lon00.setter # PYCHOK setter!
577
+ def lon00(self, lon00):
578
+ '''Set the arbitrary, I{polar} longitude (C{degrees}).
579
+ '''
580
+ # lon00 <https://GitHub.com/mrJean1/PyGeodesy/issues/77>
581
+ self._lon00 = Degrees(lon00=lon00)
582
+
583
+ @Property_RO
584
+ def M(self):
585
+ '''Get the rotation matrix (C{EcefMatrix}).
586
+ '''
587
+ return self._t0.M
588
+
589
+ def reset(self, latlonh0=INT0, lon0=INT0, height0=INT0, ecef=None, name=NN, **lon00):
590
+ '''Reset this converter, see L{LocalCartesian.__init__} and L{Ltp.__init__} for more details.
591
+ '''
592
+ if isinstance(latlonh0, LocalCartesian):
593
+ if self._t0:
594
+ _update_all(self)
595
+ self._ecef = latlonh0.ecef
596
+ self._lon00 = latlonh0.lon00
597
+ self._t0 = latlonh0._t0
598
+ n = name or latlonh0.name
599
+ else:
600
+ lat0, lon0, height0, n = _llhn4(latlonh0, lon0, height0, suffix=_0_,
601
+ Error=LocalError, name=name or self.name)
602
+ if ecef: # PYCHOK no cover
603
+ _xinstanceof(self._Ecef, ecef=ecef)
604
+ _update_all(self)
605
+ self._ecef = ecef
606
+ elif self._t0:
607
+ _update_all(self)
608
+ self._t0 = self.ecef._forward(lat0, lon0, height0, n, M=True)
609
+ self.lon00 = _xattr(latlonh0, lon00=_xkwds_get(lon00, lon00=lon0))
610
+ if n:
611
+ self.rename(n)
612
+
613
+ def reverse(self, xyz, y=None, z=None, M=False, name=NN, **lon00):
614
+ '''Convert I{local} C{(x, y, z)} to I{geodetic} C{(lat, lon, height)}.
615
+
616
+ @arg xyz: A I{local} (L{XyzLocal}, L{Enu}, L{Ned}, L{Aer}, L{Local9Tuple}) or
617
+ local C{x} coordinate (C{scalar}).
618
+ @kwarg y: Local C{y} coordinate for C{scalar} B{C{xyz}} and B{C{z}} (C{meter}).
619
+ @kwarg z: Local C{z} coordinate for C{scalar} B{C{xyz}} and B{C{y}} (C{meter}).
620
+ @kwarg M: Optionally, return the I{concatenated} rotation L{EcefMatrix}, iff
621
+ available (C{bool}).
622
+ @kwarg name: Optional name (C{str}).
623
+ @kwarg lon00: An arbitrary, I{polar} longitude (C{degrees}), returned for local
624
+ C{B{x}=0} and C{B{y}=0} at I{polar} latitudes C{abs(B{lat0}) == 90},
625
+ overriding property C{lon00} and default C{B{lon00}=B{lon0}}.
626
+
627
+ @return: An L{Local9Tuple}C{(x, y, z, lat, lon, height, ltp, ecef, M)} with
628
+ I{local} C{x}, C{y}, C{z}, I{geodetic} C{lat}, C{lon}, C{height},
629
+ this C{ltp}, an C{ecef} (L{Ecef9Tuple}) with the I{geocentric} C{x},
630
+ C{y}, C{z} (and I{geodetic} C{lat}, C{lon}, C{height}) and the
631
+ I{concatenated} rotation matrix C{M} (L{EcefMatrix}) if requested.
632
+
633
+ @raise LocalError: Invalid B{C{xyz}} or C{scalar} C{x} or B{C{y}} and/or B{C{z}}
634
+ not C{scalar} for C{scalar} B{C{xyz}}.
635
+ '''
636
+ x, y, z, n = _xyzn4(xyz, y, z, _XyzLocals5, Error=LocalError, name=name)
637
+ c = self.M.unrotate((x, y, z), *self._t0_xyz)
638
+ t = self.ecef.reverse(*c, M=M, lon00=_xkwds_get(lon00, lon00=self.lon00))
639
+ m = self.M.multiply(t.M) if M else None
640
+ return self._9Tuple(x, y, z, t.lat, t.lon, t.height, self, t, m, name=n or self.name)
641
+
642
+ @Property_RO
643
+ def _t0_xyz(self):
644
+ '''(INTERNAL) Get C{(x0, y0, z0)} as L{Vector3Tuple}.
645
+ '''
646
+ return self._t0.xyz
647
+
648
+ def toStr(self, prec=9, **unused): # PYCHOK signature
649
+ '''Return this L{LocalCartesian} as a string.
650
+
651
+ @kwarg prec: Precision, number of (decimal) digits (0..9).
652
+
653
+ @return: This L{LocalCartesian} representation (C{str}).
654
+ '''
655
+ return self.attrs(_lat0_, _lon0_, _height0_, _M_, _ecef_, _name_, prec=prec)
656
+
657
+
658
+ class Ltp(LocalCartesian):
659
+ '''A I{local tangent plan} (LTP), a sub-class of C{LocalCartesian} with
660
+ (re-)configurable ECEF converter.
661
+ '''
662
+ _Ecef = _EcefBase
663
+
664
+ def __init__(self, latlonh0=INT0, lon0=INT0, height0=INT0, ecef=None, name=NN, **lon00):
665
+ '''New C{Ltp}, see L{LocalCartesian.__init__} for more details.
666
+
667
+ @kwarg ecef: Optional ECEF converter (L{EcefKarney}, L{EcefFarrell21},
668
+ L{EcefFarrell22}, L{EcefSudano}, L{EcefVeness} or
669
+ L{EcefYou} I{instance}), overriding the default
670
+ L{EcefKarney}C{(datum=Datums.WGS84)} for C{scalar}.
671
+
672
+ @raise TypeError: Invalid B{C{ecef}}.
673
+ '''
674
+ LocalCartesian.reset(self, latlonh0, lon0=lon0, height0=height0,
675
+ ecef=ecef, name=name, **lon00)
676
+
677
+ @Property
678
+ def ecef(self):
679
+ '''Get this LTP's ECEF converter (C{Ecef...} I{instance}).
680
+ '''
681
+ return self._ecef
682
+
683
+ @ecef.setter # PYCHOK setter!
684
+ def ecef(self, ecef):
685
+ '''Set this LTP's ECEF converter (C{Ecef...} I{instance}).
686
+
687
+ @raise TypeError: Invalid B{C{ecef}}.
688
+ '''
689
+ _xinstanceof(_EcefBase, ecef=ecef)
690
+ if self._ecef != ecef: # PYCHOK no cover
691
+ self.reset(self._t0)
692
+ self._ecef = ecef
693
+
694
+
695
+ class _ChLV(object):
696
+ '''(INTERNAL) Base class for C{ChLV*} classes.
697
+ '''
698
+ _03_falsing = ChLVyx2Tuple(0.6e6, 0.2e6)
699
+ # _92_falsing = ChLVYX2Tuple(2.0e6, 1.0e6) # _95_ - _03_
700
+ _95_falsing = ChLVEN2Tuple(2.6e6, 1.2e6)
701
+
702
+ def _ChLV9Tuple(self, fw, M, name, *Y_X_h_lat_lon_h):
703
+ '''(INTERNAL) Helper for C{ChLVa/e.forward} and C{.reverse}.
704
+ '''
705
+ if bool(M): # PYCHOK no cover
706
+ m = self.forward if fw else self.reverse # PYCHOK attr
707
+ n = _DOT_(self.__class__.__name__, m.__name__)
708
+ raise _NotImplementedError(unstr(n, M=M), txt=None)
709
+ t = Y_X_h_lat_lon_h + (self, self._t0, None) # PYCHOK _t0
710
+ return ChLV9Tuple(t, name=name)
711
+
712
+ @property_RO
713
+ def _enh_n_h(self):
714
+ '''(INTERNAL) Get C{ChLV*.reverse} args[1:4] names, I{once}.
715
+ '''
716
+ _ChLV._enh_n_h = t = _args_kwds_names(_ChLV.reverse)[1:4] # overwrite property_RO
717
+ # assert _args_kwds_names( ChLV.reverse)[1:4] == t
718
+ # assert _args_kwds_names(ChLVa.reverse)[1:4] == t
719
+ # assert _args_kwds_names(ChLVe.reverse)[1:4] == t
720
+ return t
721
+
722
+ def forward(self, latlonh, lon=None, height=0, M=None, name=NN): # PYCHOK no cover
723
+ '''Convert WGS84 geodetic to I{Swiss} projection coordinates. I{Must be overloaded}.
724
+
725
+ @arg latlonh: Either a C{LatLon}, L{Ltp} or C{scalar} (geodetic) latitude (C{degrees}).
726
+ @kwarg lon: Optional, C{scalar} (geodetic) longitude for C{scalar} B{C{latlonh}} (C{degrees}).
727
+ @kwarg height: Optional, height, vertically above (or below) the surface of the ellipsoid
728
+ (C{meter}) for C{scalar} B{C{latlonh}} and B{C{lon}}.
729
+ @kwarg M: If C{True}, return the I{concatenated} rotation L{EcefMatrix} iff available
730
+ for C{ChLV} only, C{None} otherwise (C{bool}).
731
+ @kwarg name: Optional name (C{str}).
732
+
733
+ @return: A L{ChLV9Tuple}C{(Y, X, h_, lat, lon, height, ltp, ecef, M)} with the unfalsed
734
+ I{Swiss Y, X} coordinates, I{Swiss h_} height, the given I{geodetic} C{lat},
735
+ C{lon} and C{height}, this C{ChLV*} instance and C{ecef} (L{Ecef9Tuple}) at
736
+ I{Bern, Ch} and rotation matrix C{M}. The returned C{ltp} is this C{ChLV},
737
+ C{ChLVa} or C{ChLVe} instance.
738
+
739
+ @raise LocalError: Invalid or non-C{scalar} B{C{latlonh}}, B{C{lon}} or B{C{height}}.
740
+ '''
741
+ notOverloaded(self, latlonh, lon=lon, height=height, M=M, name=name)
742
+
743
+ def reverse(self, enh_, n=None, h_=0, M=None, **name): # PYCHOK no cover
744
+ '''Convert I{Swiss} projection to WGS84 geodetic coordinates.
745
+
746
+ @arg enh_: A Swiss projection (L{ChLV9Tuple}) or the C{scalar}, falsed I{Swiss E_LV95}
747
+ or I{y_LV03} easting (C{meter}).
748
+ @kwarg n: Falsed I{Swiss N_LV85} or I{x_LV03} northing for C{scalar} B{C{enh_}} and
749
+ B{C{h_}} (C{meter}).
750
+ @kwarg h_: I{Swiss h'} height for C{scalar} B{C{enh_}} and B{C{n}} (C{meter}).
751
+ @kwarg M: If C{True}, return the I{concatenated} rotation L{EcefMatrix} iff available
752
+ for C{ChLV} only, C{None} otherwise (C{bool}).
753
+ @kwarg name: Optional name (C{str}).
754
+
755
+ @return: A L{ChLV9Tuple}C{(Y, X, h_, lat, lon, height, ltp, ecef, M)} with the unfalsed
756
+ I{Swiss Y, X} coordinates, I{Swiss h_} height, the given I{geodetic} C{lat},
757
+ C{lon} and C{height}, this C{ChLV*} instance and C{ecef} (L{Ecef9Tuple}) at
758
+ I{Bern, Ch} and rotation matrix C{M}. The returned C{ltp} is this C{ChLV},
759
+ C{ChLVa} or C{ChLVe} instance.
760
+
761
+ @raise LocalError: Invalid or non-C{scalar} B{C{enh_}}, B{C{n}} or B{C{h_}}.
762
+ '''
763
+ notOverloaded(self, enh_, n=n, h_=h_, M=M, **name)
764
+
765
+ @staticmethod
766
+ def _falsing2(LV95):
767
+ '''(INTERNAL) Get the C{LV95} or C{LV03} falsing.
768
+ '''
769
+ return _ChLV._95_falsing if LV95 in (True, 95) else (
770
+ _ChLV._03_falsing if LV95 in (False, 3) else ChLVYX2Tuple(0, 0))
771
+
772
+ @staticmethod
773
+ def _llh2abh_3(lat, lon, h):
774
+ '''(INTERNAL) Helper for C{ChLVa/e.forward}.
775
+ '''
776
+ def _deg2ab(deg, sLL):
777
+ # convert degrees to arc-seconds
778
+ def _dms(ds, p, q, swap):
779
+ d = _floor(ds)
780
+ t = (ds - d) * p
781
+ m = _floor(t)
782
+ s = (t - m) * p
783
+ if swap:
784
+ d, s = s, d
785
+ return d + (m + s * q) * q
786
+
787
+ s = _dms(deg, _60_0, _0_01, False) # deg2sexag
788
+ s = _dms( s, _100_0, _60_0, True) # sexag2asec
789
+ return (s - sLL) / ChLV._s_ab
790
+
791
+ a = _deg2ab(lat, ChLV._sLat) # phi', lat_aux
792
+ b = _deg2ab(lon, ChLV._sLon) # lam', lng_aux
793
+ h_ = fsumf_(h, -ChLV.Bern.height, 2.73 * b, 6.94 * a)
794
+ return a, b, h_
795
+
796
+ @staticmethod
797
+ def _YXh_2abh3(Y, X, h_):
798
+ '''(INTERNAL) Helper for C{ChLVa/e.reverse}.
799
+ '''
800
+ def _YX2ab(YX):
801
+ return YX * ChLV._ab_m
802
+
803
+ a, b = map1(_YX2ab, Y, X)
804
+ h = fsumf_(h_, ChLV.Bern.height, -12.6 * a, -22.64 * b)
805
+ return a, b, h
806
+
807
+ def _YXh_n4(self, enh_, n, h_, **name):
808
+ '''(INTERNAL) Helper for C{ChLV*.reverse}.
809
+ '''
810
+ Y, X, h_, name = _xyzn4(enh_, n, h_, ChLV9Tuple,
811
+ _xyz_y_z_names=self._enh_n_h, **name)
812
+ if isinstance(enh_, ChLV9Tuple):
813
+ Y, X = enh_.Y, enh_.X
814
+ else: # isscalar(enh_)
815
+ Y, X = ChLV.unfalse2(Y, X) # PYCHOK ChLVYX2Tuple
816
+ return Y, X, h_, name
817
+
818
+
819
+ class ChLV(_ChLV, Ltp):
820
+ '''Conversion between I{WGS84 geodetic} and I{Swiss} projection coordinates using
821
+ L{pygeodesy.EcefKarney}'s Earth-Centered, Earth-Fixed (ECEF) methods.
822
+
823
+ @see: U{Swiss projection formulas<https://www.SwissTopo.admin.CH/en/maps-data-online/
824
+ calculation-services.html>}, page 7ff, U{NAVREF<https://www.SwissTopo.admin.CH/en/
825
+ maps-data-online/calculation-services/navref.html>}, U{REFRAME<https://www.SwissTopo.admin.CH/
826
+ en/maps-data-online/calculation-services/reframe.html>} and U{SwissTopo Scripts GPS WGS84
827
+ <-> LV03<https://GitHub.com/ValentinMinder/Swisstopo-WGS84-LV03>}.
828
+ '''
829
+ _9Tuple = ChLV9Tuple
830
+
831
+ _ab_d = 0.36 # a, b units per degree, ...
832
+ _ab_m = 1.0e-6 # ... per meter and ...
833
+ _ab_M = _1_0 # ... per 1,000 Km or 1 Mm
834
+ _s_d = _3600_0 # arc-seconds per degree ...
835
+ _s_ab = _s_d / _ab_d # ... and per a, b unit
836
+ _sLat = 169028.66 # Bern, Ch in ...
837
+ _sLon = 26782.5 # ... arc-seconds ...
838
+ # lat, lon, height == 46°57'08.66", 7°26'22.50", 49.55m ("new" 46°57'07.89", 7°26'22.335")
839
+ Bern = LatLon4Tuple(_sLat / _s_d, _sLon / _s_d, 49.55, _WGS84, name='Bern')
840
+
841
+ def __init__(self, latlonh0=Bern, **other_Ltp_kwds):
842
+ '''New ECEF-based I{WGS84-Swiss} L{ChLV} converter, centered at I{Bern, Ch}.
843
+
844
+ @kwarg latlonh0: The I{geodetic} origin and height, overriding C{Bern, Ch}.
845
+ @kwarg other_Ltp_kwds: Optional, other L{Ltp.__init__} keyword arguments.
846
+
847
+ @see: L{Ltp.__init__} for more information.
848
+ '''
849
+ Ltp.__init__(self, latlonh0, **_xkwds(other_Ltp_kwds, ecef=None, name=ChLV.Bern.name))
850
+
851
+ def forward(self, latlonh, lon=None, height=0, M=None, name=NN): # PYCHOK unused M
852
+ # overloaded for the _ChLV.forward.__doc__
853
+ return Ltp.forward(self, latlonh, lon=lon, height=height, M=M, name=name)
854
+
855
+ def reverse(self, enh_, n=None, h_=0, M=None, **name): # PYCHOK signature
856
+ # overloaded for the _ChLV.reverse.__doc__
857
+ Y, X, h_, name = self._YXh_n4(enh_, n, h_, **name)
858
+ return Ltp.reverse(self, Y, X, h_, M=M, name=name)
859
+
860
+ @staticmethod
861
+ def false2(Y, X, LV95=True, name=NN):
862
+ '''Add the I{Swiss LV95} or I{LV03} falsing.
863
+
864
+ @arg Y: Unfalsed I{Swiss Y} easting (C{meter}).
865
+ @arg X: Unfalsed I{Swiss X} northing (C{meter}).
866
+ @kwarg LV95: If C{True} add C{LV95} falsing, if C{False} add
867
+ C{LV03} falsing, otherwise leave unfalsed.
868
+ @kwarg name: Optional name (C{str}).
869
+
870
+ @return: A L{ChLVEN2Tuple}C{(E_LV95, N_LV95)} or a
871
+ L{ChLVyx2Tuple}C{(y_LV03, x_LV03)} with falsed B{C{Y}}
872
+ and B{C{X}}, otherwise a L{ChLVYX2Tuple}C{(Y, X)}
873
+ with B{C{Y}} and B{C{X}} as-is.
874
+ '''
875
+ e, n = t = _ChLV._falsing2(LV95)
876
+ return t.classof(e + Y, n + X, name=name)
877
+
878
+ @staticmethod
879
+ def isLV03(e, n):
880
+ '''Is C{(B{e}, B{n})} a valid I{Swiss LV03} projection?
881
+
882
+ @arg e: Falsed (or unfalsed) I{Swiss} easting (C{meter}).
883
+ @arg n: Falsed (or unfalsed) I{Swiss} northing (C{meter}).
884
+
885
+ @return: C{True} if C{(B{e}, B{n})} is a valid, falsed I{Swiss
886
+ LV03}, projection C{False} otherwise.
887
+ '''
888
+ # @see: U{Map<https://www.SwissTopo.admin.CH/en/knowledge-facts/
889
+ # surveying-geodesy/reference-frames/local/lv95.html>}
890
+ return 400.0e3 < e < 900.0e3 and 40.0e3 < n < 400.0e3
891
+
892
+ @staticmethod
893
+ def isLV95(e, n, raiser=True):
894
+ '''Is C{(B{e}, B{n})} a valid I{Swiss LV95} or I{LV03} projection?
895
+
896
+ @arg e: Falsed (or unfalsed) I{Swiss} easting (C{meter}).
897
+ @arg n: Falsed (or unfalsed) I{Swiss} northing (C{meter}).
898
+ @kwarg raiser: If C{True}, throw a L{LocalError} if B{C{e}} and
899
+ B{C{n}} are invalid I{Swiss LV95} nor I{LV03}.
900
+
901
+ @return: C{True} or C{False} if C{(B{e}, B{n})} is a valid I{Swiss
902
+ LV95} respectively I{LV03} projection, C{None} otherwise.
903
+ '''
904
+ if ChLV.isLV03(e, n):
905
+ return False
906
+ elif ChLV.isLV03(e - 2.0e6, n - 1.0e6): # _92_falsing = _95_ - _03_
907
+ return True
908
+ elif raiser: # PYCHOK no cover
909
+ raise LocalError(unstr(ChLV.isLV95, e=e, n=n))
910
+ return None
911
+
912
+ @staticmethod
913
+ def unfalse2(e, n, LV95=None, name=NN):
914
+ '''Remove the I{Swiss LV95} or I{LV03} falsing.
915
+
916
+ @arg e: Falsed I{Swiss E_LV95} or I{y_LV03} easting (C{meter}).
917
+ @arg n: Falsed I{Swiss N_LV95} or I{x_LV03} northing (C{meter}).
918
+ @kwarg LV95: If C{True} remove I{LV95} falsing, if C{False} remove
919
+ I{LV03} falsing, otherwise use method C{isLV95(B{e},
920
+ B{n})}.
921
+ @kwarg name: Optional name (C{str}).
922
+
923
+ @return: A L{ChLVYX2Tuple}C{(Y, X)} with the unfalsed B{C{e}}
924
+ respectively B{C{n}}.
925
+ '''
926
+ Y, X = _ChLV._falsing2(ChLV.isLV95(e, n) if LV95 is None else LV95)
927
+ return ChLVYX2Tuple(e - Y, n - X, name=name)
928
+
929
+
930
+ class ChLVa(_ChLV, LocalCartesian):
931
+ '''Conversion between I{WGS84 geodetic} and I{Swiss} projection coordinates
932
+ using the U{Approximate<https://www.SwissTopo.admin.CH/en/maps-data-online/
933
+ calculation-services.html>} formulas, page 13.
934
+
935
+ @see: Older U{references<https://GitHub.com/alphasldiallo/Swisstopo-WGS84-LV03>}.
936
+ '''
937
+ def __init__(self, name=ChLV.Bern.name):
938
+ '''New I{Approximate WGS84-Swiss} L{ChLVa} converter, centered at I{Bern, Ch}.
939
+
940
+ @kwarg name: Optional name (C{str}), overriding C{Bern.name}.
941
+ '''
942
+ LocalCartesian.__init__(self, latlonh0=ChLV.Bern, name=name)
943
+
944
+ def forward(self, latlonh, lon=None, height=0, M=None, name=NN):
945
+ # overloaded for the _ChLV.forward.__doc__
946
+ lat, lon, h, name = _llhn4(latlonh, lon, height, name=name)
947
+ a, b, h_ = _ChLV._llh2abh_3(lat, lon, h)
948
+ a2, b2 = a**2, b**2
949
+
950
+ Y = fsumf_( 72.37, 211455.93 * b,
951
+ -10938.51 * b * a,
952
+ -0.36 * b * a2,
953
+ -44.54 * b * b2) # + 600_000
954
+ X = fsumf_(147.07, 308807.95 * a,
955
+ 3745.25 * b2,
956
+ 76.63 * a2,
957
+ -194.56 * b2 * a,
958
+ 119.79 * a2 * a) # + 200_000
959
+ return self._ChLV9Tuple(True, M, name, Y, X, h_, lat, lon, h)
960
+
961
+ def reverse(self, enh_, n=None, h_=0, M=None, **name): # PYCHOK signature
962
+ # overloaded for the _ChLV.reverse.__doc__
963
+ Y, X, h_, name = self._YXh_n4(enh_, n, h_, **name)
964
+ a, b, h = _ChLV._YXh_2abh3(Y, X, h_)
965
+ ab_d, a2, b2 = ChLV._ab_d, a**2, b**2
966
+
967
+ lat = Fsum(16.9023892, 3.238272 * b,
968
+ -0.270978 * a2,
969
+ -0.002528 * b2,
970
+ -0.0447 * a2 * b,
971
+ -0.014 * b2 * b).fover(ab_d)
972
+ lon = Fsum( 2.6779094, 4.728982 * a,
973
+ 0.791484 * a * b,
974
+ 0.1306 * a * b2,
975
+ -0.0436 * a * a2).fover(ab_d)
976
+ return self._ChLV9Tuple(False, M, name, Y, X, h_, lat, lon, h)
977
+
978
+
979
+ class ChLVe(_ChLV, LocalCartesian):
980
+ '''Conversion between I{WGS84 geodetic} and I{Swiss} projection coordinates
981
+ using the U{Ellipsoidal approximate<https://www.SwissTopo.admin.CH/en/
982
+ maps-data-online/calculation-services.html>} formulas, pp 10-11 and U{Bolliger,
983
+ J.<https://eMuseum.GGGS.CH/literatur-lv/liste-Dateien/1967_Bolliger_a.pdf>}
984
+ pp 148-151 (also U{GGGS<https://eMuseum.GGGS.CH/literatur-lv/liste.htm>}).
985
+
986
+ @note: Methods L{ChLVe.forward} and L{ChLVe.reverse} have an additional keyword
987
+ argument C{B{gamma}=False} to approximate the I{meridian convergence}.
988
+ If C{B{gamma}=True} a 2-tuple C{(t, gamma)} is returned with C{t} the
989
+ usual result (C{ChLV9Tuple}) and C{gamma}, the I{meridian convergence}
990
+ (decimal C{degrees}). To convert C{gamma} to C{grades} or C{gons},
991
+ use function L{pygeodesy.degrees2grades}.
992
+
993
+ @see: Older U{references<https://GitHub.com/alphasldiallo/Swisstopo-WGS84-LV03>}.
994
+ '''
995
+ def __init__(self, name=ChLV.Bern.name):
996
+ '''New I{Approximate WGS84-Swiss} L{ChLVe} converter, centered at I{Bern, Ch}.
997
+
998
+ @kwarg name: Optional name (C{str}), overriding C{Bern.name}.
999
+ '''
1000
+ LocalCartesian.__init__(self, latlonh0=ChLV.Bern, name=name)
1001
+
1002
+ def forward(self, latlonh, lon=None, height=0, M=None, name=NN, gamma=False): # PYCHOK gamma
1003
+ # overloaded for the _ChLV.forward.__doc__
1004
+ lat, lon, h, name = _llhn4(latlonh, lon, height, name=name)
1005
+ a, b, h_ = _ChLV._llh2abh_3(lat, lon, h)
1006
+ ab_M, z, _H = ChLV._ab_M, 0, Fhorner
1007
+
1008
+ B1 = _H(a, 211428.533991, -10939.608605, -2.658213, -8.539078, -0.00345, -0.007992)
1009
+ B3 = _H(a, -44.232717, 4.291740, -0.309883, 0.013924)
1010
+ B5 = _H(a, 0.019784, -0.004277)
1011
+ Y = _H(b, z, B1, z, B3, z, B5).fover(ab_M) # 1,000 Km!
1012
+
1013
+ B0 = _H(a, z, 308770.746371, 75.028131, 120.435227, 0.009488, 0.070332, -0.00001)
1014
+ B2 = _H(a, 3745.408911, -193.792705, 4.340858, -0.376174, 0.004053)
1015
+ B4 = _H(a, -0.734684, 0.144466, -0.011842)
1016
+ B6 = 0.000488
1017
+ X = _H(b, B0, z, B2, z, B4, z, B6).fover(ab_M) # 1,000 Km!
1018
+
1019
+ t = self._ChLV9Tuple(True, M, name, Y, X, h_, lat, lon, h)
1020
+ if gamma:
1021
+ U1 = _H(a, 2255515.207166, 2642.456961, 1.284180, 2.577486, 0.001165)
1022
+ U3 = _H(a, -412.991934, 64.106344, -2.679566, 0.123833)
1023
+ U5 = _H(a, 0.204129, -0.037725)
1024
+ g = _H(b, z, U1, z, U3, z, U5).fover(ChLV._ab_m) # * ChLV._ab_d degrees?
1025
+ t = t, g
1026
+ return t
1027
+
1028
+ def reverse(self, enh_, n=None, h_=0, M=None, name=NN, gamma=False): # PYCHOK gamma
1029
+ # overloaded for the _ChLV.reverse.__doc__
1030
+ Y, X, h_, name = self._YXh_n4(enh_, n, h_, name=name)
1031
+ a, b, h = _ChLV._YXh_2abh3(Y, X, h_)
1032
+ s_d, _H, z = ChLV._s_d, Fhorner, 0
1033
+
1034
+ A0 = _H(b, ChLV._sLat, 32386.4877666, -25.486822, -132.457771, 0.48747, 0.81305, -0.0069)
1035
+ A2 = _H(b, -2713.537919, -450.442705, -75.53194, -14.63049, -2.7604)
1036
+ A4 = _H(b, 24.42786, 13.20703, 4.7476)
1037
+ A6 = -0.4249
1038
+ lat = _H(a, A0, z, A2, z, A4, z, A6).fover(s_d)
1039
+
1040
+ A1 = _H(b, 47297.3056722, 7925.714783, 1328.129667, 255.02202, 48.17474, 9.0243)
1041
+ A3 = _H(b, -442.709889, -255.02202, -96.34947, -30.0808)
1042
+ A5 = _H(b, 9.63495, 9.0243)
1043
+ lon = _H(a, ChLV._sLon, A1, z, A3, z, A5).fover(s_d)
1044
+ # == (ChLV._sLon + a * (A1 + a**2 * (A3 + a**2 * A5))) / s_d
1045
+
1046
+ t = self._ChLV9Tuple(False, M, name, Y, X, h_, lat, lon, h)
1047
+ if gamma:
1048
+ U1 = _H(b, 106679.792202, 17876.57022, 4306.5241, 794.87772, 148.1545, 27.8725)
1049
+ U3 = _H(b, -1435.508, -794.8777, -296.309, -92.908)
1050
+ U5 = _H(b, 29.631, 27.873)
1051
+ g = _H(a, z, U1, z, U3, z, U5).fover(ChLV._s_ab) # degrees
1052
+ t = t, g
1053
+ return t
1054
+
1055
+
1056
+ def tyr3d(tilt=INT0, yaw=INT0, roll=INT0, Vector=Vector3d, **Vector_kwds):
1057
+ '''Convert an attitude oriention into a (3-D) direction vector.
1058
+
1059
+ @kwarg tilt: Pitch, elevation from horizontal (C{degrees}), negative down
1060
+ (clockwise rotation along and around the x-axis).
1061
+ @kwarg yaw: Bearing, heading (compass C{degrees360}), clockwise from North
1062
+ (counter-clockwise rotation along and around the z-axis).
1063
+ @kwarg roll: Roll, bank (C{degrees}), positive to the right and down
1064
+ (clockwise rotation along and around the y-axis).
1065
+
1066
+ @return: A named B{C{Vector}} instance or if B{C{Vector}} is C{None},
1067
+ a named L{Vector3Tuple}C{(x, y, z)}.
1068
+
1069
+ @see: U{Yaw, pitch, and roll rotations<http://MSL.CS.UIUC.edu/planning/node102.html>}
1070
+ and function L{pygeodesy.hartzell} argument C{los}.
1071
+ '''
1072
+ d = Attitude4Tuple(_0_0, tilt, yaw, roll).tyr3d
1073
+ return d if Vector is type(d) else (
1074
+ Vector3Tuple(d.x, d.y, d.z, name=d.name) if Vector is None else
1075
+ Vector(d.x, d.y, d.z, **_xkwds(Vector_kwds, name=d.name))) # PYCHOK indent
1076
+
1077
+
1078
+ def _xLtp(ltp, *dflt):
1079
+ '''(INTERNAL) Validate B{C{ltp}}.
1080
+ '''
1081
+ if dflt and ltp is None:
1082
+ ltp = dflt[0]
1083
+ if isinstance(ltp, (LocalCartesian, Ltp)):
1084
+ return ltp
1085
+ raise _TypesError(_ltp_, ltp, Ltp, LocalCartesian)
1086
+
1087
+ # **) MIT License
1088
+ #
1089
+ # Copyright (C) 2016-2024 -- mrJean1 at Gmail -- All Rights Reserved.
1090
+ #
1091
+ # Permission is hereby granted, free of charge, to any person obtaining a
1092
+ # copy of this software and associated documentation files (the "Software"),
1093
+ # to deal in the Software without restriction, including without limitation
1094
+ # the rights to use, copy, modify, merge, publish, distribute, sublicense,
1095
+ # and/or sell copies of the Software, and to permit persons to whom the
1096
+ # Software is furnished to do so, subject to the following conditions:
1097
+ #
1098
+ # The above copyright notice and this permission notice shall be included
1099
+ # in all copies or substantial portions of the Software.
1100
+ #
1101
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
1102
+ # OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
1103
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
1104
+ # THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
1105
+ # OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
1106
+ # ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
1107
+ # OTHER DEALINGS IN THE SOFTWARE.