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