1
2# -*- coding: utf-8 -*-
3
4u'''Parsers and formatters of degrees, minutes and seconds.
5
6Functions to parse and format bearing, compass, lat- and longitudes
7in various forms of degrees, minutes and seconds.
8
9After I{(C) Chris Veness 2011-2015} published under the same MIT Licence**, see
10U{Latitude/Longitude<https://www.Movable-Type.co.UK/scripts/latlong.html>} and
11U{Vector-based geodesy<https://www.Movable-Type.co.UK/scripts/latlong-vectors.html>}.
12'''
13
14from pygeodesy.basics import copysign0, isodd, issequence, isstr, map2, neg
15from pygeodesy.errors import ParseError, _parseX,  RangeError, \
16                            _rangerrors, _ValueError
17from pygeodesy.interns import _COMMA_, _NE_, _NSEW_, _NW_, _SE_  # PYCHOK used!
18from pygeodesy.interns import NN, _deg_, _degrees_, _DOT_, _e_, _E_, \
19                             _f_, _g_, _MINUS_, _PLUSMINUS_, _EW_, \
20                             _N_, _NS_, _PERCENTDOTSTAR_, _PLUS_, \
21                             _radians_, _S_, _SPACE_, _SW_, _W_, _0_, \
22                             _0_5, _60_0, _360_0, _3600_0
23from pygeodesy.lazily import _ALL_LAZY
24from pygeodesy.streprs import Fmt, fstr, fstrzs, _0wpF
25
26from math import modf, radians
27try:
28    from string import letters as _LETTERS
29except ImportError:  # Python 3+
30    from string import ascii_letters as _LETTERS
31
32__all__ = _ALL_LAZY.dms
33__version__ = '21.08.12'
34
35F_D   = 'd'    # unsigned format "deg°" plus suffix N, S, E or W
36F_DM  = 'dm'   # unsigned format "deg°min′" plus suffix
37F_DMS = 'dms'  # unsigned format "deg°min′sec″" plus suffix
38F_DEG = _deg_  # unsigned format "[D]DD" plus suffix without symbol
39F_MIN = 'min'  # unsigned format "[D]DDMM" plus suffix without symbols
40F_SEC = 'sec'  # unsigned format "[D]DDMMSS" plus suffix without symbols
41F__E  = _e_    # unsigned format "%E" plus suffix without symbol
42F__F  = _f_    # unsigned format "%F" plus suffix without symbol
43F__G  = _g_    # unsigned format "%G" plus suffix without symbol
44F_RAD = 'rad'  # convert degrees to radians and format unsigned "RR" plus suffix
45
46F_D_   = '-d'    # signed format "-/deg°" without suffix
47F_DM_  = '-dm'   # signed format "-/deg°min′" without suffix
48F_DMS_ = '-dms'  # signed format "-/deg°min′sec″" without suffix
49F_DEG_ = '-deg'  # signed format "-/[D]DD" without suffix and symbol
50F_MIN_ = '-min'  # signed format "-/[D]DDMM" without suffix and symbols
51F_SEC_ = '-sec'  # signed format "-/[D]DDMMSS" without suffix and symbols
52F__E_  = '-e'    # signed format "-%E" without suffix and symbol
53F__F_  = '-f'    # signed format "-%F" without suffix and symbol
54F__G_  = '-g'    # signed format "-%G" without suffix and symbol
55F_RAD_ = '-rad'  # convert degrees to radians and format as signed "-/RR" without suffix
56
57F_D__   = '+d'    # signed format "-/+deg°" without suffix
58F_DM__  = '+dm'   # signed format "-/+deg°min′" without suffix
59F_DMS__ = '+dms'  # signed format "-/+deg°min′sec″" without suffix
60F_DEG__ = '+deg'  # signed format "-/+[D]DD" without suffix and symbol
61F_MIN__ = '+min'  # signed format "-/+[D]DDMM" without suffix and symbols
62F_SEC__ = '+sec'  # signed format "-/+[D]DDMMSS" without suffix and symbols
63F__E__  = '+e'    # signed format "-/+%E" without suffix and symbol
64F__F__  = '+f'    # signed format "-/+%F" without suffix and symbol
65F__G__  = '+g'    # signed format "-/+%G" without suffix and symbol
66F_RAD__ = '+rad'  # convert degrees to radians and format signed "-/+RR" without suffix
67
68S_DEG = '°'  # degrees "°" symbol
69S_MIN = '′'  # minutes "′" symbol
70S_SEC = '″'  # seconds "″" symbol
71S_RAD = NN   # radians symbol ""
72S_SEP = NN   # separator between deg, min and sec ""
73S_NUL = NN   # empty string
74
75_F_case = {F_D:   F_D,  F_DEG: F_D,  _degrees_: F_D,
76           F_DM:  F_DM, F_MIN: F_DM, 'deg+min': F_DM,
77           F__E:  F__E, F__F:  F__F,  F__G:     F__G,
78           F_RAD: F_RAD,             _radians_: F_RAD}
79
80_F_prec = {F_D:   6, F_DM:  4, F_DMS: 2,  # default precs
81           F_DEG: 6, F_MIN: 4, F_SEC: 2,
82           F__E:  8, F__F:  8, F__G:  8, F_RAD: 5}
83
84_F_symb = {F_DEG, F_MIN, F_SEC, F__E, F__F, F__G}  # set({}) pychok -Tb
85
86_S_norm = {'^': S_DEG, '˚': S_DEG,  # normalized DMS
87           "'": S_MIN, '’': S_MIN, '′': S_MIN,
88           '"': S_SEC, '″': S_SEC, '”': S_SEC}
89_S_ALL  = (S_DEG, S_MIN, S_SEC) + tuple(_S_norm.keys())  # alternates
90
91
92def _toDMS(deg, form, prec, sep, ddd, suff):  # MCCABE 15 by .units.py
93    '''(INTERNAL) Convert degrees to C{str}, with/-out sign and/or suffix.
94    '''
95    try:
96        deg = float(deg)
97    except (TypeError, ValueError) as x:
98        raise _ValueError(deg=deg, txt=str(x))
99
100    form = form.lower()
101    sign = form[:1]
102    if sign in _PLUSMINUS_:
103        form = form[1:]
104    else:
105        sign = S_NUL
106
107    if prec is None:
108        z = p = _F_prec.get(form, 6)
109    else:
110        z = int(prec)
111        p = abs(z)
112    w = p + (1 if p else 0)
113    d = abs(deg)
114
115    if form in _F_symb:
116        s_deg = s_min = s_sec = S_NUL  # no symbols
117    else:
118        s_deg, s_min, s_sec = S_DEG, S_MIN, S_SEC
119
120    F = _F_case.get(form, F_DMS)
121    if F is F_DMS:  # 'deg+min+sec'
122        d, s = divmod(round(d * _3600_0, p), _3600_0)
123        m, s = divmod(s, _60_0)
124        t = NN(_0wpF(ddd, 0, d), s_deg, sep,
125               _0wpF(  2, 0, m), s_min, sep,
126               _0wpF(w+2, p, s))
127        s = s_sec
128
129    elif F is F_DM:  # 'deg+min'
130        d, m = divmod(round(d * _60_0, p), _60_0)
131        t = NN(_0wpF(ddd, 0, d), s_deg, sep,
132               _0wpF(w+2, p, m))
133        s = s_min
134
135    elif F is F_D:  # 'deg'
136        t = _0wpF(ddd+w, p, d)
137        s = s_deg
138
139    elif F is F_RAD:
140        t = NN(_PERCENTDOTSTAR_, 'F') % (p, radians(d))
141        s = S_RAD
142
143    else:  # F in (F__E, F__F, F__G)
144        t = NN(_PERCENTDOTSTAR_, F) % (p, d)
145        s = S_NUL
146
147    if z > 1:
148        t = fstrzs(t, ap1z=F is F__G)
149
150    if sign:
151        if deg < 0:
152            t = _MINUS_ + t
153        elif deg > 0 and sign == _PLUS_:
154            t = _PLUS_ + t
155    elif suff:  # and deg:  # zero suffix?
156        s += sep + suff
157    return t + s
158
159
160def bearingDMS(bearing, form=F_D, prec=None, sep=S_SEP):
161    '''Convert bearing to a string.
162
163       @arg bearing: Bearing from North (compass C{degrees360}).
164       @kwarg form: Optional B{C{bearing}} format (C{str} or L{F_D},
165                    L{F_DM}, L{F_DMS}, L{F_DEG}, L{F_MIN}, L{F_SEC},
166                    L{F__E}, L{F__F}, L{F__G}, L{F_RAD}, L{F_D_},
167                    L{F_DM_}, L{F_DMS_}, L{F_DEG_}, L{F_MIN_},
168                    L{F_SEC_}, L{F__E_}, L{F__F_}, L{F__G_}, L{F_RAD_},
169                    L{F_D__}, L{F_DM__}, L{F_DMS__}, L{F_DEG__},
170                    L{F_MIN__}, L{F_SEC__}, L{F__E__}, L{F__F__},
171                    L{F__G__} or L{F_RAD__}).
172       @kwarg prec: Optional number of decimal digits (0..9 or
173                    C{None} for default).  Trailing zero decimals
174                    are stripped for B{C{prec}} values of 1 and
175                    above, but kept for negative B{C{prec}}.
176       @kwarg sep: Optional separator (C{str}).
177
178       @return: Compass degrees per the specified B{C{form}} (C{str}).
179
180       @JSname: I{toBrng}.
181    '''
182    return _toDMS(bearing % _360_0, form, prec, sep, 1, NN)
183
184
185def _clipped_(angle, limit, units):
186    '''(INTERNAL) Helper for C{clipDegrees} and C{clipRadians}.
187    '''
188    c = min(limit, max(-limit, angle))
189    if c != angle and _rangerrors:
190        t = _SPACE_(fstr(angle, prec=6, ints=True),
191                   'beyond', copysign0(limit, angle), units)
192        raise RangeError(t, txt=None)
193    return c
194
195
196def clipDegrees(deg, limit):
197    '''Clip a lat- or longitude to the given range.
198
199       @arg deg: Unclipped lat- or longitude (C{degrees}).
200       @arg limit: Valid B{C{-limit..+limit}} range (C{degrees}).
201
202       @return: Clipped value (C{degrees}).
203
204       @raise RangeError: If B{C{abs(deg)}} beyond B{C{limit}} and
205                          L{rangerrors} set to C{True}.
206    '''
207    return _clipped_(deg, limit, _degrees_) if limit and limit > 0 else deg
208
209
210def clipRadians(rad, limit):
211    '''Clip a lat- or longitude to the given range.
212
213       @arg rad: Unclipped lat- or longitude (C{radians}).
214       @arg limit: Valid B{C{-limit..+limit}} range (C{radians}).
215
216       @return: Clipped value (C{radians}).
217
218       @raise RangeError: If B{C{abs(rad)}} beyond B{C{limit}} and
219                          L{rangerrors} set to C{True}.
220    '''
221    return _clipped_(rad, limit, _radians_) if limit and limit > 0 else rad
222
223
224def compassDMS(bearing, form=F_D, prec=None, sep=S_SEP):
225    '''Convert bearing to a string suffixed with compass point.
226
227       @arg bearing: Bearing from North (compass C{degrees360}).
228       @kwarg form: Optional B{C{bearing}} format (C{str} or L{F_D},
229                    L{F_DM}, L{F_DMS}, L{F_DEG}, L{F_MIN}, L{F_SEC},
230                    L{F__E}, L{F__F}, L{F__G}, L{F_RAD}, L{F_D_},
231                    L{F_DM_}, L{F_DMS_}, L{F_DEG_}, L{F_MIN_},
232                    L{F_SEC_}, L{F__E_}, L{F__F_}, L{F__G_}, L{F_RAD_},
233                    L{F_D__}, L{F_DM__}, L{F_DMS__}, L{F_DEG__},
234                    L{F_MIN__}, L{F_SEC__}, L{F__E__}, L{F__F__},
235                    L{F__G__} or L{F_RAD__}).
236       @kwarg prec: Optional number of decimal digits (0..9 or
237                    C{None} for default).  Trailing zero decimals
238                    are stripped for B{C{prec}} values of 1 and
239                    above, but kept for negative B{C{prec}}.
240       @kwarg sep: Optional separator (C{str}).
241
242       @return: Compass degrees and point in the specified form (C{str}).
243    '''
244    return _toDMS(bearing % _360_0, form, prec, sep, 1, compassPoint(bearing))
245
246
247def compassPoint(bearing, prec=3):
248    '''Convert bearing to a compass point.
249
250       @arg bearing: Bearing from North (compass C{degrees360}).
251       @kwarg prec: Optional precision (1 for cardinal or basic winds,
252                    2 for intercardinal or ordinal or principal winds,
253                    3 for secondary-intercardinal or half-winds or
254                    4 for quarter-winds).
255
256       @return: Compass point (1-, 2-, 3- or 4-letter C{str}).
257
258       @raise ValueError: Invalid B{C{prec}}.
259
260       @see: U{Dms.compassPoint
261             <https://GitHub.com/ChrisVeness/geodesy/blob/master/dms.js>}
262             and U{Compass rose<https://WikiPedia.org/wiki/Compass_rose>}.
263
264       @example:
265
266        >>> p = compassPoint(24, 1)  # 'N'
267        >>> p = compassPoint(24, 2)  # 'NE'
268        >>> p = compassPoint(24, 3)  # 'NNE'
269        >>> p = compassPoint(24)     # 'NNE'
270        >>> p = compassPoint(11, 4)  # 'NbE'
271        >>> p = compassPoint(30, 4)  # 'NEbN'
272
273        >>> p = compassPoint(11.249)  # 'N'
274        >>> p = compassPoint(11.25)   # 'NNE'
275        >>> p = compassPoint(-11.25)  # 'N'
276        >>> p = compassPoint(348.749) # 'NNW'
277    '''
278    try:  # m = 2 << prec; x = 32 // m
279        m, x = _MOD_X[prec]
280    except KeyError:
281        raise _ValueError(prec=prec)
282    # not round(), i.e. half-even rounding in Python 3,
283    # but round-away-from-zero as int(b + 0.5) iff b is
284    # non-negative, otherwise int(b + copysign0(_0_5, b))
285    q = int((bearing % _360_0) * m / _360_0 + _0_5) % m
286    return _WINDS[q * x]
287
288
289_MOD_X = {1: (4, 8), 2: (8, 4), 3: (16, 2), 4: (32, 1)}  # [prec]
290_WINDS = (_N_, 'NbE', 'NNE', 'NEbN', _NE_, 'NEbE', 'ENE', 'EbN',
291          _E_, 'EbS', 'ESE', 'SEbE', _SE_, 'SEbS', 'SSE', 'SbE',
292          _S_, 'SbW', 'SSW', 'SWbS', _SW_, 'SWbW', 'WSW', 'WbS',
293          _W_, 'WbN', 'WNW', 'NWbW', _NW_, 'NWbN', 'NNW', 'NbW')  # cardinals
294
295
296def degDMS(deg, prec=6, s_D=S_DEG, s_M=S_MIN, s_S=S_SEC, neg=_MINUS_, pos=NN):
297    '''Convert degrees to a string in degrees, minutes I{or} seconds.
298
299       @arg deg: Value in degrees (C{scalar}).
300       @kwarg prec: Optional number of decimal digits (0..9 or
301                    C{None} for default).  Trailing zero decimals
302                    are stripped for B{C{prec}} values of 1 and
303                    above, but kept for negative B{C{prec}}.
304       @kwarg s_D: Symbol for degrees (C{str}).
305       @kwarg s_M: Symbol for minutes (C{str}) or C{""}.
306       @kwarg s_S: Symbol for seconds (C{str}) or C{""}.
307       @kwarg neg: Optional sign for negative (C{'-'}).
308       @kwarg pos: Optional sign for positive (C{''}).
309
310       @return: I{Either} degrees, minutes I{or} seconds (C{str}).
311    '''
312    try:
313        deg = float(deg)
314    except (TypeError, ValueError) as x:
315        raise _ValueError(deg=deg, txt=str(x))
316
317    d, s = abs(deg), s_D
318    if d < 1:
319        if s_M:
320            d *= _60_0
321            if d < 1 and s_S:
322                d *= _60_0
323                s = s_S
324            else:
325                s = s_M
326        elif s_S:
327            d *= 3600
328            s = s_S
329
330    z = int(prec)
331    t = Fmt.F(d, prec=abs(z))
332    if z > 1:
333        t = fstrzs(t)
334    n = neg if deg < 0 else pos
335    return NN(n, t, s)
336
337
338def latDMS(deg, form=F_DMS, prec=2, sep=S_SEP):
339    '''Convert latitude to a string, optionally suffixed with N or S.
340
341       @arg deg: Latitude to be formatted (C{degrees}).
342       @kwarg form: Optional B{C{deg}} format (C{str} or L{F_D},
343                    L{F_DM}, L{F_DMS}, L{F_DEG}, L{F_MIN}, L{F_SEC},
344                    L{F__E}, L{F__F}, L{F__G}, L{F_D_}, L{F_RAD},
345                    L{F_D_}, L{F_DM_}, L{F_DMS_}, L{F_DEG_}, L{F_MIN_}
346                    L{F_SEC_}, L{F__E_}, L{F__F_}, L{F__G_}, L{F_RAD_},
347                    L{F_D__}, L{F_DM__}, L{F_DMS__}, L{F_DEG__},
348                    L{F_MIN__}, L{F_SEC__}, L{F__E__}, L{F__F__},
349                    L{F__G__} or L{F_RAD__}).
350       @kwarg prec: Optional number of decimal digits (0..9 or
351                    C{None} for default).  Trailing zero decimals
352                    are stripped for B{C{prec}} values of 1 and
353                    above, but kept for negative B{C{prec}}.
354       @kwarg sep: Optional separator (C{str}).
355
356       @return: Degrees in the specified form (C{str}).
357
358       @JSname: I{toLat}.
359    '''
360    return _toDMS(deg, form, prec, sep, 2, _S_ if deg < 0 else _N_)
361
362
363def latlonDMS(lls, form=F_DMS, prec=None, sep=None):
364    '''Convert one or more C{LatLon} instances to strings.
365
366       @arg lls: Single or a list, sequence, tuple, etc. (C{LatLon}s).
367       @kwarg form: Optional B{C{deg}} format (C{str} or L{F_D},
368                    L{F_DM}, L{F_DMS}, L{F_DEG}, L{F_MIN}, L{F_SEC},
369                    L{F__E}, L{F__F}, L{F__G}, L{F_RAD}, L{F_D_},
370                    L{F_DM_}, L{F_DMS_}, L{F_DEG_}, L{F_MIN_},
371                    L{F_SEC_}, L{F__E_}, L{F__F_}, L{F__G_}, L{F_RAD_},
372                    L{F_D__}, L{F_DM__}, L{F_DMS__}, L{F_DEG__},
373                    L{F_MIN__}, L{F_SEC__}, L{F__E__}, L{F__F__},
374                    L{F__G__} or L{F_RAD__}).
375       @kwarg prec: Optional number of decimal digits (0..9 or
376                    C{None} for default).  Trailing zero decimals
377                    are stripped for B{C{prec}} values of 1 and
378                    above, but kept for negative B{C{prec}}.
379       @kwarg sep: Separator joining B{C{lls}} (C{str} or C{None}).
380
381       @return: A C{str} or C{tuple} of B{C{sep}} is C{None} or C{NN}.
382    '''
383    if issequence(lls):
384        t = tuple(ll.toStr(form=form, prec=prec) for ll in lls)
385        if sep:
386            t = sep.join(t)
387    else:
388        t = lls.toStr(form=form, prec=prec)
389    return t
390
391
392def lonDMS(deg, form=F_DMS, prec=2, sep=S_SEP):
393    '''Convert longitude to a string, optionally suffixed with E or W.
394
395       @arg deg: Longitude to be formatted (C{degrees}).
396       @kwarg form: Optional B{C{deg}} format (C{str} or L{F_D},
397                    L{F_DM}, L{F_DMS}, L{F_DEG}, L{F_MIN}, L{F_SEC},
398                    L{F__E}, L{F__F}, L{F__G}, L{F_RAD}, L{F_D_},
399                    L{F_DM_}, L{F_DMS_}, L{F_DEG_}, L{F_MIN_},
400                    L{F_SEC_}, L{F__E_}, L{F__F_}, L{F__G_}, L{F_RAD_},
401                    L{F_D__}, L{F_DM__}, L{F_DMS__}, L{F_DEG__},
402                    L{F_MIN__}, L{F_SEC__}, L{F__E__}, L{F__F__},
403                    L{F__G__} or L{F_RAD__}).
404       @kwarg prec: Optional number of decimal digits (0..9 or
405                    C{None} for default).  Trailing zero decimals
406                    are stripped for B{C{prec}} values of 1 and
407                    above, but kept for negative B{C{prec}}.
408       @kwarg sep: Optional separator (C{str}).
409
410       @return: Degrees in the specified form (C{str}).
411
412       @JSname: I{toLon}.
413    '''
414    return _toDMS(deg, form, prec, sep, 3, _W_ if deg < 0 else _E_)
415
416
417def normDMS(strDMS, norm=NN):
418    '''Normalize all degree ˚, minute ' and second " symbols in a
419       string to the default symbols %s, %s and %s.
420
421       @arg strDMS: DMS (C{str}).
422       @kwarg norm: Optional replacement symbol, default symbol
423                    otherwise (C{str}).
424
425       @return: Normalized DMS (C{str}).
426    '''
427    if norm:
428        for s in _S_ALL:
429            strDMS = strDMS.replace(s, norm)
430        strDMS = strDMS.rstrip(norm)
431    else:
432        for s, S in _S_norm.items():
433            strDMS = strDMS.replace(s, S)
434    return strDMS
435
436
437def parseDDDMMSS(strDDDMMSS, suffix=_NSEW_, sep=S_SEP, clip=0):
438    '''Parse a lat- or longitude represention form [D]DDMMSS in degrees.
439
440       @arg strDDDMMSS: Degrees in any of several forms (C{str}) and
441                        types (C{float}, C{int}, other).
442       @kwarg suffix: Optional, valid compass points (C{str}, C{tuple}).
443       @kwarg sep: Optional separator between "[D]DD", "MM" and "SS" (%r).
444       @kwarg clip: Optionally, limit value to -clip..+clip (C{degrees}).
445
446       @return: Degrees (C{float}).
447
448       @raise ParseError: Invalid B{C{strDDDMMSS}} or B{C{clip}} or the
449                          B{C{strDDDMMSS}} form is incompatible with the
450                          suffixed or B{C{suffix}} compass point.
451
452       @raise RangeError: Value of B{C{strDDDMMSS}} outside the valid
453                          range and L{rangerrors} set to C{True}.
454
455       @note: Type C{str} values "[D]DD", "[D]DDMM" and "[D]DDMMSS" for
456              B{C{strDDDMMSS}} are parsed properly only if I{either}
457              unsigned and suffixed with a valid, compatible, C{cardinal}
458              L{compassPoint} I{or} if unsigned or signed, unsuffixed and
459              with keyword argument B{C{suffix}} set to B{%r}, B{%r} or a
460              compatible L{compassPoint}.
461
462       @note: Unlike function L{parseDMS}, type C{float}, C{int} and
463              other non-C{str} B{C{strDDDMMSS}} values are interpreted
464              form [D]DDMMSS.  For example, C{int(1230)} is returned as
465              12.5 I{and not 1230.0} degrees.  However, C{int(345)} is
466              considered form "DDD" 345 I{and not "DDMM" 0345}, unless
467              B{C{suffix}} specifies compass point B{%r}.
468
469       @see: Functions L{parseDMS}, L{parseDMS2} and L{parse3llh}.
470    '''
471    def _DDDMMSS_(strDDDMMSS, suffix, sep, clip):
472        S = suffix.upper()
473        if isstr(strDDDMMSS):
474            t = strDDDMMSS.strip()
475            if sep:
476                t = t.replace(sep, NN).strip()
477
478            s = t[:1]   # sign or digit
479            P = t[-1:]  # compass point, digit or dot
480
481            t = t.lstrip(_PLUSMINUS_).rstrip(S).strip()
482            f = t.split(_DOT_)
483            d = len(f[0])
484            f = NN.join(f)
485            if 1 < d < 8 and f.isdigit() and (
486                                 (P in S and s.isdigit()) or
487                            (P.isdigit() and s in '-0123456789+'  # PYCHOK indent
488                                         and S in ((_NS_, _EW_) + _WINDS))):
489                # check [D]DDMMSS form and compass point
490                X = _EW_ if isodd(d) else _NS_
491                if not (P in X or (S in X and (P.isdigit() or P == _DOT_))):
492                    t = 'DDDMMSS'[0 if isodd(d) else 1:d | 1], X[:1], X[1:]
493                    raise ParseError('form %s applies %s-%s' % t)
494                f = 0  # fraction
495            else:  # try other forms
496                return _DMS2deg(strDDDMMSS, S, sep, clip)
497
498        else:  # float or int to [D]DDMMSS[.fff]
499            f = float(strDDDMMSS)
500            s = _MINUS_ if f < 0 else NN
501            P = _0_  # anything except _SW_
502            f, i = modf(abs(f))
503            t = Fmt.f(i, prec=0)  # str(i) == 'i.0'
504            d = len(t)
505            # bump number of digits to match
506            # the given, valid compass point
507            if S in (_NS_ if isodd(d) else _EW_):
508                t = _0_ + t
509                d += 1
510            #   P = S
511            # elif d > 1:
512            #   P = (_EW_ if isodd(d) else _NS_)[0]
513
514        if d < 4:  # [D]DD[.ddd]
515            if f:
516                t = float(t) + f
517            t = t, 0, 0
518        else:
519            f += float(t[d-2:])
520            if d < 6:  # [D]DDMM[.mmm]
521                t = t[:d-2], f, 0
522            else:  # [D]DDMMSS[.sss]
523                t = t[:d-4], t[d-4:d-2], f
524        d = _dms2deg(s, P, *map2(float, t))
525
526        return clipDegrees(d, float(clip)) if clip else d
527
528    return _parseX(_DDDMMSS_, strDDDMMSS, suffix, sep, clip,
529                              strDDDMMSS=strDDDMMSS, suffix=suffix)
530
531
532if __debug__:  # no __doc__ at -O and -OO
533    parseDDDMMSS.__doc__  %= (S_SEP, _NS_, _EW_, _NS_)
534
535
536def _dms2deg(s, P, deg, min, sec):
537    '''(INTERNAL) Helper for C{parseDDDMMSS} and C{parseDMS}.
538    '''
539    deg += (min + (sec / _60_0)) / _60_0
540    if s == _MINUS_ or P in _SW_:
541        deg = neg(deg)
542    return deg
543
544
545def _DMS2deg(strDMS, suffix, sep, clip):
546    '''(INTERNAL) Helper for C{parseDDDMMSS} and C{parseDMS}.
547    '''
548    try:
549        d = float(strDMS)
550
551    except (TypeError, ValueError):
552        strDMS = strDMS.strip()
553
554        t = strDMS.lstrip(_PLUSMINUS_).rstrip(suffix.upper()).strip()
555        if sep:
556            t = t.replace(sep, _SPACE_)
557            for s in _S_ALL:
558                t = t.replace(s, NN)
559        else:
560            for s in _S_ALL:
561                t = t.replace(s, _SPACE_)
562        t =  map2(float, t.strip().split()) + (0, 0)
563        d = _dms2deg(strDMS[:1], strDMS[-1:], *t[:3])
564
565    return clipDegrees(d, float(clip)) if clip else d
566
567
568def parseDMS(strDMS, suffix=_NSEW_, sep=S_SEP, clip=0):  # MCCABE 14
569    '''Parse a lat- or longitude representation C{"lat, lon"} in C{degrees}.
570
571       This is very flexible on formats, allowing signed decimal
572       degrees, degrees and minutes or degrees minutes and seconds
573       optionally suffixed by a cardinal compass point.
574
575       A variety of symbols, separators and suffixes are accepted,
576       for example "3°37′09″W".  Minutes and seconds may be omitted.
577
578       @arg strDMS: Degrees in any of several forms (C{str}) and
579                    types (C{float}, C{int}, other).
580       @kwarg suffix: Optional, valid compass points (C{str}, C{tuple}).
581       @kwarg sep: Optional separator between deg°, min′ and sec″ ("''").
582       @kwarg clip: Optionally, limit value to -clip..+clip (C{degrees}).
583
584       @return: Degrees (C{float}).
585
586       @raise ParseError: Invalid B{C{strDMS}} or B{C{clip}}.
587
588       @raise RangeError: Value of B{C{strDMS}} outside the valid range
589                          and L{rangerrors} set to C{True}.
590
591       @note: Inlike function L{parseDDDMMSS}, type C{float}, C{int}
592              and other non-C{str} B{C{strDMS}} values are considered
593              as decimal degrees.  For example, C{int(1230)} is returned
594              as 1230.0 I{and not as 12.5} degrees and C{float(345)} as
595              345.0 I{and not as 3.75} degrees!
596
597       @see: Functions L{parseDDDMMSS}, L{parseDMS2} and L{parse3llh}.
598    '''
599    return _parseX(_DMS2deg, strDMS, suffix, sep, clip, strDMS=strDMS, suffix=suffix)
600
601
602def parseDMS2(strLat, strLon, sep=S_SEP, clipLat=90, clipLon=180):
603    '''Parse a lat- and a longitude representions in C{degrees}.
604
605       @arg strLat: Latitude in any of several forms (C{str} or C{degrees}).
606       @arg strLon: Longitude in any of several forms (C{str} or C{degrees}).
607       @kwarg sep: Optional separator between deg°, min′ and sec″ ('').
608       @kwarg clipLat: Keep latitude in B{C{-clipLat..+clipLat}} range (C{degrees}).
609       @kwarg clipLon: Keep longitude in B{C{-clipLon..+clipLon}} range (C{degrees}).
610
611       @return: A L{LatLon2Tuple}C{(lat, lon)} in C{degrees}.
612
613       @raise ParseError: Invalid B{C{strLat}} or B{C{strLon}}.
614
615       @raise RangeError: Value of B{C{strLat}} or B{C{strLon}} outside the
616                          valid range and L{rangerrors} set to C{True}.
617
618       @note: See the B{Notes} at function L{parseDMS}.
619
620       @see: Functions L{parseDDDMMSS}, L{parseDMS} and L{parse3llh}.
621    '''
622    from pygeodesy.namedTuples import LatLon2Tuple  # avoid circluar import
623
624    return LatLon2Tuple(parseDMS(strLat, suffix=_NS_, sep=sep, clip=clipLat),
625                        parseDMS(strLon, suffix=_EW_, sep=sep, clip=clipLon))
626
627
628def parse3llh(strllh, height=0, sep=_COMMA_, clipLat=90, clipLon=180):
629    '''Parse a string C{"lat lon [h]"} representing lat-, longitude in
630       C{degrees} and optional height in C{meter}.
631
632       The lat- and longitude value must be separated by a separator
633       character.  If height is present it must follow, separated by
634       another separator.
635
636       The lat- and longitude values may be swapped, provided at least
637       one ends with the proper compass point.
638
639       @arg strllh: Latitude, longitude[, height] (C{str}, ...).
640       @kwarg height: Optional, default height (C{meter}) or C{None}.
641       @kwarg sep: Optional separator (C{str}).
642       @kwarg clipLat: Keep latitude in B{C{-clipLat..+clipLat}} (C{degrees}).
643       @kwarg clipLon: Keep longitude in B{C{-clipLon..+clipLon}} range (C{degrees}).
644
645       @return: A L{LatLon3Tuple}C{(lat, lon, height)} in C{degrees},
646                C{degrees} and C{float}.
647
648       @raise RangeError: Lat- or longitude value of B{C{strllh}} outside
649                          valid range and L{rangerrors} set to C{True}.
650
651       @raise ValueError: Invalid B{C{strllh}} or B{C{height}}.
652
653       @note: See the B{Notes} at function L{parseDMS}.
654
655       @see: Functions L{parseDDDMMSS}, L{parseDMS} and L{parseDMS2}.
656
657       @example:
658
659        >>> parse3llh('000°00′05.31″W, 51° 28′ 40.12″ N')
660        (51.4778°N, 000.0015°W, 0)
661    '''
662    from pygeodesy.namedTuples import LatLon3Tuple  # avoid circluar import
663
664    def _3llh_(strllh, height, sep):
665        ll = strllh.strip().split(sep)
666        if len(ll) > 2:  # XXX interpret height unit
667            h = float(ll.pop(2).rstrip(_LETTERS).strip())
668        else:
669            h = height  # None from wgrs.Georef.__new__
670        if len(ll) != 2:
671            raise ValueError
672
673        a, b = [_.strip() for _ in ll]  # PYCHOK false
674        if a[-1:] in _EW_ or b[-1:] in _NS_:
675            a, b = b, a
676        return LatLon3Tuple(parseDMS(a, suffix=_NS_, clip=clipLat),
677                            parseDMS(b, suffix=_EW_, clip=clipLon), h)
678
679    return _parseX(_3llh_, strllh, height, sep, strllh=strllh)
680
681
682def parseRad(strRad, suffix=_NSEW_, clip=0):
683    '''Parse a string representing angle in C{radians}.
684
685       @arg strRad: Degrees in any of several forms (C{str} or C{radians}).
686       @kwarg suffix: Optional, valid compass points (C{str}, C{tuple}).
687       @kwarg clip: Optionally, limit value to -clip..+clip (C{radians}).
688
689       @return: Radians (C{float}).
690
691       @raise ParseError: Invalid B{C{strRad}} or B{C{clip}}.
692
693       @raise RangeError: Value of B{C{strRad}} outside the valid range
694                          and L{rangerrors} set to C{True}.
695    '''
696    def _Rad_(strRad, suffix, clip):
697        try:
698            r = float(strRad)
699
700        except (TypeError, ValueError):
701            strRad = strRad.strip()
702
703            r = float(strRad.lstrip(_PLUSMINUS_).rstrip(suffix.upper()).strip())
704            if strRad[:1] == _MINUS_ or strRad[-1:] in _SW_:
705                r = neg(r)
706
707        return clipRadians(r, float(clip)) if clip else r
708
709    return _parseX(_Rad_, strRad, suffix, clip, strRad=strRad, suffix=suffix)
710
711
712def precision(form, prec=None):
713    '''Set the default precison for a given F_ form.
714
715       @arg form: L{F_D}, L{F_DM}, L{F_DMS}, L{F_DEG}, L{F_MIN},
716                  L{F_SEC}, L{F__E}, L{F__F}, L{F__G} or L{F_RAD}
717                  (C{str}).
718       @kwarg prec: Optional number of decimal digits (0..9 or
719                    C{None} for default).  Trailing zero decimals
720                    are stripped for B{C{prec}} values of 1 and
721                    above, but kept for negative B{C{prec}}.
722
723       @return: Previous precision (C{int}).
724
725       @raise ValueError: Invalid B{C{form}} or B{C{prec}} or
726                          B{C{prec}} outside valid range.
727    '''
728    try:
729        p = _F_prec[form]
730    except KeyError:
731        raise _ValueError(form=form)
732
733    if prec is not None:
734        from pygeodesy.units import Precision_
735        _F_prec[form] = Precision_(prec=prec, low=-9, high=9)
736
737    return p
738
739
740def toDMS(deg, form=F_DMS, prec=2, sep=S_SEP, ddd=2, neg=_MINUS_, pos=NN):
741    '''Convert I{signed} C{degrees} to string, without suffix.
742
743       @arg deg: Degrees to be formatted (C{degrees}).
744       @kwarg form: Optional B{C{deg}} format (C{str} or L{F_D},
745                    L{F_DM}, L{F_DMS}, L{F_DEG}, L{F_MIN}, L{F_SEC},
746                    L{F__E}, L{F__F}, L{F__G}, L{F_RAD}, L{F_D_},
747                    L{F_DM_}, L{F_DMS_}, L{F_DEG_}, L{F_MIN_},
748                    L{F_SEC_}, L{F__E_}, L{F__F_}, L{F__G_}, L{F_RAD_},
749                    L{F_D__}, L{F_DM__}, L{F_DMS__}, L{F_DEG__},
750                    L{F_MIN__}, L{F_SEC__}, L{F__E__}, L{F__F__},
751                    L{F__G__} or L{F_RAD__}).
752       @kwarg prec: Optional number of decimal digits (0..9 or
753                    C{None} for default).  Trailing zero decimals
754                    are stripped for B{C{prec}} values of 1 and
755                    above, but kept for negative B{C{prec}}.
756       @kwarg sep: Optional separator (C{str}).
757       @kwarg ddd: Optional number of digits for deg° (2 or 3).
758       @kwarg neg: Optional sign for negative degrees ('-').
759       @kwarg pos: Optional sign for positive degrees ('').
760
761       @return: Degrees in the specified form (C{str}).
762    '''
763    t = _toDMS(deg, form, prec, sep, ddd, NN)
764    if deg and form[:1] not in _PLUSMINUS_:
765        t = NN((neg if deg < 0 else (pos if deg > 0 else NN)), t)
766    return t
767
768# **) MIT License
769#
770# Copyright (C) 2016-2021 -- mrJean1 at Gmail -- All Rights Reserved.
771#
772# Permission is hereby granted, free of charge, to any person obtaining a
773# copy of this software and associated documentation files (the "Software"),
774# to deal in the Software without restriction, including without limitation
775# the rights to use, copy, modify, merge, publish, distribute, sublicense,
776# and/or sell copies of the Software, and to permit persons to whom the
777# Software is furnished to do so, subject to the following conditions:
778#
779# The above copyright notice and this permission notice shall be included
780# in all copies or substantial portions of the Software.
781#
782# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
783# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
784# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
785# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
786# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
787# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
788# OTHER DEALINGS IN THE SOFTWARE.
789