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