Coverage for pygeodesy/dms.py: 95%

282 statements  

« prev     ^ index     » next       coverage.py v7.2.2, created at 2023-08-28 15:52 -0400

1 

2# -*- coding: utf-8 -*- 

3 

4u'''Parsers and formatters of angles in degrees, minutes and seconds or radians. 

5 

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. 

9 

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>}. 

13 

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}). 

25 

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}). 

37 

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}). 

49 

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} 

56 

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''' 

60 

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_, _NS_, \ 

68 _NSEW_, _NW_, _of_, _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 

72# from pygeodesy.utily import _Wrap # _MODS 

73 

74from math import fabs, modf, radians 

75try: 

76 from string import letters as _LETTERS 

77except ImportError: # Python 3+ 

78 from string import ascii_letters as _LETTERS 

79 

80__all__ = _ALL_LAZY.dms 

81__version__ = '23.08.24' 

82 

83_beyond_ = 'beyond' 

84_DDDMMSS_ = 'DDDMMSS' 

85_deg_min_ = 'deg+min' 

86_keyword_ = 'keyword' 

87_SDIGITS_ = '-0123456789+' 

88_sexagecimal_ = 'sexagecimal' 

89_SEXAGECIMUL = 1.e4 # sexagecimal C{D.MMSSss} into decimal C{DMMSS.ss} 

90 

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 

98 

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 

103 

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} 

107 

108_F_symb = set((F_D, F_DM, F_DMS, _deg_min_)) # == {} pychok -Tb 

109 

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 

117 

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_} 

122 

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') 

127 

128 

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 

137 

138 

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) 

143 

144 

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)) 

153 

154 

155def _fstrzs(t, **unused): 

156 '''(INTERNAL) Pass-thru version of C{.streprs.fstrzs}. 

157 ''' 

158 return t 

159 

160 

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 

169 

170 

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) 

178 

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) 

193 

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) 

202 

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) 

209 

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) 

215 

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) 

219 

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) 

225 

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) 

230 

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) 

235 

236 return t # NOT unicode in Python 2- 

237 

238 

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). 

241 

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}. 

259 

260 @return: Compass degrees per the specified B{C{form}} (C{str}). 

261 

262 @see: Function L{pygeodesy.toDMS}. 

263 ''' 

264 return _toDMS(_umod_360(bearing), form, prec, sep, 1, NN, s_D_M_S) 

265 

266 

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 

276 

277 

278def clipDegrees(deg, limit): 

279 '''Clip a lat- or longitude to the given range. 

280 

281 @arg deg: Unclipped lat- or longitude (C{scalar degrees}). 

282 @arg limit: Valid C{-/+B{limit}} range (C{degrees}). 

283 

284 @return: Clipped value (C{degrees}). 

285 

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 

290 

291 

292def clipRadians(rad, limit): 

293 '''Clip a lat- or longitude to the given range. 

294 

295 @arg rad: Unclipped lat- or longitude (C{radians}). 

296 @arg limit: Valid C{-/+B{limit}} range (C{radians}). 

297 

298 @return: Clipped value (C{radians}). 

299 

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 

304 

305 

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. 

308 

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}. 

326 

327 @return: Compass degrees and point in the specified form (C{str}). 

328 

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) 

333 

334 

335def compassPoint(bearing, prec=3): 

336 '''Convert bearing to a compass point. 

337 

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). 

344 

345 @return: Compass point (1-, 2-, 3- or 4-letter C{str}). 

346 

347 @raise ValueError: Invalid B{C{prec}}. 

348 

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>}. 

352 

353 @example: 

354 

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' 

361 

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] 

376 

377 raise ValueError 

378 except (IndexError, TypeError, ValueError) as x: 

379 raise _ValueError(bearing=bearing, prec=prec, cause=x) 

380 

381 

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. 

384 

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{''}). 

394 

395 @return: I{Either} degrees, minutes I{or} seconds (C{str}). 

396 

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) 

403 

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 

416 

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- 

423 

424 

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. 

427 

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}. 

445 

446 @return: Degrees in the specified form (C{str}). 

447 

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) 

452 

453 

454def latlonDMS(lls, **m_form_prec_sep_s_D_M_S): 

455 '''Convert one or more C{LatLon} instances to strings. 

456 

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}. 

463 

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}. 

467 

468 @see: Functions L{pygeodesy.latDMS}, L{pygeodesy.latlonDMS_}, 

469 L{pygeodesy.lonDMS} and L{pygeodesy.toDMS} and method 

470 C{LatLon.toStr}. 

471 

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 

486 

487 

488def latlonDMS_(*lls, **m_form_prec_sep_s_D_M_S): 

489 '''Convert one or more C{LatLon} instances to strings. 

490 

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}. 

497 

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}}. 

501 

502 @see: Function L{pygeodesy.latlonDMS}. 

503 

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 

515 

516 

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 

525 

526 

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. 

529 

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}. 

547 

548 @return: Degrees in the specified form (C{str}). 

549 

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) 

554 

555 

556def normDMS(strDMS, norm=None, **s_D_M_S): 

557 '''Normalize all degrees, minutes and seconds (DMS) I{symbols} in 

558 a string to the default symbols L{S_DEG}, L{S_MIN}, L{S_SEC}. 

559 

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}}. 

567 

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 

575 

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 

579 

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) 

584 

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? 

592 

593 return strDMS # NOT unicode in Python 2- 

594 

595 

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. 

598 

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__}. 

607 

608 @return: Degrees (C{float}). 

609 

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. 

613 

614 @raise RangeError: Value of B{C{strDDDMMSS}} outside the valid C{-/+B{clip}} 

615 range and L{pygeodesy.rangerrors} set to C{True}. 

616 

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}. 

622 

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}. 

632 

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, {}) 

655 

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 

663 

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) 

685 

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 

696 

697 return _parseX(_DDDMMSS, strDDDMMSS, suffix, sep, clip, sexagecimal, 

698 strDDDMMSS=strDDDMMSS, suffix=suffix, sexagecimal=sexagecimal) 

699 

700 

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 

708 

709 

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) 

715 

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]) 

725 

726 return clipDegrees(d, float(clip)) if clip else d 

727 

728 

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}. 

731 

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. 

735 

736 A variety of symbols, separators and suffixes are accepted, 

737 for example "3°37′09″W". Minutes and seconds may be omitted. 

738 

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}. 

746 

747 @return: Degrees (C{float}). 

748 

749 @raise ParseError: Invalid B{C{strDMS}} or B{C{clip}}. 

750 

751 @raise RangeError: Value of B{C{strDMS}} outside the valid C{-/+B{clip}} 

752 range and L{pygeodesy.rangerrors} set to C{True}. 

753 

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! 

759 

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) 

764 

765 

766def parseDMS2(strLat, strLon, sep=S_SEP, clipLat=90, clipLon=180, wrap=False, **s_D_M_S): 

767 '''Parse a lat- and a longitude representions C{"lat, lon"} in C{degrees}. 

768 

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 wrap: If C{True}, wrap or I{normalize} the lat- and longitude, 

775 overriding B{C{clipLat}} and B{C{clipLon}} (C{bool}). 

776 @kwarg s_D_M_S: Optional, alternate symbol for degrees C{B{s_D}=str}, 

777 minutes C{B{s_M}=str} and/or seconds C{B{s_S}=str}. 

778 

779 @return: A L{LatLon2Tuple}C{(lat, lon)} in C{degrees}. 

780 

781 @raise ParseError: Invalid B{C{strLat}} or B{C{strLon}}. 

782 

783 @raise RangeError: Value of B{C{strLat}} or B{C{strLon}} outside the 

784 valid C{-/+B{clipLat}} or C{-/+B{clipLon}} range 

785 and L{pygeodesy.rangerrors} set to C{True}. 

786 

787 @note: See the B{Notes} at function L{parseDMS}. 

788 

789 @see: Functions L{pygeodesy.parseDDDMMSS}, L{pygeodesy.parseDMS}, 

790 L{pygeodesy.parse3llh} and L{pygeodesy.toDMS}. 

791 ''' 

792 return _2Tuple(strLat, strLon, clipLat, clipLon, wrap, sep=sep, **s_D_M_S) 

793 

794 

795def _2Tuple(strLat, strLon, clipLat, clipLon, wrap, **kwds): 

796 '''(INTERNAL) Helper for C{parseDMS2} and C{parse3llh}. 

797 ''' 

798 if wrap: 

799 _W = _MODS.utily._Wrap 

800 lat, lon = _W.latlon(parseDMS(strLat, suffix=_NS_, **kwds), 

801 parseDMS(strLon, suffix=_EW_, **kwds)) 

802 else: 

803 # if wrap is None: 

804 # clipLat = clipLon = 0 

805 lat = parseDMS(strLat, suffix=_NS_, clip=clipLat, **kwds) 

806 lon = parseDMS(strLon, suffix=_EW_, clip=clipLon, **kwds) 

807 return _MODS.namedTuples.LatLon2Tuple(lat, lon) 

808 

809 

810def parse3llh(strllh, height=0, sep=_COMMA_, clipLat=90, clipLon=180, wrap=False, **s_D_M_S): 

811 '''Parse a string C{"lat, lon [, h]"} representing lat-, longitude in 

812 C{degrees} and optional height in C{meter}. 

813 

814 The lat- and longitude value must be separated by a separator 

815 character. If height is present it must follow, separated by 

816 another separator. 

817 

818 The lat- and longitude values may be swapped, provided at least 

819 one ends with the proper compass point. 

820 

821 @arg strllh: Latitude, longitude[, height] (C{str}, ...). 

822 @kwarg height: Optional, default height (C{meter}) or C{None}. 

823 @kwarg sep: Optional separator between C{"lat lon [h] suffix"} (C{str}). 

824 @kwarg clipLat: Limit latitude to range C{-/+B{clipLat}} (C{degrees}). 

825 @kwarg clipLon: Limit longitude to range C{-/+B{clipLon}} (C{degrees}). 

826 @kwarg wrap: If C{True}, wrap or I{normalize} the lat- and longitude, 

827 overriding B{C{clipLat}} and B{C{clipLon}} (C{bool}). 

828 @kwarg s_D_M_S: Optional, alternate symbol for degrees C{B{s_D}=str}, 

829 minutes C{B{s_M}=str} and/or seconds C{B{s_S}=str}. 

830 

831 @return: A L{LatLon3Tuple}C{(lat, lon, height)} in C{degrees}, 

832 C{degrees} and C{float}. 

833 

834 @raise RangeError: Lat- or longitude value of B{C{strllh}} outside 

835 the valid C{-/+B{clipLat}} or C{-/+B{clipLon}} 

836 range and L{pygeodesy.rangerrors} set to C{True}. 

837 

838 @raise ValueError: Invalid B{C{strllh}} or B{C{height}}. 

839 

840 @note: See the B{Notes} at function L{parseDMS}. 

841 

842 @see: Functions L{pygeodesy.parseDDDMMSS}, L{pygeodesy.parseDMS}, 

843 L{pygeodesy.parseDMS2} and L{pygeodesy.toDMS}. 

844 

845 @example: 

846 

847 >>> parse3llh('000°00′05.31″W, 51° 28′ 40.12″ N') 

848 (51.4778°N, 000.0015°W, 0) 

849 ''' 

850 

851 def _3llh(strllh, height, sep, wrap): 

852 ll = strllh.strip().split(sep) 

853 if len(ll) > 2: # XXX interpret height unit 

854 h = float(ll.pop(2).rstrip(_LETTERS + _SPACE_)) 

855 else: 

856 h = height # None from wgrs.Georef.__new__ 

857 if len(ll) != 2: 

858 raise ValueError 

859 

860 a, b = [_.strip() for _ in ll] # PYCHOK false 

861 if a[-1:] in _EW_ or b[-1:] in _NS_: 

862 a, b = b, a 

863 return _2Tuple(a, b, clipLat, clipLon, wrap, **s_D_M_S).to3Tuple(h) 

864 

865 return _parseX(_3llh, strllh, height, sep, wrap, strllh=strllh) 

866 

867 

868def parseRad(strRad, suffix=_NSEW_, clip=0): 

869 '''Parse a string representing angle in C{radians}. 

870 

871 @arg strRad: Degrees in any of several forms (C{str} or C{radians}). 

872 @kwarg suffix: Optional, valid compass points (C{str}, C{tuple}). 

873 @kwarg clip: Optionally, limit value to range C{-/+B{clip}} (C{radians}). 

874 

875 @return: Radians (C{float}). 

876 

877 @raise ParseError: Invalid B{C{strRad}} or B{C{clip}}. 

878 

879 @raise RangeError: Value of B{C{strRad}} outside the valid C{-/+B{clip}} 

880 range and L{pygeodesy.rangerrors} set to C{True}. 

881 ''' 

882 def _Rad(strRad, suffix, clip): 

883 try: 

884 r = float(strRad) 

885 

886 except (TypeError, ValueError): 

887 s, t, P = _split3(strRad, suffix.upper()) 

888 r = _dms2deg(s, P, float(t)) 

889 

890 return clipRadians(r, float(clip)) if clip else r 

891 

892 return _parseX(_Rad, strRad, suffix, clip, strRad=strRad, suffix=suffix) 

893 

894 

895def precision(form, prec=None): 

896 '''Set the default precison for a given F_ form. 

897 

898 @arg form: L{F_D}, L{F_DM}, L{F_DMS}, L{F_DEG}, L{F_MIN}, 

899 L{F_SEC}, L{F_D60}, L{F__E}, L{F__F}, L{F__G} 

900 or L{F_RAD} (C{str}). 

901 @kwarg prec: Number of decimal digits (0..9 or C{None} for 

902 default). Trailing zero decimals are stripped 

903 for B{C{prec}} values of 1 and above, but kept 

904 for negative B{C{prec}}. 

905 

906 @return: Previous precision for the B{C{form}} (C{int}). 

907 

908 @raise ValueError: Invalid B{C{form}} or B{C{prec}} or B{C{prec}} 

909 outside the valid range C{-/+9}. 

910 ''' 

911 try: 

912 p = _F_prec[form] 

913 except KeyError: 

914 raise _ValueError(form=form) 

915 

916 if prec is not None: # set as default 

917 _F_prec[form] = _MODS.units.Precision_(prec=prec, low=-9, high=9) 

918 

919 return p 

920 

921 

922def toDMS(deg, form=F_DMS, prec=2, sep=S_SEP, ddd=2, neg=_MINUS_, pos=_PLUS_, **s_D_M_S): 

923 '''Convert I{signed} C{degrees} to string, without suffix. 

924 

925 @arg deg: Degrees to be formatted (C{scalar degrees}). 

926 @kwarg form: Format specifier for B{C{deg}} (C{str} or L{F_D}, 

927 L{F_DM}, L{F_DMS}, L{F_DEG}, L{F_MIN}, L{F_SEC}, 

928 L{F_D60}, L{F__E}, L{F__F}, L{F__G}, L{F_RAD}, 

929 L{F_D_}, L{F_DM_}, L{F_DMS_}, L{F_DEG_}, L{F_MIN_}, 

930 L{F_SEC_}, L{F_D60_}, L{F__E_}, L{F__F_}, L{F__G_}, 

931 L{F_RAD_}, L{F_D__}, L{F_DM__}, L{F_DMS__}, L{F_DEG__}, 

932 L{F_MIN__}, L{F_SEC__}, L{F_D60__}, L{F__E__}, 

933 L{F__F__}, L{F__G__} or L{F_RAD__}). 

934 @kwarg prec: Number of decimal digits (0..9 or C{None} for default). 

935 Trailing zero decimals are stripped for B{C{prec}} 

936 values of 1 and above, but kept for negative B{C{prec}}. 

937 @kwarg sep: Separator between degrees, minutes, seconds, suffix (C{str}). 

938 @kwarg ddd: Number of digits for B{C{deg}°} (2 or 3). 

939 @kwarg neg: Prefix for negative B{C{deg}} (C{'-'}). 

940 @kwarg pos: Prefix for positive B{C{deg}} and signed B{C{form}} (C{'+'}). 

941 @kwarg s_D_M_S: Optional keyword arguments C{B{s_D}=str}, C{B{s_M}=str} 

942 C{B{s_S}=str} and C{B{s_DMS}=True} to override any or 

943 cancel all DMS symbols, defaults L{S_DEG}, L{S_MIN} 

944 respectively L{S_SEC}. See B{Notes} below. 

945 

946 @return: Degrees in the specified form (C{str}). 

947 

948 @note: The degrees, minutes and seconds (DMS) symbol can be overridden in 

949 this and other C{*DMS} functions by using optional keyword argments 

950 C{B{s_D}="d"}, C{B{s_M}="'"} respectively C{B{s_S}='"'}. Using 

951 keyword argument B{C{s_DMS}=None} cancels all C{DMS} symbols to 

952 C{B{S_NUL}=NN}. 

953 

954 @note: Sexagecimal format B{C{F_D60}} supports overridable pseudo-DMS symbols 

955 positioned at C{"[D]DD<B{s_D}>MM<B{s_M}>SS<B{s_S}>"} with defaults 

956 C{B{s_D}="."}, C{B{s_M}=B{sep}} and C{B{s_S}=}L{pygeodesy.NN}. 

957 

958 @note: Formats B{C{F__E}}, B{C{F__F}} and B{C{F__G}} can be extended with 

959 a C{D}-only symbol if defined with keyword argument C{B{s_D}=str}. 

960 Likewise for B{C{F_RAD}} formats with keyword argument C{B{s_R}=str}. 

961 

962 @see: Function L{pygeodesy.degDMS} 

963 ''' 

964 s = form[:1] 

965 f = form[1:] if s in _PLUSMINUS_ else form 

966 t = _toDMS(deg, f, prec, sep, ddd, NN, s_D_M_S) # unsigned and -suffixed 

967 if deg < 0 and neg: 

968 t = neg + t 

969 elif deg > 0 and s == _PLUS_ and pos: 

970 t = pos + t 

971 return t 

972 

973# **) MIT License 

974# 

975# Copyright (C) 2016-2023 -- mrJean1 at Gmail -- All Rights Reserved. 

976# 

977# Permission is hereby granted, free of charge, to any person obtaining a 

978# copy of this software and associated documentation files (the "Software"), 

979# to deal in the Software without restriction, including without limitation 

980# the rights to use, copy, modify, merge, publish, distribute, sublicense, 

981# and/or sell copies of the Software, and to permit persons to whom the 

982# Software is furnished to do so, subject to the following conditions: 

983# 

984# The above copyright notice and this permission notice shall be included 

985# in all copies or substantial portions of the Software. 

986# 

987# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 

988# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 

989# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 

990# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR 

991# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 

992# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 

993# OTHER DEALINGS IN THE SOFTWARE.