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/dms.py ADDED
@@ -0,0 +1,986 @@
1
+
2
+ # -*- coding: utf-8 -*-
3
+
4
+ u'''Parsers and formatters of angles in degrees, minutes and seconds or radians.
5
+
6
+ Functions to parse and format bearing, compass, lat- and longitudes in various forms of
7
+ degrees, minutes and seconds with or without degrees, minute and second symbols plus a
8
+ compass point suffix, including parsing of C{decimal} and C{sexagecimal} degrees.
9
+
10
+ Set env variable C{PYGEODESY_FMT_FORM} to any C{F_...} form to override default C{F_DMS}
11
+ formatting of lat- and longitudes or to an empty string to restore the default.
12
+
13
+ After I{(C) Chris Veness 2011-2015} published under the same MIT Licence**, see
14
+ U{Latitude/Longitude<https://www.Movable-Type.co.UK/scripts/latlong.html>} and
15
+ U{Vector-based geodesy<https://www.Movable-Type.co.UK/scripts/latlong-vectors.html>}.
16
+
17
+ @var F_D: Format degrees as unsigned "deg°" with symbol, plus compass point suffix C{N, S, E} or C{W} (C{str}).
18
+ @var F_DM: Format degrees as unsigned "deg°min′" with symbols, plus suffix (C{str}).
19
+ @var F_DMS: Format degrees as unsigned "deg°min′sec″" with symbols, plus suffix (C{str}).
20
+ @var F_DEG: Format degrees as unsigned "[D]DD" I{without} symbol, plus suffix (C{str}).
21
+ @var F_MIN: Format degrees as unsigned "[D]DDMM" I{without} symbols, plus suffix (C{str}).
22
+ @var F_SEC: Format degrees as unsigned "[D]DDMMSS" I{without} symbols, plus suffix (C{str}).
23
+ @var F_D60: Format degrees as unsigned "[D]DD.MMSS" C{sexagecimal} I{without} symbols, plus suffix (C{str}).
24
+ @var F__E: Format degrees as unsigned "%E" I{without} symbols, plus suffix (C{str}).
25
+ @var F__F: Format degrees as unsigned "%F" I{without} symbols, plus suffix (C{str}).
26
+ @var F__G: Format degrees as unsigned "%G" I{without} symbols, plus suffix (C{str}).
27
+ @var F_RAD: Convert degrees to radians and format as unsigned "RR" with symbol, plus suffix (C{str}).
28
+
29
+ @var F_D_: Format degrees as signed "-/deg°" with symbol, I{without} suffix (C{str}).
30
+ @var F_DM_: Format degrees as signed "-/deg°min′" with symbols, I{without} suffix (C{str}).
31
+ @var F_DMS_: Format degrees as signed "-/deg°min′sec″" with symbols, I{without} suffix (C{str}).
32
+ @var F_DEG_: Format degrees as signed "-/[D]DD" I{without} symbol, I{without} suffix (C{str}).
33
+ @var F_MIN_: Format degrees as signed "-/[D]DDMM" I{without} symbols, I{without} suffix (C{str}).
34
+ @var F_SEC_: Format degrees as signed "-/[D]DDMMSS" I{without} symbols, I{without} suffix (C{str}).
35
+ @var F_D60_: Format degrees as signed "-/[D]DD.MMSS" C{sexagecimal} I{without} symbols, I{without} suffix (C{str}).
36
+ @var F__E_: Format degrees as signed "-/%E" I{without} symbols, I{without} suffix (C{str}).
37
+ @var F__F_: Format degrees as signed "-/%F" I{without} symbols, I{without} suffix (C{str}).
38
+ @var F__G_: Format degrees as signed "-/%G" I{without} symbols, I{without} suffix (C{str}).
39
+ @var F_RAD_: Convert degrees to radians and format as signed "-/RR" I{without} symbol, I{without} suffix (C{str}).
40
+
41
+ @var F_D__: Format degrees as signed "-/+deg°" with symbol, I{without} suffix (C{str}).
42
+ @var F_DM__: Format degrees as signed "-/+deg°min′" with symbols, I{without} suffix (C{str}).
43
+ @var F_DMS__: Format degrees as signed "-/+deg°min′sec″" with symbols, I{without} suffix (C{str}).
44
+ @var F_DEG__: Format degrees as signed "-/+[D]DD" I{without} symbol, I{without} suffix (C{str}).
45
+ @var F_MIN__: Format degrees as signed "-/+[D]DDMM" I{without} symbols, without suffix (C{str}).
46
+ @var F_SEC__: Format degrees as signed "-/+[D]DDMMSS" I{without} symbols, I{without} suffix (C{str}).
47
+ @var F_D60__: Format degrees as signed "-/+[D]DD.MMSS" C{sexagecimal} I{without} symbols, I{without} suffix (C{str}).
48
+ @var F__E__: Format degrees as signed "-/+%E" I{without} symbols, I{without} suffix (C{str}).
49
+ @var F__F__: Format degrees as signed "-/+%F" I{without} symbols, I{without} suffix (C{str}).
50
+ @var F__G__: Format degrees as signed "-/+%G" I{without} symbols, I{without} suffix (C{str}).
51
+ @var F_RAD__: Convert degrees to radians and format as signed "-/+RR" I{without} symbol, I{without} suffix (C{str}).
52
+
53
+ @var S_DEG: Degrees symbol, default C{"°"}
54
+ @var S_MIN: Minutes symbol, default C{"′"} aka I{PRIME}
55
+ @var S_SEC: Seconds symbol, default C{"″"} aka I{DOUBLE_PRIME}
56
+ @var S_RAD: Radians symbol, default C{""} aka L{pygeodesy.NN}
57
+ @var S_DMS: If C{True} include, otherwise cancel all DMS symbols, default C{True}.
58
+ @var S_SEP: Separator between C{deg°|min′|sec″|suffix}, default C{""} aka L{pygeodesy.NN}
59
+
60
+ @note: In Python 2-, L{S_DEG}, L{S_MIN}, L{S_SEC}, L{S_RAD} and L{S_SEP} may be multi-byte,
61
+ non-ascii characters and if so, I{not} C{unicode}.
62
+ '''
63
+
64
+ from pygeodesy.basics import copysign0, isLatLon, isodd, issequence, isstr, map2, \
65
+ neg as _neg # in .ups
66
+ from pygeodesy.constants import _umod_360, _0_0, _0_5, _60_0, _360_0, _3600_0
67
+ from pygeodesy.errors import ParseError, RangeError, _TypeError, _ValueError,\
68
+ _parseX, rangerrors, _xkwds, _xkwds_get
69
+ from pygeodesy.interns import NN, _arg_, _COMMA_, _d_, _DASH_, _deg_, _degrees_, _DOT_, \
70
+ _0_, _e_, _E_, _EW_, _f_, _F_, _g_, _MINUS_, _N_, _NE_, _NS_, \
71
+ _NSEW_, _NW_, _of_, _PERCENTDOTSTAR_, _PLUS_, _PLUSMINUS_, \
72
+ _QUOTE1_, _QUOTE2_, _radians_, _S_, _SE_, _SPACE_, _SW_, _W_
73
+ from pygeodesy.lazily import _ALL_LAZY, _ALL_MODS as _MODS, _getenv
74
+ # from pygeodesy.namedTuples import LatLon2Tuple # _MODS
75
+ # from pygeodesy.props import _throwarning # _MODS
76
+ from pygeodesy.streprs import Fmt, fstr, fstrzs, _0wpF
77
+ # from pygeodesy.units import Precision_ # _MODS
78
+ # from pygeodesy.utily import _Wrap # _MODS
79
+
80
+ from math import fabs, modf, radians
81
+ try:
82
+ from string import letters as _LETTERS
83
+ except ImportError: # Python 3+
84
+ from string import ascii_letters as _LETTERS
85
+
86
+ __all__ = _ALL_LAZY.dms
87
+ __version__ = '24.03.21'
88
+
89
+ _beyond_ = 'beyond'
90
+ _DDDMMSS_ = 'DDDMMSS'
91
+ _deg_min_ = 'deg+min'
92
+ _keyword_ = 'keyword'
93
+ _SDIGITS_ = '-0123456789+'
94
+ _sexagecimal_ = 'sexagecimal'
95
+ _SEXAGECIMUL = 1.e4 # sexagecimal C{D.MMSSss} into decimal C{DMMSS.ss}
96
+
97
+ F_D, F_DM, F_DMS, F_DEG, F_MIN, F_SEC, F_D60, F__E, F__F, F__G, F_RAD = _F_s = (
98
+ _d_, 'dm', 'dms', _deg_, 'min', 'sec', 'd60', _e_, _f_, _g_, 'rad')
99
+ F_D_, F_DM_, F_DMS_, F_DEG_, F_MIN_, F_SEC_, F_D60_, F__E_, F__F_, F__G_, F_RAD_ = (NN(
100
+ _MINUS_, _) for _ in _F_s)
101
+ F_D__, F_DM__, F_DMS__, F_DEG__, F_MIN__, F_SEC__, F_D60__, F__E__, F__F__, F__G__, F_RAD__ = (NN(
102
+ _PLUS_, _) for _ in _F_s)
103
+ del _F_s
104
+ _F_DMS = _getenv('PYGEODESY_FMT_FORM', NN) or F_DMS
105
+
106
+ _F_case = {F_D: F_D, F_DEG: F_D, _degrees_: F_D, # unsigned _F_s
107
+ F_DM: F_DM, F_MIN: F_DM, _deg_min_: F_DM,
108
+ F_D60: F_D60, F_RAD: F_RAD, _radians_: F_RAD,
109
+ F__E: F__E, F__F: F__F, F__G: F__G} # default F_DMS
110
+ _F_prec = {F_D: 6, F_DM: 4, F_DMS: 2, # default precs
111
+ F_DEG: 6, F_MIN: 4, F_SEC: 2, F_D60: 0,
112
+ F__E: 8, F__F: 8, F__G: 8, F_RAD: 5}
113
+ _F_symb = set((F_D, F_DM, F_DMS, _deg_min_)) # == {} pychok -Tb
114
+
115
+ S_DEG = _DEGREES_ = '°' # ord() = 176
116
+ S_MIN = _MINUTES_ = '′' # PRIME
117
+ S_SEC = _SECONDS_ = '″' # DOUBLE_PRIME
118
+ S_RAD = _RADIANS_ = NN # PYCHOK radians symbol ""
119
+ S_DMS = True # include DMS symbols
120
+ S_SEP = NN # separator between deg|min|sec|suffix ""
121
+ S_NUL = NN # empty string, kept INTERNAL
122
+
123
+ # note: ord(_DEGREES_) == ord('°') == 176, ord('˚') == 730
124
+ _S_norm = {S_DEG: _DEGREES_, '˚': _DEGREES_, '^': _DEGREES_, # _d_: _DEGREES_,
125
+ S_MIN: _MINUTES_, '’': _MINUTES_, _QUOTE1_: _MINUTES_, # _r_: _RADIANS_
126
+ S_SEC: _SECONDS_, '”': _SECONDS_, _QUOTE2_: _SECONDS_}
127
+
128
+ _WINDS = (_N_, 'NbE', 'NNE', 'NEbN', _NE_, 'NEbE', 'ENE', 'EbN',
129
+ _E_, 'EbS', 'ESE', 'SEbE', _SE_, 'SEbS', 'SSE', 'SbE',
130
+ _S_, 'SbW', 'SSW', 'SWbS', _SW_, 'SWbW', 'WSW', 'WbS',
131
+ _W_, 'WbN', 'WNW', 'NWbW', _NW_, 'NWbN', 'NNW', 'NbW')
132
+
133
+
134
+ def _D603(sep, s_D=_DOT_, s_M=None, s_S=S_NUL, s_DMS=S_DMS, **unused):
135
+ '''(INTERNAL) Get the overridden or default pseudo-C{DMS} symbols.
136
+ '''
137
+ if s_DMS:
138
+ M = sep if s_M is None else s_M
139
+ return s_D, (M or S_NUL), s_S
140
+ else: # no overriden symbols
141
+ return _DOT_, sep, S_NUL
142
+
143
+
144
+ def _DMS3(form, s_D=S_DEG, s_M=S_MIN, s_S=S_SEC, s_DMS=S_DMS, **unused):
145
+ '''(INTERNAL) Get the overridden or default C{DMS} symbols.
146
+ '''
147
+ return (s_D, s_M, s_S) if s_DMS and form in _F_symb else (S_NUL, S_NUL, S_NUL)
148
+
149
+
150
+ def _dms3(d, ddd, p, w):
151
+ '''(INTERNAL) Format C{d} as (deg, min, sec) C{str}s with leading zeros.
152
+ '''
153
+ d, s = divmod(round(d * _3600_0, p), _3600_0)
154
+ m, s = divmod(s, _60_0)
155
+ return (_0wpF(ddd, 0, d),
156
+ _0wpF( 2, 0, m),
157
+ _0wpF(w+2, p, s))
158
+
159
+
160
+ def _fstrzs(t, **unused):
161
+ '''(INTERNAL) Pass-thru version of C{.streprs.fstrzs}.
162
+ '''
163
+ return t
164
+
165
+
166
+ def _split3(strDMS, suffix=_NSEW_):
167
+ '''(INTERNAL) Return sign, stripped B{C{strDMS}} and compass point.
168
+ '''
169
+ t = strDMS.strip()
170
+ s = t[:1] # sign or digit
171
+ P = t[-1:] # compass point or digit or dot
172
+ t = t.lstrip(_PLUSMINUS_).rstrip(suffix).strip()
173
+ return s, t, P
174
+
175
+
176
+ def _toDMS(deg, form, prec, sep, ddd, suff, s_D_M_S): # MCCABE 13 in .units
177
+ '''(INTERNAL) Convert C{deg} to C{str}, with/-out sign, DMS symbols and/or suffix.
178
+ '''
179
+ f = form
180
+ try:
181
+ deg = float(deg)
182
+ except (TypeError, ValueError) as x:
183
+ raise _ValueError(deg=deg, form=f, prec=prec, cause=x)
184
+
185
+ if f[:1] in _PLUSMINUS_: # signed
186
+ sign = _MINUS_ if deg < 0 else (
187
+ _PLUS_ if deg > 0 and f[:1] == _PLUS_ else NN)
188
+ f = f.lstrip(_PLUSMINUS_)
189
+ suff = NN # no suffix if signed
190
+ else: # suffixed
191
+ sign = NN # no sign if suffixed
192
+ if suff and sep: # no sep if no suffix
193
+ suff = NN(sep, suff)
194
+ try:
195
+ F = _F_case[f] # .strip()
196
+ except KeyError:
197
+ f = f.lower() # .strip()
198
+ F = _F_case.get(f, F_DMS)
199
+
200
+ if prec is None:
201
+ z = p = _F_prec.get(F, 6)
202
+ else:
203
+ z = int(prec)
204
+ p = abs(z)
205
+ w = p + (1 if p else 0)
206
+ z = fstrzs if z > 1 else _fstrzs
207
+ d = fabs(deg)
208
+
209
+ try:
210
+ if F is F_DMS: # 'deg+min+sec', default
211
+ D, M, S = _DMS3(f, **s_D_M_S)
212
+ d, m, s = _dms3(d, ddd, p, w)
213
+ t = NN(sign, d, D, sep,
214
+ m, M, sep,
215
+ z(s), S, suff)
216
+
217
+ elif F is F_DM: # 'deg+min'
218
+ D, M, _ = _DMS3(f, **s_D_M_S)
219
+ d, m = divmod(round(d * _60_0, p), _60_0)
220
+ t = NN(sign, _0wpF(ddd, 0, d), D, sep,
221
+ z(_0wpF(w+2, p, m)), M, suff)
222
+
223
+ elif F is F_D: # 'deg'
224
+ D, _, _ = _DMS3(f, **s_D_M_S)
225
+ t = NN(sign, z(_0wpF(w+ddd, p, d)), D, suff)
226
+
227
+ elif F is F_D60: # 'deg.MM|SSss|'
228
+ D, M, S = _D603(sep, **s_D_M_S)
229
+ d, m, s = _dms3(d, ddd, p, w)
230
+ t = z(s).split(_DOT_) + [S, suff]
231
+ t = NN(sign, d, D, m, M, *t)
232
+
233
+ elif F is F_RAD:
234
+ R = _xkwds_get(s_D_M_S, s_R=S_RAD)
235
+ r = NN(_PERCENTDOTSTAR_, _F_) % (p, radians(d))
236
+ t = NN(sign, z(r), R, suff)
237
+
238
+ else: # F in (F__E, F__F, F__G)
239
+ D = _xkwds_get(s_D_M_S, s_D=S_NUL)
240
+ d = NN(_PERCENTDOTSTAR_, F) % (p, d) # XXX f?
241
+ t = NN(sign, z(d, ap1z=F is F__G), D, suff)
242
+
243
+ except Exception as x:
244
+ raise _ValueError(deg=deg, form=form, F=F, prec=prec, suff=suff, cause=x)
245
+
246
+ return t # NOT unicode in Python 2-
247
+
248
+
249
+ def bearingDMS(bearing, form=F_D, prec=None, sep=S_SEP, **s_D_M_S):
250
+ '''Convert bearing to a string (without compass point suffix).
251
+
252
+ @arg bearing: Bearing from North (compass C{degrees360}).
253
+ @kwarg form: Format specifier for B{C{deg}} (C{str} or L{F_D},
254
+ L{F_DM}, L{F_DMS}, L{F_DEG}, L{F_MIN}, L{F_SEC},
255
+ L{F_D60}, L{F__E}, L{F__F}, L{F__G}, L{F_RAD},
256
+ L{F_D_}, L{F_DM_}, L{F_DMS_}, L{F_DEG_}, L{F_MIN_},
257
+ L{F_SEC_}, L{F_D60_}, L{F__E_}, L{F__F_}, L{F__G_},
258
+ L{F_RAD_}, L{F_D__}, L{F_DM__}, L{F_DMS__}, L{F_DEG__},
259
+ L{F_MIN__}, L{F_SEC__}, L{F_D60__}, L{F__E__},
260
+ L{F__F__}, L{F__G__} or L{F_RAD__}).
261
+ @kwarg prec: Number of decimal digits (0..9 or C{None} for default).
262
+ Trailing zero decimals are stripped for B{C{prec}}
263
+ values of 1 and above, but kept for negative B{C{prec}}.
264
+ @kwarg sep: Separator between degrees, minutes, seconds, suffix (C{str}).
265
+ @kwarg s_D_M_S: Optional keyword arguments C{B{s_D}=str}, C{B{s_M}=str},
266
+ C{B{s_S}=str} and C{B{s_DMS}=True} to override any or
267
+ cancel all DMS symbols, defaults L{S_DEG}, L{S_MIN}
268
+ respectively L{S_SEC}.
269
+
270
+ @return: Compass degrees per the specified B{C{form}} (C{str}).
271
+
272
+ @see: Function L{pygeodesy.toDMS}.
273
+ '''
274
+ return _toDMS(_umod_360(bearing), form, prec, sep, 1, NN, s_D_M_S)
275
+
276
+
277
+ def _clip(angle, limit, units):
278
+ '''(INTERNAL) Helper for C{clipDegrees} and C{clipRadians}.
279
+ '''
280
+ c = min(limit, max(-limit, angle))
281
+ if c != angle and rangerrors():
282
+ t = _SPACE_(fstr(angle, prec=6, ints=True), _beyond_,
283
+ copysign0(limit, angle), units)
284
+ raise RangeError(t, txt=None)
285
+ return c
286
+
287
+
288
+ def clipDegrees(deg, limit):
289
+ '''Clip a lat- or longitude to the given range.
290
+
291
+ @arg deg: Unclipped lat- or longitude (C{scalar degrees}).
292
+ @arg limit: Valid C{-/+B{limit}} range (C{degrees}).
293
+
294
+ @return: Clipped value (C{degrees}).
295
+
296
+ @raise RangeError: If B{C{deg}} outside the valid C{-/+B{limit}}
297
+ range and L{pygeodesy.rangerrors} set to C{True}.
298
+ '''
299
+ return _clip(deg, limit, _degrees_) if limit and limit > 0 else deg
300
+
301
+
302
+ def clipRadians(rad, limit):
303
+ '''Clip a lat- or longitude to the given range.
304
+
305
+ @arg rad: Unclipped lat- or longitude (C{radians}).
306
+ @arg limit: Valid C{-/+B{limit}} range (C{radians}).
307
+
308
+ @return: Clipped value (C{radians}).
309
+
310
+ @raise RangeError: If B{C{rad}} outside the valid C{-/+B{limit}}
311
+ range and L{pygeodesy.rangerrors} set to C{True}.
312
+ '''
313
+ return _clip(rad, limit, _radians_) if limit and limit > 0 else rad
314
+
315
+
316
+ def compassDMS(bearing, form=F_D, prec=None, sep=S_SEP, **s_D_M_S):
317
+ '''Convert bearing to a string suffixed with compass point.
318
+
319
+ @arg bearing: Bearing from North (compass C{degrees360}).
320
+ @kwarg form: Format specifier for B{C{deg}} (C{str} or L{F_D},
321
+ L{F_DM}, L{F_DMS}, L{F_DEG}, L{F_MIN}, L{F_SEC},
322
+ L{F_D60}, L{F__E}, L{F__F}, L{F__G}, L{F_RAD},
323
+ L{F_D_}, L{F_DM_}, L{F_DMS_}, L{F_DEG_}, L{F_MIN_},
324
+ L{F_SEC_}, L{F_D60_}, L{F__E_}, L{F__F_}, L{F__G_},
325
+ L{F_RAD_}, L{F_D__}, L{F_DM__}, L{F_DMS__}, L{F_DEG__},
326
+ L{F_MIN__}, L{F_SEC__}, L{F_D60__}, L{F__E__},
327
+ L{F__F__}, L{F__G__} or L{F_RAD__}).
328
+ @kwarg prec: Number of decimal digits (0..9 or C{None} for default).
329
+ Trailing zero decimals are stripped for B{C{prec}}
330
+ values of 1 and above, but kept for negative B{C{prec}}.
331
+ @kwarg sep: Separator between degrees, minutes, seconds, suffix (C{str}).
332
+ @kwarg s_D_M_S: Optional keyword arguments C{B{s_D}=str}, C{B{s_M}=str}
333
+ C{B{s_S}=str} and C{B{s_DMS}=True} to override any or
334
+ cancel all DMS symbols, defaults L{S_DEG}, L{S_MIN}
335
+ respectively L{S_SEC}.
336
+
337
+ @return: Compass degrees and point in the specified form (C{str}).
338
+
339
+ @see: Function L{pygeodesy.toDMS}.
340
+ '''
341
+ b = _umod_360(bearing)
342
+ return _toDMS(b, form, prec, sep, 1, compassPoint(b), s_D_M_S)
343
+
344
+
345
+ def compassPoint(bearing, prec=3):
346
+ '''Convert a C{bearing} from North to a compass point.
347
+
348
+ @arg bearing: Bearing (compass C{degrees360}).
349
+ @kwarg prec: Precision, number of compass point characters:
350
+ 1 for cardinal or basic winds,
351
+ 2 for intercardinal or ordinal or principal winds,
352
+ 3 for secondary-intercardinal or half-winds or
353
+ 4 for quarter-winds).
354
+
355
+ @return: Compass point (1-, 2-, 3- or 4-letter C{str}).
356
+
357
+ @raise ValueError: Invalid B{C{bearing}} or B{C{prec}}.
358
+
359
+ @see: U{Dms.compassPoint
360
+ <https://GitHub.com/ChrisVeness/geodesy/blob/master/dms.js>}
361
+ and U{Compass rose<https://WikiPedia.org/wiki/Compass_rose>}.
362
+ '''
363
+ try: # like .streprs.enstr2
364
+ b = _umod_360(bearing)
365
+ p = _MODS.units.Precision_(prec, low=1, high=4) \
366
+ if prec != 3 else int(prec)
367
+ m = 2 << p
368
+ w = 32 // m # if m in (4, 8, 16, 32)
369
+ # not round(b), half-even rounding in Python 3+, but
370
+ # round-away-from-zero as int(b + copysign0(_0_5, b))
371
+ w *= int(b * m / _360_0 + _0_5) % m
372
+ return _WINDS[w]
373
+ except Exception as x:
374
+ raise _ValueError(bearing=bearing, prec=prec, cause=x)
375
+
376
+
377
+ def degDMS(deg, prec=6, s_D=S_DEG, s_M=S_MIN, s_S=S_SEC, neg=_MINUS_, pos=NN):
378
+ '''Convert degrees to a string in degrees, minutes I{or} seconds.
379
+
380
+ @arg deg: Value in degrees (C{scalar degrees}).
381
+ @kwarg prec: Number of decimal digits (0..9 or C{None} for default).
382
+ Trailing zero decimals are stripped for B{C{prec}}
383
+ values of 1 and above, but kept for negative B{C{prec}}.
384
+ @kwarg s_D: D symbol for degrees (C{str}).
385
+ @kwarg s_M: M symbol for minutes (C{str}) or C{""}.
386
+ @kwarg s_S: S symbol for seconds (C{str}) or C{""}.
387
+ @kwarg neg: Optional sign for negative (C{'-'}).
388
+ @kwarg pos: Optional sign for positive (C{''}).
389
+
390
+ @return: I{Either} degrees, minutes I{or} seconds (C{str}).
391
+
392
+ @see: Function L{pygeodesy.toDMS}.
393
+ '''
394
+ try:
395
+ deg = float(deg)
396
+ except (TypeError, ValueError) as x:
397
+ raise _ValueError(deg=deg, prec=prec, cause=x)
398
+
399
+ d, s = fabs(deg), s_D
400
+ if d < 1:
401
+ if s_M:
402
+ d *= _60_0
403
+ if d < 1 and s_S:
404
+ d *= _60_0
405
+ s = s_S
406
+ else:
407
+ s = s_M
408
+ elif s_S:
409
+ d *= _3600_0
410
+ s = s_S
411
+
412
+ z = int(prec)
413
+ t = Fmt.F(d, prec=abs(z))
414
+ if z > 1:
415
+ t = fstrzs(t)
416
+ n = neg if deg < 0 else pos
417
+ return NN(n, t, s) # NOT unicode in Python 2-
418
+
419
+
420
+ def latDMS(deg, form=_F_DMS, prec=None, sep=S_SEP, **s_D_M_S):
421
+ '''Convert latitude to a string, optionally suffixed with N or S.
422
+
423
+ @arg deg: Latitude to be formatted (C{scalar degrees}).
424
+ @kwarg form: Format specifier for B{C{deg}} (C{str} or L{F_D},
425
+ L{F_DM}, L{F_DMS}, L{F_DEG}, L{F_MIN}, L{F_SEC},
426
+ L{F_D60}, L{F__E}, L{F__F}, L{F__G}, L{F_RAD},
427
+ L{F_D_}, L{F_DM_}, L{F_DMS_}, L{F_DEG_}, L{F_MIN_},
428
+ L{F_SEC_}, L{F_D60_}, L{F__E_}, L{F__F_}, L{F__G_},
429
+ L{F_RAD_}, L{F_D__}, L{F_DM__}, L{F_DMS__}, L{F_DEG__},
430
+ L{F_MIN__}, L{F_SEC__}, L{F_D60__}, L{F__E__},
431
+ L{F__F__}, L{F__G__} or L{F_RAD__}).
432
+ @kwarg prec: Number of decimal digits (0..9 or C{None} for default).
433
+ Trailing zero decimals are stripped for B{C{prec}}
434
+ values of 1 and above, but kept for negative B{C{prec}}.
435
+ @kwarg sep: Separator between degrees, minutes, seconds, suffix (C{str}).
436
+ @kwarg s_D_M_S: Optional keyword arguments C{B{s_D}=str}, C{B{s_M}=str}
437
+ C{B{s_S}=str} and C{B{s_DMS}=True} to override any or
438
+ cancel all DMS symbols, defaults L{S_DEG}, L{S_MIN}
439
+ respectively L{S_SEC}.
440
+
441
+ @return: Degrees in the specified form (C{str}).
442
+
443
+ @see: Functions L{pygeodesy.toDMS} and L{pygeodesy.lonDMS}.
444
+ '''
445
+ p = _S_ if deg < 0 else _N_
446
+ return _toDMS(deg, form, prec, sep, 2, p, s_D_M_S)
447
+
448
+
449
+ def latlonDMS(lls, **m_form_prec_sep_s_D_M_S):
450
+ '''Convert one or more C{LatLon} instances to strings.
451
+
452
+ @arg lls: Single (C{LatLon}) or list, sequence, tuple, etc. (C{LatLon}s).
453
+ @kwarg m_form_prec_sep_s_D_M_S: Optional keyword arguments C{B{m}eter},
454
+ C{B{form}at}, C{B{prec}ision}, B{C{s_D}}, B{C{s_M}}, B{C{s_S}},
455
+ B{C{s_DMS}} and I{DEPRECATED} C{B{sep}=None}, see method
456
+ C{LatLon.toStr} and functions L{pygeodesy.latDMS} and
457
+ L{pygeodesy.lonDMS} for more details.
458
+
459
+ @return: A C{tuple} of C{str}s if B{C{lls}} is a list, sequence,
460
+ tuple, etc. of C{LatLon} instances or a single C{str}
461
+ if B{C{lls}} is a single C{LatLon}.
462
+
463
+ @see: Functions L{pygeodesy.latlonDMS_}, L{pygeodesy.latDMS},
464
+ L{pygeodesy.lonDMS} and L{pygeodesy.toDMS} and method
465
+ C{LatLon.toStr}.
466
+
467
+ @note: Keyword argument C{B{sep}=None} to join a C{str}ing
468
+ from the returned C{tuple} has been I{DEPRECATED},
469
+ use C{B{sep}.join(B{latlonDMS_}(...))} instead.
470
+ '''
471
+ sep, kwds = _latlonDMS_sep2(latlonDMS, **m_form_prec_sep_s_D_M_S)
472
+ if isLatLon(lls):
473
+ t = lls.toStr(**kwds)
474
+ elif issequence(lls):
475
+ t = tuple(ll.toStr(**kwds) for ll in lls)
476
+ if sep: # XXX TO BE REMOVED
477
+ t = sep.join(t)
478
+ else:
479
+ raise _TypeError(lls=lls, **m_form_prec_sep_s_D_M_S)
480
+ return t
481
+
482
+
483
+ def latlonDMS_(*lls, **m_form_prec_sep_s_D_M_S):
484
+ '''Convert one or more C{LatLon} instances to strings.
485
+
486
+ @arg lls: The instances (C{LatLon}s), all positional arguments.
487
+ @kwarg m_form_prec_sep_s_D_M_S: Optional keyword arguments
488
+ C{B{m}eter}, C{B{form}at}, C{B{prec}ision}, B{C{s_D}},
489
+ B{C{s_M}}, B{C{s_S}}, B{C{s_DMS}} and I{DEPRECATED}
490
+ C{B{sep}=None}, see method C{LatLon.toStr} and
491
+ functions L{pygeodesy.latDMS} and L{pygeodesy.lonDMS}
492
+ for more details.
493
+
494
+ @return: A C{tuple} of C{str}s if 2 or more C{LatLon} instances
495
+ or a single C{str} if only a single C{LatLon} instance
496
+ is given in B{C{lls}}.
497
+
498
+ @see: Functions L{pygeodesy.latlonDMS}, L{pygeodesy.latDMS} and
499
+ L{pygeodesy.lonDMS} and L{pygeodesy.toDMS} and method
500
+ C{LatLon.toStr}.
501
+
502
+ @note: Keyword argument C{B{sep}=None} to join a C{str}ing
503
+ from the returned C{tuple} has been I{DEPRECATED},
504
+ use C{B{sep}.join(B{latlonDMS_}(...))} instead.
505
+ '''
506
+ sep, kwds = _latlonDMS_sep2(latlonDMS, **m_form_prec_sep_s_D_M_S)
507
+ if not lls:
508
+ raise _ValueError(lls=lls, **m_form_prec_sep_s_D_M_S)
509
+ elif len(lls) < 2:
510
+ lls, sep = lls[0], None
511
+ t = latlonDMS(lls, **kwds)
512
+ return sep.join(t) if sep else t
513
+
514
+
515
+ def _latlonDMS_sep2(where, sep=None, **kwds):
516
+ '''DEPRECATED, instead use: %r.join(%s(...))'''
517
+ if sep:
518
+ k = _SPACE_(_keyword_, _arg_, Fmt.EQUAL(sep=repr(sep)), _of_)
519
+ n = where.__name__
520
+ t = _latlonDMS_sep2.__doc__ % (sep, n)
521
+ _MODS.props._throwarning(k, n, t)
522
+ return sep, kwds
523
+
524
+
525
+ def lonDMS(deg, form=_F_DMS, prec=None, sep=S_SEP, **s_D_M_S):
526
+ '''Convert longitude to a string, optionally suffixed with E or W.
527
+
528
+ @arg deg: Longitude to be formatted (C{scalar degrees}).
529
+ @kwarg form: Format specifier for B{C{deg}} (C{str} or L{F_D},
530
+ L{F_DM}, L{F_DMS}, L{F_DEG}, L{F_MIN}, L{F_SEC},
531
+ L{F_D60}, L{F__E}, L{F__F}, L{F__G}, L{F_RAD},
532
+ L{F_D_}, L{F_DM_}, L{F_DMS_}, L{F_DEG_}, L{F_MIN_},
533
+ L{F_SEC_}, L{F_D60_}, L{F__E_}, L{F__F_}, L{F__G_},
534
+ L{F_RAD_}, L{F_D__}, L{F_DM__}, L{F_DMS__}, L{F_DEG__},
535
+ L{F_MIN__}, L{F_SEC__}, L{F_D60__}, L{F__E__},
536
+ L{F__F__}, L{F__G__} or L{F_RAD__}).
537
+ @kwarg prec: Number of decimal digits (0..9 or C{None} for default).
538
+ Trailing zero decimals are stripped for B{C{prec}}
539
+ values of 1 and above, but kept for negative B{C{prec}}.
540
+ @kwarg sep: Separator between degrees, minutes, seconds, suffix (C{str}).
541
+ @kwarg s_D_M_S: Optional keyword arguments C{B{s_D}=str}, C{B{s_M}=str}
542
+ C{B{s_S}=str} and C{B{s_DMS}=True} to override any or
543
+ cancel all DMS symbols, defaults L{S_DEG}, L{S_MIN}
544
+ respectively L{S_SEC}.
545
+
546
+ @return: Degrees in the specified form (C{str}).
547
+
548
+ @see: Functions L{pygeodesy.toDMS} and L{pygeodesy.latDMS}.
549
+ '''
550
+ p = _W_ if deg < 0 else _E_
551
+ return _toDMS(deg, form, prec, sep, 3, p, s_D_M_S)
552
+
553
+
554
+ def normDMS(strDMS, norm=None, **s_D_M_S):
555
+ '''Normalize all degrees, minutes and seconds (DMS) I{symbols} in
556
+ a string to the default symbols L{S_DEG}, L{S_MIN}, L{S_SEC}.
557
+
558
+ @arg strDMS: Original DMS string (C{str}).
559
+ @kwarg norm: Optional replacement symbol (C{str}) or C{None} for
560
+ the default DMS symbols). Use C{B{norm}=""} to
561
+ remove all DMS symbols.
562
+ @kwarg s_D_M_S: Optional, alternate DMS symbols C{B{s_D}=str},
563
+ C{B{s_M}=str}, C{B{s_S}=str} and/or C{B{s_R}=str}
564
+ for radians, each to be replaced by B{C{norm}}.
565
+
566
+ @return: Normalized DMS (C{str}).
567
+ '''
568
+ def _s2S2(s_D=S_DEG, s_M=S_MIN, s_S=S_SEC, s_R=S_RAD):
569
+ d = {s_D: S_DEG, s_M: S_MIN, s_S: S_SEC, s_R: S_RAD}
570
+ for s, S in _xkwds(d, **_S_norm).items():
571
+ if s:
572
+ yield s, S
573
+
574
+ # XXX strDMS isn't unicode in Python 2- and looping
575
+ # thru strDMS will yield each byte, hence the loop
576
+ # thru _s2S2 and replacing the DMS symbols in strDMS
577
+
578
+ if norm is None: # back to default DMS
579
+ for s, S in _s2S2(**s_D_M_S):
580
+ if s != S:
581
+ strDMS = strDMS.replace(s, S)
582
+
583
+ else: # replace or remove all DMS
584
+ n = norm or NN
585
+ for s, _ in _s2S2(**s_D_M_S):
586
+ if s != n:
587
+ strDMS = strDMS.replace(s, n)
588
+ if n:
589
+ strDMS = strDMS.rstrip(n) # XXX not .strip?
590
+
591
+ return strDMS # NOT unicode in Python 2-
592
+
593
+
594
+ def parseDDDMMSS(strDDDMMSS, suffix=_NSEW_, sep=S_SEP, clip=0, sexagecimal=False): # MCCABE 14
595
+ '''Parse a lat- or longitude represention forms as [D]DDMMSS in degrees.
596
+
597
+ @arg strDDDMMSS: Degrees in any of several forms (C{str}) and types (C{float},
598
+ C{int}, other).
599
+ @kwarg suffix: Optional, valid compass points (C{str}, C{tuple}).
600
+ @kwarg sep: Optional separator between "[D]DD", "MM", "SS", B{C{suffix}} (L{S_SEP}).
601
+ @kwarg clip: Optionally, limit value to range C{-/+B{clip}} (C{degrees}).
602
+ @kwarg sexagecimal: If C{True}, convert C{"D.MMSS"} or C{float(D.MMSS)} to
603
+ C{base-60} "MM" and "SS" digits. See C{form}s L{F_D60},
604
+ L{F_D60_} and L{F_D60__}.
605
+
606
+ @return: Degrees (C{float}).
607
+
608
+ @raise ParseError: Invalid B{C{strDDDMMSS}} or B{C{clip}} or the form of
609
+ B{C{strDDDMMSS}} is incompatible with the suffixed or
610
+ B{C{suffix}} compass point.
611
+
612
+ @raise RangeError: Value of B{C{strDDDMMSS}} outside the valid C{-/+B{clip}}
613
+ range and L{pygeodesy.rangerrors} set to C{True}.
614
+
615
+ @note: Type C{str} values "[D]DD", "[D]DDMM", "[D]DDMMSS" and "[D]DD.MMSS"
616
+ for B{C{strDDDMMSS}} are parsed properly only if I{either} unsigned
617
+ and suffixed with a valid, compatible, C{cardinal} L{compassPoint}
618
+ I{or} signed I{or} unsigned, unsuffixed and with keyword argument
619
+ B{C{suffix}="NS"}, B{C{suffix}="EW"} or a compatible L{compassPoint}.
620
+
621
+ @note: Unlike function L{parseDMS}, type C{float}, C{int} and other non-C{str}
622
+ B{C{strDDDMMSS}} values are interpreted as C{form} [D]DDMMSS or
623
+ [D]DD.MMSS. For example, C{int(1230)} is returned as 12.5 and I{not
624
+ 1230.0} degrees. However, C{int(345)} is considered C{form} "DDD"
625
+ 345 I{and not "DDMM" 0345}, unless B{C{suffix}} specifies the compass
626
+ point. Also, C{float(15.0523)} is returned as 15.0523 decimal
627
+ degrees and I{not 15°5′23″ sexagecimal}. To consider the latter, use
628
+ C{float(15.0523)} or C{"15.0523"} and specify the keyword argument
629
+ C{B{sexagecimal}=True}.
630
+
631
+ @see: Functions L{pygeodesy.parseDMS}, L{pygeodesy.parseDMS2} and
632
+ L{pygeodesy.parse3llh}.
633
+ '''
634
+ def _DDDMMSS(strDDDMMSS, suffix, sep, clip, sexagecimal):
635
+ S = suffix.upper()
636
+ if isstr(strDDDMMSS):
637
+ t = strDDDMMSS.replace(sep, NN) if sep else strDDDMMSS
638
+ s, t, P = _split3(t, S)
639
+ f = t.split(_DOT_)
640
+ n = len(f[0])
641
+ f = NN.join(f)
642
+ if 1 < n < 8 and f.isdigit() and ( # dddN/S/E/W or ddd or +/-ddd
643
+ (P in S and s.isdigit()) or
644
+ (P.isdigit() and s in _SDIGITS_ # PYCHOK indent
645
+ and S in _WINDS)):
646
+ # check [D]DDMMSS form and compass point
647
+ X = _EW_ if isodd(n) else _NS_
648
+ if not (P in X or (S in X and (P.isdigit() or P == _DOT_))):
649
+ t = _DDDMMSS_[int(X is _NS_):(n | 1)], _DASH_.join(X)
650
+ raise ParseError('form %s applies %s' % t)
651
+ elif not sexagecimal: # try other forms
652
+ return _DMS2deg(strDDDMMSS, S, sep, clip, {})
653
+
654
+ if sexagecimal: # move decimal dot from ...
655
+ n += 4 # ... [D]DD.MMSSs to [D]DDMMSS.s
656
+ if n < 6:
657
+ raise ParseError('%s digits (%s)' % (_sexagecimal_, n))
658
+ z = n - len(f) # zeros to append
659
+ t = (f + (_0_ * z)) if z > 0 else _DOT_(f[:n], f[n:])
660
+ f = _0_0 # fraction
661
+
662
+ else: # float or int to [D]DDMMSS[.fff]
663
+ f, m = float(strDDDMMSS), 0
664
+ if sexagecimal:
665
+ f *= _SEXAGECIMUL
666
+ m = 6
667
+ s = P = _0_ # anything except NN, _S_, _SW_, _W_
668
+ if f < 0:
669
+ f = -f
670
+ s = _MINUS_
671
+ f, i = modf(f) # returns ...
672
+ t = str(int(i)) # ... float(i)
673
+ n = len(t) # number of digits to ...
674
+ if n < m: # ... required min or ...
675
+ t = (_0_ * (m - n)) + t
676
+ # ... match the given compass point
677
+ elif S in (_NS_ if isodd(n) else _EW_):
678
+ t = _0_ + t
679
+ # P = S
680
+ # elif n > 1:
681
+ # P = (_EW_ if isodd(n) else _NS_)[0]
682
+ n = len(t)
683
+
684
+ if n < 4: # [D]DD[.ddd]
685
+ t = (float(t) + f),
686
+ else:
687
+ f += float(t[n-2:])
688
+ if n < 6: # [D]DDMM[.mmm]
689
+ t = float(t[:n-2]), f
690
+ else: # [D]DDMMSS[.sss]
691
+ t = float(t[:n-4]), float(t[n-4:n-2]), f
692
+ d = _dms2deg(s, P, *t)
693
+ return clipDegrees(d, float(clip)) if clip else d
694
+
695
+ return _parseX(_DDDMMSS, strDDDMMSS, suffix, sep, clip, sexagecimal,
696
+ strDDDMMSS=strDDDMMSS, suffix=suffix, sexagecimal=sexagecimal)
697
+
698
+
699
+ def _dms2deg(s, P, deg, min=_0_0, sec=_0_0):
700
+ '''(INTERNAL) Helper for C{parseDDDMMSS} and C{_DMS2deg}.
701
+ '''
702
+ deg += (min + (sec / _60_0)) / _60_0
703
+ if s == _MINUS_ or (P and P in _SW_):
704
+ deg = _neg(deg)
705
+ return deg
706
+
707
+
708
+ def _DMS2deg(strDMS, suffix, sep, clip, s_D_M_S):
709
+ '''(INTERNAL) Helper for C{parseDDDMMSS} and C{parseDMS}.
710
+ '''
711
+ try:
712
+ d = float(strDMS)
713
+
714
+ except (TypeError, ValueError):
715
+ s, t, P = _split3(strDMS, suffix.upper())
716
+ if sep: # remove all DMS symbols
717
+ t = t.replace(sep, _SPACE_)
718
+ t = normDMS(t, norm=NN, **s_D_M_S)
719
+ else: # replace all DMS symbols
720
+ t = normDMS(t, norm=_SPACE_, **s_D_M_S)
721
+ t = map2(float, t.strip().split())
722
+ d = _dms2deg(s, P, *t[:3])
723
+
724
+ return clipDegrees(d, float(clip)) if clip else d
725
+
726
+
727
+ def parseDMS(strDMS, suffix=_NSEW_, sep=S_SEP, clip=0, **s_D_M_S): # MCCABE 14
728
+ '''Parse a lat- or longitude representation in C{degrees}.
729
+
730
+ This is very flexible on formats, allowing signed decimal
731
+ degrees, degrees and minutes or degrees minutes and seconds
732
+ optionally suffixed by a cardinal compass point.
733
+
734
+ A variety of symbols, separators and suffixes are accepted,
735
+ for example "3°37′09″W". Minutes and seconds may be omitted.
736
+
737
+ @arg strDMS: Degrees in any of several forms (C{str}) and
738
+ types (C{float}, C{int}, other).
739
+ @kwarg suffix: Optional, valid compass points (C{str}, C{tuple}).
740
+ @kwarg sep: Optional separator between deg°, min′, sec″, B{C{suffix}} (C{''}).
741
+ @kwarg clip: Optionally, limit value to range C{-/+B{clip}} (C{degrees}).
742
+ @kwarg s_D_M_S: Optional, alternate symbol for degrees C{B{s_D}=str},
743
+ minutes C{B{s_M}=str} and/or seconds C{B{s_S}=str}.
744
+
745
+ @return: Degrees (C{float}).
746
+
747
+ @raise ParseError: Invalid B{C{strDMS}} or B{C{clip}}.
748
+
749
+ @raise RangeError: Value of B{C{strDMS}} outside the valid C{-/+B{clip}}
750
+ range and L{pygeodesy.rangerrors} set to C{True}.
751
+
752
+ @note: Unlike function L{parseDDDMMSS}, type C{float}, C{int} and other
753
+ non-C{str} B{C{strDMS}} values are considered decimal (and not
754
+ sexagecimal) degrees. For example, C{int(1230)} is returned
755
+ as 1230.0 I{and not as 12.5} degrees and C{float(345)} as 345.0
756
+ I{and not as 3.75} degrees!
757
+
758
+ @see: Functions L{pygeodesy.parseDDDMMSS}, L{pygeodesy.parseDMS2},
759
+ L{pygeodesy.parse3llh} and L{pygeodesy.toDMS}.
760
+ '''
761
+ return _parseX(_DMS2deg, strDMS, suffix, sep, clip, s_D_M_S, strDMS=strDMS, suffix=suffix)
762
+
763
+
764
+ def parseDMS2(strLat, strLon, sep=S_SEP, clipLat=90, clipLon=180, wrap=False, **s_D_M_S):
765
+ '''Parse a lat- and a longitude representions C{"lat, lon"} in C{degrees}.
766
+
767
+ @arg strLat: Latitude in any of several forms (C{str} or C{degrees}).
768
+ @arg strLon: Longitude in any of several forms (C{str} or C{degrees}).
769
+ @kwarg sep: Optional separator between deg°, min′, sec″, suffix (C{''}).
770
+ @kwarg clipLat: Limit latitude to range C{-/+B{clipLat}} (C{degrees}).
771
+ @kwarg clipLon: Limit longitude to range C{-/+B{clipLon}} (C{degrees}).
772
+ @kwarg wrap: If C{True}, wrap or I{normalize} the lat- and longitude,
773
+ overriding B{C{clipLat}} and B{C{clipLon}} (C{bool}).
774
+ @kwarg s_D_M_S: Optional, alternate symbol for degrees C{B{s_D}=str},
775
+ minutes C{B{s_M}=str} and/or seconds C{B{s_S}=str}.
776
+
777
+ @return: A L{LatLon2Tuple}C{(lat, lon)} in C{degrees}.
778
+
779
+ @raise ParseError: Invalid B{C{strLat}} or B{C{strLon}}.
780
+
781
+ @raise RangeError: Value of B{C{strLat}} or B{C{strLon}} outside the
782
+ valid C{-/+B{clipLat}} or C{-/+B{clipLon}} range
783
+ and L{pygeodesy.rangerrors} set to C{True}.
784
+
785
+ @note: See the B{Notes} at function L{parseDMS}.
786
+
787
+ @see: Functions L{pygeodesy.parseDDDMMSS}, L{pygeodesy.parseDMS},
788
+ L{pygeodesy.parse3llh} and L{pygeodesy.toDMS}.
789
+ '''
790
+ return _2Tuple(strLat, strLon, clipLat, clipLon, wrap, sep=sep, **s_D_M_S)
791
+
792
+
793
+ def _2Tuple(strLat, strLon, clipLat, clipLon, wrap, **kwds):
794
+ '''(INTERNAL) Helper for C{parseDMS2} and C{parse3llh}.
795
+ '''
796
+ if wrap:
797
+ _W = _MODS.utily._Wrap
798
+ lat, lon = _W.latlon(parseDMS(strLat, suffix=_NS_, **kwds),
799
+ parseDMS(strLon, suffix=_EW_, **kwds))
800
+ else:
801
+ # if wrap is None:
802
+ # clipLat = clipLon = 0
803
+ lat = parseDMS(strLat, suffix=_NS_, clip=clipLat, **kwds)
804
+ lon = parseDMS(strLon, suffix=_EW_, clip=clipLon, **kwds)
805
+ return _MODS.namedTuples.LatLon2Tuple(lat, lon)
806
+
807
+
808
+ def parse3llh(strllh, height=0, sep=_COMMA_, clipLat=90, clipLon=180, wrap=False, **s_D_M_S):
809
+ '''Parse a string C{"lat, lon [, h]"} representing lat-, longitude in
810
+ C{degrees} and optional height in C{meter}.
811
+
812
+ The lat- and longitude value must be separated by a separator
813
+ character. If height is present it must follow, separated by
814
+ another separator.
815
+
816
+ The lat- and longitude values may be swapped, provided at least
817
+ one ends with the proper compass point.
818
+
819
+ @arg strllh: Latitude, longitude[, height] (C{str}, ...).
820
+ @kwarg height: Optional, default height (C{meter}) or C{None}.
821
+ @kwarg sep: Optional separator between C{"lat lon [h] suffix"} (C{str}).
822
+ @kwarg clipLat: Limit latitude to range C{-/+B{clipLat}} (C{degrees}).
823
+ @kwarg clipLon: Limit longitude to range C{-/+B{clipLon}} (C{degrees}).
824
+ @kwarg wrap: If C{True}, wrap or I{normalize} the lat- and longitude,
825
+ overriding B{C{clipLat}} and B{C{clipLon}} (C{bool}).
826
+ @kwarg s_D_M_S: Optional, alternate symbol for degrees C{B{s_D}=str},
827
+ minutes C{B{s_M}=str} and/or seconds C{B{s_S}=str}.
828
+
829
+ @return: A L{LatLon3Tuple}C{(lat, lon, height)} in C{degrees},
830
+ C{degrees} and C{float}.
831
+
832
+ @raise RangeError: Lat- or longitude value of B{C{strllh}} outside
833
+ the valid C{-/+B{clipLat}} or C{-/+B{clipLon}}
834
+ range and L{pygeodesy.rangerrors} set to C{True}.
835
+
836
+ @raise ValueError: Invalid B{C{strllh}} or B{C{height}}.
837
+
838
+ @note: See the B{Notes} at function L{parseDMS}.
839
+
840
+ @see: Functions L{pygeodesy.parseDDDMMSS}, L{pygeodesy.parseDMS},
841
+ L{pygeodesy.parseDMS2} and L{pygeodesy.toDMS}.
842
+ '''
843
+
844
+ def _3llh(strllh, height, sep, wrap):
845
+ ll = strllh.strip().split(sep)
846
+ if len(ll) > 2: # XXX interpret height unit
847
+ h = float(ll.pop(2).rstrip(_LETTERS + _SPACE_))
848
+ else:
849
+ h = height # None from wgrs.Georef.__new__
850
+ if len(ll) != 2:
851
+ raise ValueError
852
+
853
+ a, b = [_.strip() for _ in ll] # PYCHOK false
854
+ if a[-1:] in _EW_ or b[-1:] in _NS_:
855
+ a, b = b, a
856
+ return _2Tuple(a, b, clipLat, clipLon, wrap, **s_D_M_S).to3Tuple(h)
857
+
858
+ return _parseX(_3llh, strllh, height, sep, wrap, strllh=strllh)
859
+
860
+
861
+ def parseRad(strRad, suffix=_NSEW_, clip=0):
862
+ '''Parse a string representing angle in C{radians}.
863
+
864
+ @arg strRad: Degrees in any of several forms (C{str} or C{radians}).
865
+ @kwarg suffix: Optional, valid compass points (C{str}, C{tuple}).
866
+ @kwarg clip: Optionally, limit value to range C{-/+B{clip}} (C{radians}).
867
+
868
+ @return: Radians (C{float}).
869
+
870
+ @raise ParseError: Invalid B{C{strRad}} or B{C{clip}}.
871
+
872
+ @raise RangeError: Value of B{C{strRad}} outside the valid C{-/+B{clip}}
873
+ range and L{pygeodesy.rangerrors} set to C{True}.
874
+ '''
875
+ def _Rad(strRad, suffix, clip):
876
+ try:
877
+ r = float(strRad)
878
+
879
+ except (TypeError, ValueError):
880
+ s, t, P = _split3(strRad, suffix.upper())
881
+ r = _dms2deg(s, P, float(t))
882
+
883
+ return clipRadians(r, float(clip)) if clip else r
884
+
885
+ return _parseX(_Rad, strRad, suffix, clip, strRad=strRad, suffix=suffix)
886
+
887
+
888
+ def precision(form, prec=None):
889
+ '''Set the default precison for a given F_ form.
890
+
891
+ @arg form: L{F_D}, L{F_DM}, L{F_DMS}, L{F_DEG}, L{F_MIN},
892
+ L{F_SEC}, L{F_D60}, L{F__E}, L{F__F}, L{F__G}
893
+ or L{F_RAD} (C{str}).
894
+ @kwarg prec: Number of decimal digits (0..9 or C{None} for
895
+ default). Trailing zero decimals are stripped
896
+ for B{C{prec}} values of 1 and above, but kept
897
+ for negative B{C{prec}}.
898
+
899
+ @return: Previous precision for the B{C{form}} (C{int}).
900
+
901
+ @raise ValueError: Invalid B{C{form}} or B{C{prec}} or B{C{prec}}
902
+ outside range C{-9..+9}.
903
+ '''
904
+ try:
905
+ p = _F_prec[form]
906
+ except KeyError:
907
+ raise _ValueError(form=form)
908
+
909
+ if prec is not None: # set as default
910
+ _F_prec[form] = _MODS.units.Precision_(prec, low=-9, high=9)
911
+
912
+ return p
913
+
914
+
915
+ def toDMS(deg, form=_F_DMS, prec=2, sep=S_SEP, ddd=2, neg=_MINUS_, pos=_PLUS_, **s_D_M_S):
916
+ '''Convert I{signed} C{degrees} to string, without suffix.
917
+
918
+ @arg deg: Degrees to be formatted (C{scalar degrees}).
919
+ @kwarg form: Format specifier for B{C{deg}} (C{str} or L{F_D},
920
+ L{F_DM}, L{F_DMS}, L{F_DEG}, L{F_MIN}, L{F_SEC},
921
+ L{F_D60}, L{F__E}, L{F__F}, L{F__G}, L{F_RAD},
922
+ L{F_D_}, L{F_DM_}, L{F_DMS_}, L{F_DEG_}, L{F_MIN_},
923
+ L{F_SEC_}, L{F_D60_}, L{F__E_}, L{F__F_}, L{F__G_},
924
+ L{F_RAD_}, L{F_D__}, L{F_DM__}, L{F_DMS__}, L{F_DEG__},
925
+ L{F_MIN__}, L{F_SEC__}, L{F_D60__}, L{F__E__},
926
+ L{F__F__}, L{F__G__} or L{F_RAD__}).
927
+ @kwarg prec: Number of decimal digits (0..9 or C{None} for default).
928
+ Trailing zero decimals are stripped for B{C{prec}}
929
+ values of 1 and above, but kept for negative B{C{prec}}.
930
+ @kwarg sep: Separator between degrees, minutes, seconds, suffix (C{str}).
931
+ @kwarg ddd: Number of digits for B{C{deg}°} (2 or 3).
932
+ @kwarg neg: Prefix for negative B{C{deg}} (C{'-'}).
933
+ @kwarg pos: Prefix for positive B{C{deg}} and signed B{C{form}} (C{'+'}).
934
+ @kwarg s_D_M_S: Optional keyword arguments C{B{s_D}=str}, C{B{s_M}=str}
935
+ C{B{s_S}=str} and C{B{s_DMS}=True} to override any or
936
+ cancel all DMS symbols, defaults L{S_DEG}, L{S_MIN}
937
+ respectively L{S_SEC}. See B{Notes} below.
938
+
939
+ @return: Degrees in the specified form (C{str}).
940
+
941
+ @note: The degrees, minutes and seconds (DMS) symbol can be overridden in
942
+ this and other C{*DMS} functions by using optional keyword argments
943
+ C{B{s_D}="d"}, C{B{s_M}="'"} respectively C{B{s_S}='"'}. Using
944
+ keyword argument B{C{s_DMS}=None} cancels all C{DMS} symbols to
945
+ C{B{S_NUL}=NN}.
946
+
947
+ @note: Sexagecimal format B{C{F_D60}} supports overridable pseudo-DMS symbols
948
+ positioned at C{"[D]DD<B{s_D}>MM<B{s_M}>SS<B{s_S}>"} with defaults
949
+ C{B{s_D}="."}, C{B{s_M}=B{sep}} and C{B{s_S}=}L{pygeodesy.NN}.
950
+
951
+ @note: Formats B{C{F__E}}, B{C{F__F}} and B{C{F__G}} can be extended with
952
+ a C{D}-only symbol if defined with keyword argument C{B{s_D}=str}.
953
+ Likewise for B{C{F_RAD}} formats with keyword argument C{B{s_R}=str}.
954
+
955
+ @see: Function L{pygeodesy.degDMS}
956
+ '''
957
+ s = form[:1]
958
+ f = form[1:] if s in _PLUSMINUS_ else form
959
+ t = _toDMS(deg, f, prec, sep, ddd, NN, s_D_M_S) # unsigned and -suffixed
960
+ if deg < 0 and neg:
961
+ t = neg + t
962
+ elif deg > 0 and s == _PLUS_ and pos:
963
+ t = pos + t
964
+ return t
965
+
966
+ # **) MIT License
967
+ #
968
+ # Copyright (C) 2016-2024 -- mrJean1 at Gmail -- All Rights Reserved.
969
+ #
970
+ # Permission is hereby granted, free of charge, to any person obtaining a
971
+ # copy of this software and associated documentation files (the "Software"),
972
+ # to deal in the Software without restriction, including without limitation
973
+ # the rights to use, copy, modify, merge, publish, distribute, sublicense,
974
+ # and/or sell copies of the Software, and to permit persons to whom the
975
+ # Software is furnished to do so, subject to the following conditions:
976
+ #
977
+ # The above copyright notice and this permission notice shall be included
978
+ # in all copies or substantial portions of the Software.
979
+ #
980
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
981
+ # OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
982
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
983
+ # THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
984
+ # OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
985
+ # ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
986
+ # OTHER DEALINGS IN THE SOFTWARE.