Coverage for pygeodesy/dms.py: 97%

298 statements  

« prev     ^ index     » next       coverage.py v7.6.1, created at 2025-01-06 12:20 -0500

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 

10Set env variable C{PYGEODESY_FMT_FORM} to any C{F_...} form to override default C{F_DMS} 

11formatting of lat- and longitudes or to an empty string to restore the default. 

12 

13After I{(C) Chris Veness 2011-2024} published under the same MIT Licence**, see 

14U{Latitude/Longitude<https://www.Movable-Type.co.UK/scripts/latlong.html>} and 

15U{Vector-based geodesy<https://www.Movable-Type.co.UK/scripts/latlong-vectors.html>}. 

16 

17@var F_D: Format degrees as unsigned "deg°" with symbol, plus compass point suffix C{N, S, E} or C{W} (C{str}). 

18@var F_DM: Format degrees as unsigned "deg°min′" with symbols, plus suffix (C{str}). 

19@var F_DMS: Format degrees as unsigned "deg°min′sec″" with symbols, plus suffix (C{str}). 

20@var F_DEG: Format degrees as unsigned "[D]DD" I{without} symbol, plus suffix (C{str}). 

21@var F_MIN: Format degrees as unsigned "[D]DDMM" I{without} symbols, plus suffix (C{str}). 

22@var F_SEC: Format degrees as unsigned "[D]DDMMSS" I{without} symbols, plus suffix (C{str}). 

23@var F_D60: Format degrees as unsigned "[D]DD.MMSS" C{sexagecimal} I{without} symbols, plus suffix (C{str}). 

24@var F__E: Format degrees as unsigned "%E" I{without} symbols, plus suffix (C{str}). 

25@var F__F: Format degrees as unsigned "%F" I{without} symbols, plus suffix (C{str}). 

26@var F__G: Format degrees as unsigned "%G" I{without} symbols, plus suffix (C{str}). 

27@var F_RAD: Convert degrees to radians and format as unsigned "RR" with symbol, plus suffix (C{str}). 

28 

29@var F_D_: Format degrees as signed "-/deg°" with symbol, I{without} suffix (C{str}). 

30@var F_DM_: Format degrees as signed "-/deg°min′" with symbols, I{without} suffix (C{str}). 

31@var F_DMS_: Format degrees as signed "-/deg°min′sec″" with symbols, I{without} suffix (C{str}). 

32@var F_DEG_: Format degrees as signed "-/[D]DD" I{without} symbol, I{without} suffix (C{str}). 

33@var F_MIN_: Format degrees as signed "-/[D]DDMM" I{without} symbols, I{without} suffix (C{str}). 

34@var F_SEC_: Format degrees as signed "-/[D]DDMMSS" I{without} symbols, I{without} suffix (C{str}). 

35@var F_D60_: Format degrees as signed "-/[D]DD.MMSS" C{sexagecimal} I{without} symbols, I{without} suffix (C{str}). 

36@var F__E_: Format degrees as signed "-/%E" I{without} symbols, I{without} suffix (C{str}). 

37@var F__F_: Format degrees as signed "-/%F" I{without} symbols, I{without} suffix (C{str}). 

38@var F__G_: Format degrees as signed "-/%G" I{without} symbols, I{without} suffix (C{str}). 

39@var F_RAD_: Convert degrees to radians and format as signed "-/RR" I{without} symbol, I{without} suffix (C{str}). 

40 

41@var F_D__: Format degrees as signed "-/+deg°" with symbol, I{without} suffix (C{str}). 

42@var F_DM__: Format degrees as signed "-/+deg°min′" with symbols, I{without} suffix (C{str}). 

43@var F_DMS__: Format degrees as signed "-/+deg°min′sec″" with symbols, I{without} suffix (C{str}). 

44@var F_DEG__: Format degrees as signed "-/+[D]DD" I{without} symbol, I{without} suffix (C{str}). 

45@var F_MIN__: Format degrees as signed "-/+[D]DDMM" I{without} symbols, without suffix (C{str}). 

46@var F_SEC__: Format degrees as signed "-/+[D]DDMMSS" I{without} symbols, I{without} suffix (C{str}). 

47@var F_D60__: Format degrees as signed "-/+[D]DD.MMSS" C{sexagecimal} I{without} symbols, I{without} suffix (C{str}). 

48@var F__E__: Format degrees as signed "-/+%E" I{without} symbols, I{without} suffix (C{str}). 

49@var F__F__: Format degrees as signed "-/+%F" I{without} symbols, I{without} suffix (C{str}). 

50@var F__G__: Format degrees as signed "-/+%G" I{without} symbols, I{without} suffix (C{str}). 

51@var F_RAD__: Convert degrees to radians and format as signed "-/+RR" I{without} symbol, I{without} suffix (C{str}). 

52 

53@var S_DEG: Degrees symbol, default C{"°"} 

54@var S_MIN: Minutes symbol, default C{"′"} aka I{PRIME} 

55@var S_SEC: Seconds symbol, default C{"″"} aka I{DOUBLE_PRIME} 

56@var S_RAD: Radians symbol, default C{""} aka L{pygeodesy.NN} 

57@var S_DMS: If C{True} include, otherwise cancel all DMS symbols, default C{True}. 

58@var S_SEP: Separator between C{deg°|min′|sec″|suffix}, default C{""} aka L{pygeodesy.NN} 

59 

60@note: In Python 2-, L{S_DEG}, L{S_MIN}, L{S_SEC}, L{S_RAD} and L{S_SEP} may be multi-byte, 

61 non-ascii characters and if so, I{not} C{unicode}. 

62''' 

63 

64from pygeodesy.basics import copysign0, isLatLon, isodd, issequence, isstr, \ 

65 neg as _neg # in .ups 

66from pygeodesy.constants import _umod_360, _0_0, _0_5, _60_0, _360_0, _3600_0 

67from pygeodesy.errors import ParseError, RangeError, _TypeError, _ValueError, \ 

68 _parseX, rangerrors, _xError, _xkwds, _getPYGEODESY 

69# from pygeodesy.internals import _getPYGEODESY # from .errors 

70from pygeodesy.interns import NN, _COMMA_, _d_, _DASH_, _deg_, _degrees_, _DOT_, \ 

71 _0_, _e_, _E_, _EW_, _f_, _F_, _g_, _MINUS_, _N_, \ 

72 _NE_, _NS_, _NSEW_, _NW_, _PERCENTDOTSTAR_, _PLUS_, \ 

73 _PLUSMINUS_, _QUOTE1_, _QUOTE2_, _radians_, _S_, \ 

74 _SE_, _SPACE_, _SW_, _W_ 

75from pygeodesy.lazily import _ALL_LAZY, _ALL_MODS as _MODS 

76# from pygeodesy.namedTuples import LatLon2Tuple # _MODS 

77# from pygeodesy.props import _throwarning # _MODS 

78from pygeodesy.streprs import Fmt, fstr, fstrzs, _0wpF 

79# from pygeodesy.units import Precision_ # _MODS 

80# from pygeodesy.utily import _Wrap # _MODS 

81 

82from math import fabs, modf, radians 

83try: 

84 from string import letters as _LETTERS 

85except ImportError: # Python 3+ 

86 from string import ascii_letters as _LETTERS 

87 

88__all__ = _ALL_LAZY.dms 

89__version__ = '24.10.18' 

90 

91_beyond_ = 'beyond' 

92_deg_min_ = 'deg+min' 

93_sexagecimal_ = 'sexagecimal' 

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

95 

96F_D, F_DM, F_DMS, F_DEG, F_MIN, F_SEC, F_D60, F__E, F__F, F__G, F_RAD = _F_s = ( 

97 _d_, 'dm', 'dms', _deg_, 'min', 'sec', 'd60', _e_, _f_, _g_, 'rad') 

98F_D_, F_DM_, F_DMS_, F_DEG_, F_MIN_, F_SEC_, F_D60_, F__E_, F__F_, F__G_, F_RAD_ = (NN( 

99 _MINUS_, _) for _ in _F_s) 

100F_D__, F_DM__, F_DMS__, F_DEG__, F_MIN__, F_SEC__, F_D60__, F__E__, F__F__, F__G__, F_RAD__ = (NN( 

101 _PLUS_, _) for _ in _F_s) 

102del _F_s 

103_F_DMS = _getPYGEODESY('FMT_FORM') or F_DMS 

104 

105_F_case = {F_D: F_D, F_DEG: F_D, _degrees_: F_D, # unsigned _F_s 

106 F_DM: F_DM, F_MIN: F_DM, _deg_min_: F_DM, 

107 F_D60: F_D60, F_RAD: F_RAD, _radians_: F_RAD, 

108 F__E: F__E, F__F: F__F, F__G: F__G} # default F_DMS 

109_F_prec = {F_D: 6, F_DM: 4, F_DMS: 2, # default precs 

110 F_DEG: 6, F_MIN: 4, F_SEC: 2, F_D60: 0, 

111 F__E: 8, F__F: 8, F__G: 8, F_RAD: 5} 

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

113 

114S_DEG = _DEGREES_ = '°' # ord() = 176 

115S_MIN = _MINUTES_ = '′' # PRIME 

116S_SEC = _SECONDS_ = '″' # DOUBLE_PRIME 

117S_RAD = _RADIANS_ = NN # PYCHOK radians symbol "" 

118S_DMS = True # include DMS symbols 

119S_SEP = NN # separator between deg|min|sec|suffix "" 

120S_NUL = NN # empty string, kept INTERNAL 

121 

122# note: ord(_DEGREES_) == ord('°') == 176, ord('˚') == 730 

123_S_norm = {S_DEG: _DEGREES_, '˚': _DEGREES_, '^': _DEGREES_, # _d_: _DEGREES_, 

124 S_MIN: _MINUTES_, '’': _MINUTES_, _QUOTE1_: _MINUTES_, # _r_: _RADIANS_ 

125 S_SEC: _SECONDS_, '”': _SECONDS_, _QUOTE2_: _SECONDS_} 

126 

127_WINDS = (_N_, 'NbE', 'NNE', 'NEbN', _NE_, 'NEbE', 'ENE', 'EbN', 

128 _E_, 'EbS', 'ESE', 'SEbE', _SE_, 'SEbS', 'SSE', 'SbE', 

129 _S_, 'SbW', 'SSW', 'SWbS', _SW_, 'SWbW', 'WSW', 'WbS', 

130 _W_, 'WbN', 'WNW', 'NWbW', _NW_, 'NWbN', 'NNW', 'NbW') 

131 

132 

133def _D603(sep, s_D=_DOT_, s_M=None, s_S=S_NUL, s_DMS=S_DMS, **unused): 

134 '''(INTERNAL) Get the overridden or default pseudo-C{DMS} symbols. 

135 ''' 

136 if s_DMS: 

137 M = sep if s_M is None else s_M 

138 return s_D, (M or S_NUL), s_S 

139 else: # no overriden symbols 

140 return _DOT_, sep, S_NUL 

141 

142 

143def _DMS3(form, s_D=S_DEG, s_M=S_MIN, s_S=S_SEC, s_DMS=S_DMS, **unused): 

144 '''(INTERNAL) Get the overridden or default C{DMS} symbols. 

145 ''' 

146 return (s_D, s_M, s_S) if s_DMS and form in _F_symb else (S_NUL, S_NUL, S_NUL) 

147 

148 

149def _dms3(d, ddd, p, w): 

150 '''(INTERNAL) Format C{d} as (deg, min, sec) C{str}s with leading zeros. 

151 ''' 

152 d = round(d * _3600_0, p) 

153 d, s = divmod(d, _3600_0) 

154 m, s = divmod(s, _60_0) 

155 return (_0wpF(ddd, 0, d), 

156 _0wpF( 2, 0, m), 

157 _0wpF(w+2, p, s)) 

158 

159 

160def _DR2(s_D=S_NUL, s_R=S_RAD, **unused): 

161 '''(INTERNAL) Get the overridden or default C{D} and C{RAD} symbols. 

162 ''' 

163 return s_D, s_R 

164 

165 

166def _fstrzs(t, **unused): 

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

168 ''' 

169 return t 

170 

171 

172def _split3(strDMS, suffix=_NSEW_): 

173 '''(INTERNAL) Return sign, stripped B{C{strDMS}} and compass point. 

174 ''' 

175 t = strDMS.strip() 

176 s = t[:1] # sign or digit 

177 P = t[-1:] # compass point or digit or dot 

178 t = t.lstrip(_PLUSMINUS_).rstrip(suffix) 

179 return s, t.strip(), P 

180 

181 

182def _toDMS(deg, form, prec, sep, ddd, P, s_D_M_S): # MCCABE 15 in .units 

183 '''(INTERNAL) Convert C{deg} to C{str}, with/-out sign, DMS symbols and/or suffix. 

184 ''' 

185 try: 

186 F = None 

187 deg = float(deg) 

188 

189 f, s = form, form[:1] 

190 if s in _PLUSMINUS_: # signed 

191 n = _MINUS_ if deg < 0 else ( 

192 _PLUS_ if deg > 0 and s == _PLUS_ else NN) 

193 P = NN # no suffix 

194 f = f.lstrip(_PLUSMINUS_) 

195 else: # suffixed 

196 n = NN 

197 if sep and P: # no sep if no suffix 

198 P = NN(sep, P) 

199 try: 

200 F = _F_case[f] # .strip() 

201 except KeyError: 

202 f = f.lower() # .strip() 

203 F = _F_case.get(f, F_DMS) 

204 

205 if prec is None: 

206 z = p = _F_prec.get(F, 6) 

207 else: 

208 z = int(prec) 

209 p = abs(z) 

210 w = p + (1 if p else 0) 

211 z = fstrzs if z > 1 else _fstrzs 

212 d = fabs(deg) 

213 

214 if F is F_DMS: # 'deg+min+sec', default 

215 D, M, S = _DMS3(f, **s_D_M_S) 

216 d, m, s = _dms3(d, ddd, p, w) 

217 t = NN(n, d, D, sep, 

218 m, M, sep, 

219 z(s), S, P) 

220 

221 elif F is F_DM: # 'deg+min' 

222 D, M, _ = _DMS3(f, **s_D_M_S) 

223 d, m = divmod(round(d * _60_0, p), _60_0) 

224 t = NN(n, _0wpF(ddd, 0, d), D, sep, 

225 z(_0wpF(w+2, p, m)), M, P) 

226 

227 elif F is F_D: # 'deg' 

228 D, _, _ = _DMS3(f, **s_D_M_S) 

229 t = NN(n, z(_0wpF(w+ddd, p, d)), D, P) 

230 

231 elif F is F_D60: # 'deg.MM|SSss|' 

232 D, M, S = _D603(sep, **s_D_M_S) 

233 d, m, s = _dms3(d, ddd, p, w) 

234 t = z(s).split(_DOT_) + [S, P] 

235 t = NN(n, d, D, m, M, *t) 

236 

237 elif F is F_RAD: 

238 _, R = _DR2(**s_D_M_S) 

239 r = NN(_PERCENTDOTSTAR_, _F_) % (p, radians(d)) 

240 t = NN(n, z(r), R, P) 

241 

242 else: # F in (F__E, F__F, F__G) 

243 D, _ = _DR2(**s_D_M_S) 

244 d = NN(_PERCENTDOTSTAR_, F) % (p, d) # XXX f? 

245 t = NN(n, z(d, ap1z=F is F__G), D, P) 

246 

247 except Exception as x: 

248 d = {} if F is None else dict(F_=F, P=P) 

249 raise _xError(x, deg=deg, form=form, prec=prec, **d) 

250 

251 return t # NOT unicode in Python 2- 

252 

253 

254def bearingDMS(bearing, form=F_D, prec=None, sep=S_SEP, **s_D_M_S): 

255 '''Convert bearing to a string (without compass point suffix). 

256 

257 @arg bearing: Bearing from North (compass C{degrees360}). 

258 @kwarg form: Format specifier for B{C{deg}} (C{str} or C{F_...}). 

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

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

261 @kwarg s_D_M_S: Optional keyword arguments to override any or cancel all 

262 DMS symbol suffixes, see function L{pygeodesy.toDMS}. 

263 

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

265 

266 @see: Function L{pygeodesy.toDMS} and its B{Notes} for further details. 

267 ''' 

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

269 

270 

271def _clip(angle, limit, units): 

272 '''(INTERNAL) Helper for C{clipDegrees} and C{clipRadians}. 

273 ''' 

274 c = min(limit, max(-limit, angle)) 

275 if c != angle and rangerrors(): 

276 t = _SPACE_(fstr(angle, prec=6, ints=True), _beyond_, 

277 copysign0(limit, angle), units) 

278 raise RangeError(t, txt=None) 

279 return c 

280 

281 

282def clipDegrees(deg, limit): 

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

284 

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

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

287 

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

289 

290 @raise RangeError: If B{C{deg}} outside the valid C{-/+B{limit}} range 

291 and L{rangerrors<pygeodesy.rangerrors>} is C{True}. 

292 ''' 

293 return _clip(deg, limit, _degrees_) if limit and limit > 0 else deg 

294 

295 

296def clipRadians(rad, limit): 

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

298 

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

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

301 

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

303 

304 @raise RangeError: If B{C{rad}} outside the valid C{-/+B{limit}} range 

305 and L{rangerrors<pygeodesy.rangerrors>} is C{True}. 

306 ''' 

307 return _clip(rad, limit, _radians_) if limit and limit > 0 else rad 

308 

309 

310def compassDMS(bearing, form=F_D, prec=None, sep=S_SEP, **s_D_M_S): 

311 '''Convert bearing to a string suffixed with compass point. 

312 

313 @arg bearing: Bearing from North (compass C{degrees360}). 

314 @kwarg form: Format specifier for B{C{deg}} (C{str} or C{F_...}). 

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

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

317 @kwarg s_D_M_S: Optional keyword arguments to override any or cancel all 

318 DMS symbol suffixes, see function L{pygeodesy.toDMS}. 

319 

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

321 

322 @see: Function L{pygeodesy.toDMS} and its B{Notes} for further details. 

323 ''' 

324 b = _umod_360(bearing) 

325 return _toDMS(b, form, prec, sep, 1, compassPoint(b), s_D_M_S) 

326 

327 

328def compassPoint(bearing, prec=3): 

329 '''Convert a C{bearing} from North to a compass point. 

330 

331 @arg bearing: Bearing (compass C{degrees360}). 

332 @kwarg prec: Precision, number of compass point characters: 

333 1 for cardinal or basic winds, 

334 2 for intercardinal or ordinal or principal winds, 

335 3 for secondary-intercardinal or half-winds or 

336 4 for quarter-winds). 

337 

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

339 

340 @raise ValueError: Invalid B{C{bearing}} or B{C{prec}}. 

341 

342 @see: U{Dms.compassPoint 

343 <https://GitHub.com/ChrisVeness/geodesy/blob/master/dms.js>} 

344 and U{Compass rose<https://WikiPedia.org/wiki/Compass_rose>}. 

345 ''' 

346 try: # like .streprs.enstr2 

347 b = _umod_360(bearing) 

348 p = _MODS.units.Precision_(prec, low=1, high=4) \ 

349 if prec != 3 else int(prec) 

350 m = 2 << p 

351 w = 32 // m # if m in (4, 8, 16, 32) 

352 # not round(b), half-even rounding in Python 3+, but 

353 # round-away-from-zero as int(b + copysign0(_0_5, b)) 

354 w *= int(b * m / _360_0 + _0_5) % m 

355 return _WINDS[w] 

356 except Exception as x: 

357 raise _xError(x, bearing=bearing, prec=prec) 

358 

359 

360def degDMS(deg, prec=6, s_D=S_DEG, s_M=S_MIN, s_S=S_SEC, neg=_MINUS_, pos=NN): 

361 '''Convert degrees to a string in degrees, minutes I{or} seconds. 

362 

363 @arg deg: Value in degrees (C{scalar degrees}). 

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

365 Trailing zero decimals are stripped for C{B{prec}=1} 

366 and above, but kept for negative B{C{prec}}. 

367 @kwarg s_D: D symbol for degrees (C{str}). 

368 @kwarg s_M: M symbol for minutes (C{str}) or C{""} aka L{pygeodesy.NN}. 

369 @kwarg s_S: S symbol for seconds (C{str}) or C{""} aka L{pygeodesy.NN}. 

370 @kwarg neg: Optional sign for negative (C{'-'}). 

371 @kwarg pos: Optional sign for positive (C{""}) aka L{pygeodesy.NN}. 

372 

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

374 

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

376 ''' 

377 try: 

378 deg = float(deg) 

379 except Exception as x: 

380 raise _xError(x, deg=deg, prec=prec) 

381 

382 d, s = fabs(deg), s_D 

383 if d < 1: 

384 if s_M: 

385 d *= _60_0 

386 if d < 1 and s_S: 

387 d *= _60_0 

388 s = s_S 

389 else: 

390 s = s_M 

391 elif s_S: 

392 d *= _3600_0 

393 s = s_S 

394 

395 z = int(prec) 

396 t = Fmt.F(d, prec=abs(z)) 

397 if z > 1: 

398 t = fstrzs(t) 

399 n = neg if deg < 0 else pos 

400 return NN(n, t, s) # NOT unicode in Python 2- 

401 

402 

403def latDMS(deg, form=_F_DMS, prec=None, sep=S_SEP, **s_D_M_S): 

404 '''Convert latitude to a string, optionally suffixed with N or S. 

405 

406 @arg deg: Latitude to be formatted (C{scalar degrees}). 

407 @kwarg form: Format specifier for B{C{deg}} (C{str} or C{F_...}). 

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

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

410 @kwarg s_D_M_S: Optional keyword arguments to override any or cancel all 

411 DMS symbol suffixes, see function L{pygeodesy.toDMS}. 

412 

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

414 

415 @see: Function L{pygeodesy.toDMS} and its B{Notes} for further details. 

416 ''' 

417 P = _S_ if deg < 0 else _N_ 

418 return _toDMS(deg, form, prec, sep, 2, P, s_D_M_S) 

419 

420 

421def latlonDMS(lls, **m_form_prec_sep_s_D_M_S): 

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

423 

424 @arg lls: Single (C{LatLon}) or an iterable (C{LatLon}s). 

425 @kwarg m_form_prec_sep_s_D_M_S: Optional keyword arguments C{B{m}eter}, 

426 C{B{form}at}, C{B{prec}ision}, B{C{s_D}}, B{C{s_M}}, B{C{s_S}}, 

427 B{C{s_DMS}} and I{DEPRECATED} C{B{sep}=None}, see function 

428 L{pygeodesy.toDMS} and method C{LatLon.toStr} for more details. 

429 

430 @return: A C{tuple} of C{str}s if B{C{lls}} is a list, sequence, tuple, etc. 

431 of C{LatLon}s or a single C{str} if B{C{lls}} is a single C{LatLon}. 

432 

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

434 and L{pygeodesy.toDMS} and method C{LatLon.toStr}. 

435 

436 @note: Keyword argument C{B{sep}=None} to join the resturned C{tuple} has 

437 been I{DEPRECATED}, use C{B{sep}.join(B{latlonDMS_}(...))} instead. 

438 ''' 

439 sep, kwds = _latlonDMS_sep2(latlonDMS, **m_form_prec_sep_s_D_M_S) 

440 if isLatLon(lls): 

441 t = lls.toStr(**kwds) 

442 elif issequence(lls): 

443 t = tuple(ll.toStr(**kwds) for ll in lls) 

444 if sep: # XXX DEPRECATED, to be removed 

445 t = sep.join(t) 

446 else: 

447 raise _TypeError(lls=lls, **m_form_prec_sep_s_D_M_S) 

448 return t 

449 

450 

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

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

453 

454 @arg lls: The instances (C{LatLon}s), all positional arguments. 

455 @kwarg m_form_prec_sep_s_D_M_S: Optional keyword arguments C{B{m}eter}, 

456 C{B{form}at}, C{B{prec}ision}, B{C{s_D}}, B{C{s_M}}, B{C{s_S}}, 

457 B{C{s_DMS}} and I{DEPRECATED} C{B{sep}=None}, see function 

458 L{pygeodesy.toDMS} and method C{LatLon.toStr} for more details. 

459 

460 @return: A C{tuple} of C{str}s if 2 or more C{LatLon} instances 

461 or a single C{str} if only a single C{LatLon} instance 

462 is given in B{C{lls}}. 

463 

464 @see: Functions L{pygeodesy.latlonDMS}, L{pygeodesy.latDMS} and 

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

466 C{LatLon.toStr}. 

467 

468 @note: Keyword argument C{B{sep}=None} to join the resturned C{tuple} has 

469 been I{DEPRECATED}, use C{B{sep}.join(B{latlonDMS_}(...))} instead. 

470 ''' 

471 sep, kwds = _latlonDMS_sep2(latlonDMS, **m_form_prec_sep_s_D_M_S) 

472 if not lls: 

473 raise _ValueError(lls=lls, **m_form_prec_sep_s_D_M_S) 

474 elif len(lls) < 2: 

475 lls, sep = lls[0], None 

476 t = latlonDMS(lls, **kwds) 

477 return sep.join(t) if sep else t 

478 

479 

480def _latlonDMS_sep2(where, sep=None, **kwds): 

481 '''DEPRECATED, use %r.join(%s(...)) instead.''' 

482 if sep: # PYCHOK no cover 

483 i = _MODS.inters 

484 k = Fmt.EQUAL(sep=repr(sep)) 

485 k = _SPACE_(i._keyword_, i._arg_, k, i._of_) 

486 n = where.__name__ 

487 t = _latlonDMS_sep2.__doc__ % (sep, n) 

488 _MODS.props._throwarning(k, n, t) 

489 return sep, kwds 

490 

491 

492def lonDMS(deg, form=_F_DMS, prec=None, sep=S_SEP, **s_D_M_S): 

493 '''Convert longitude to a string, optionally suffixed with E or W. 

494 

495 @arg deg: Longitude to be formatted (C{scalar degrees}). 

496 @kwarg form: Format specifier for B{C{deg}} (C{str} or C{F_...}). 

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

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

499 @kwarg s_D_M_S: Optional keyword arguments to override any or cancel all 

500 DMS symbol suffixes. 

501 

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

503 

504 @see: Function L{pygeodesy.toDMS} and its B{Notes} for further details. 

505 ''' 

506 P = _W_ if deg < 0 else _E_ 

507 return _toDMS(deg, form, prec, sep, 3, P, s_D_M_S) 

508 

509 

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

511 '''Normalize all degrees, minutes and seconds (DMS) I{symbol} suffixes in 

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

513 

514 @arg strDMS: Original DMS string (C{str}). 

515 @kwarg norm: Optional replacement symbol (C{str}) or C{None} for the default 

516 DMS symbol). Use C{B{norm}=""} to remove all DMS symbols. 

517 @kwarg s_D_M_S: Optional, alternate DMS symbol suffixes C{B{s_D}=}L{S_DEG}, 

518 C{B{s_M}=}L{S_MIN}, C{B{s_S}=}L{S_SEC} and C{B{s_R}=}L{S_RAD} 

519 for radians, each to be replaced by B{C{norm}}. 

520 

521 @return: Normalized DMS (C{str}). 

522 ''' 

523 def _s2S2(s_D=S_DEG, s_M=S_MIN, s_S=S_SEC, s_R=S_RAD): 

524 d = {s_D: S_DEG, s_M: S_MIN, s_S: S_SEC, s_R: S_RAD} 

525 for s, S in _xkwds(d, **_S_norm).items(): 

526 if s: 

527 yield s, S 

528 

529 # XXX strDMS isn't unicode in Python 2- and looping 

530 # thru strDMS will yield each byte, hence the loop 

531 # thru _s2S2 and replacing the DMS symbols in strDMS 

532 

533 if norm is None: # back to default DMS 

534 for s, S in _s2S2(**s_D_M_S): 

535 if s != S: 

536 strDMS = strDMS.replace(s, S) 

537 

538 else: # replace or remove all DMS 

539 n = norm or NN 

540 for s, _ in _s2S2(**s_D_M_S): 

541 if s != n: 

542 strDMS = strDMS.replace(s, n) 

543 if n: 

544 strDMS = strDMS.rstrip(n) # XXX not .strip? 

545 

546 return strDMS # NOT unicode in Python 2- 

547 

548 

549def parseDDDMMSS(strDDDMMSS, suffix=_NSEW_, sep=S_SEP, clip=0, sexagecimal=False): 

550 '''Parse a lat- or longitude represention forms as [D]DDMMSS in degrees. 

551 

552 @arg strDDDMMSS: Degrees in any of several forms (C{str}) and types (C{float}, 

553 C{int}, other). 

554 @kwarg suffix: Optional compass points (C{str}), valid in B{C{strDDDMMSS}}. 

555 @kwarg sep: Optional separator between "[D]DD", "MM", "SS", B{C{suffix}} 

556 (L{S_SEP}) in B{C{strDDDMMSS}}. 

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

558 @kwarg sexagecimal: If C{True}, convert C{"D.MMSS"} or C{float(D.MMSS)} to 

559 C{base-60} "MM" and "SS" digits. See C{form}s L{F_D60}, 

560 L{F_D60_} and L{F_D60__}. 

561 

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

563 

564 @raise ParseError: Invalid B{C{strDDDMMSS}} or B{C{clip}} or the form of 

565 B{C{strDDDMMSS}} is incompatible with or invalid for the 

566 given B{C{suffix}} compass point(s). 

567 

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

569 range and L{rangerrors<pygeodesy.rangerrors>} is C{True}. 

570 

571 @note: Type C{str} values "[D]DD", "[D]DDMM", "[D]DDMMSS" and "[D]DD.MMSS" 

572 for B{C{strDDDMMSS}} are parsed properly only if I{either} unsigned 

573 and suffixed with a valid, compatible, C{cardinal} L{compassPoint} 

574 I{or} signed I{or} unsigned, unsuffixed and with keyword argument 

575 B{C{suffix}="NS"}, B{C{suffix}="EW"} or a compatible L{compassPoint}. 

576 

577 @note: Unlike function L{parseDMS}, type C{float}, C{int} and other non-C{str} 

578 B{C{strDDDMMSS}} values are interpreted as C{form} [D]DDMMSS or 

579 [D]DD.MMSS. For example, C{int(1230)} is returned as 12.5 and I{not 

580 1230.0} degrees. However, C{int(345)} is considered C{form} "DDD" 

581 345 I{and not "DDMM" 0345}, unless B{C{suffix}} specifies the compass 

582 point. Also, C{float(15.0523)} is returned as 15.0523 decimal 

583 degrees and I{not 15°5′23″ sexagecimal}. To consider the latter, use 

584 C{float(15.0523)} or C{"15.0523"} and specify the keyword argument 

585 C{B{sexagecimal}=True}. 

586 

587 @see: Functions L{pygeodesy.parseDMS}, L{pygeodesy.parseDMS2} and 

588 L{pygeodesy.parse3llh}. 

589 ''' 

590 def _DDDMMSS(strDDDMMSS, suffix, sep, clip, sexagecimal): 

591 S = suffix.upper() or _N_ # bool('' in _NE_) is True 

592 if isstr(strDDDMMSS): 

593 t = strDDDMMSS.replace(sep, NN) if sep else strDDDMMSS 

594 n, s, t, f, P, X = _DDDMMSS6(t, S) 

595 if X: 

596 pass 

597 elif not sexagecimal: # try other forms 

598 return _DMS2deg(strDDDMMSS, S, sep, clip, {}) 

599 

600 if sexagecimal: # move decimal dot from ... 

601 n += 4 # ... [D]DD.MMSSs to [D]DDMMSS.s 

602 if n < 6: 

603 t = _SPACE_(_sexagecimal_, 'digits') 

604 raise ParseError(t, n) 

605 z = n - len(f) # zeros to append 

606 t = (f + (_0_ * z)) if z > 0 else _DOT_(f[:n], f[n:]) 

607 f = _0_0 # fraction 

608 else: # float or int to [D]DDMMSS[.fff] 

609 f = float(strDDDMMSS) 

610 n, s, t, f, P = _DDDMMSS5(f, S, sexagecimal) 

611 

612 if n < 4: # [D]DD[.ddd] 

613 if t: 

614 f += float(t) 

615 t = f, 

616 else: 

617 n -= 2 

618 f += float(t[n:]) 

619 if n < 4: # [D]DDMM[.mmm] 

620 t = float(t[:n]), f 

621 else: # [D]DDMMSS[.sss] 

622 d = n - 2 

623 t = float(t[:d]), float(t[d:n]), f 

624 d = _dms2deg(s, P, *t) 

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

626 

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

628 strDDDMMSS=strDDDMMSS, suffix=suffix, sexagecimal=sexagecimal) 

629 

630 

631def _DDDMMSS5(f, S, sexagecimal): 

632 '''(INTERNAL) Partial C{parseDDDMMSS} of C{float}. 

633 ''' 

634 if sexagecimal: 

635 f *= _SEXAGECIMUL 

636 m = 6 

637 else: 

638 m = 0 

639 s = P = _PLUS_ # anything except NN, _S_, _SW_, _W_ 

640 if f < 0: 

641 f = -f 

642 s = _MINUS_ 

643 f, i = modf(f) # returns ... 

644 t = str(int(i)) # ... float(i) 

645 n = len(t) # number of digits to ... 

646 if n < m: # ... required min or ... 

647 t = (_0_ * (m - n)) + t 

648 # ... match the given compass point 

649 elif S in (_NS_ if isodd(n) else _EW_): 

650 t = _0_ + t 

651 # P = S 

652 # elif n > 1: 

653 # P = (_EW_ if isodd(n) else _NS_)[0] 

654 return len(t), s, t, f, P # float(f) fraction 

655 

656 

657def _DDDMMSS6(t, S): 

658 '''(INTERNAL) Partial C{parseDDDMMSS} of C{str}. 

659 ''' 

660 s, t, P = _split3(t, S) 

661 f = t.split(_DOT_) 

662 if len(f) > 2: 

663 raise ParseError('dots', len(f) - 1) 

664 n = len(f[0]) 

665 f = NN.join(f) 

666 if 1 < n < 8 and f.isdigit() and ( # dddN/S/E/W or ddd or +/-ddd 

667 (P in S and s.isdigit()) or 

668 (P.isdigit() and S in _WINDS # PYCHOK indent 

669 and (s in _PLUSMINUS_ or s.isdigit()))): 

670 # check [D]DDMMSS form and compass point 

671 X = _EW_ if isodd(n) else _NS_ 

672 if not (P in X or (S in X and (P.isdigit() or P == _DOT_))): 

673 t = parseDDDMMSS.__name__[5 if isodd(n) else 6:] 

674 t = _SPACE_('form', t, 'applies', _DASH_.join(X)) 

675 raise ParseError(t) 

676 else: 

677 X = NN 

678 return n, s, t, f, P, X # str(f) fraction 

679 

680 

681def _DMS2deg(strDMS, suffix, sep, clip, s_D_M_S): 

682 '''(INTERNAL) Helper for C{parseDDDMMSS} and C{parseDMS}. 

683 ''' 

684 try: 

685 d = float(strDMS) 

686 except (TypeError, ValueError): 

687 s, t, P = _split3(strDMS, suffix.upper()) 

688 if sep: # remove all DMS symbols 

689 t = t.replace(sep, _SPACE_) 

690 t = normDMS(t, norm=NN, **s_D_M_S) 

691 else: # replace all DMS symbols 

692 t = normDMS(t, norm=_SPACE_, **s_D_M_S) 

693 t = t.strip().split() 

694 d = _dms2deg(s, P, *map(float, t)) 

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

696 

697 

698def _dms2deg(n, P, d, m=_0_0, s=_0_0): 

699 '''(INTERNAL) Helper for C{parseDDDMMSS}, C{parseRad} and C{_DMS2deg}. 

700 ''' 

701 m += s / _60_0 

702 d += m / _60_0 

703 if n == _MINUS_ or (P and P in _SW_): 

704 d = _neg(d) 

705 return d 

706 

707 

708def parseDMS(strDMS, suffix=_NSEW_, sep=S_SEP, clip=0, **s_D_M_S): # MCCABE 14 

709 '''Parse a lat- or longitude representation in C{degrees}. 

710 

711 This is very flexible on formats, allowing signed decimal degrees, degrees 

712 and minutes or degrees minutes and seconds optionally suffixed by a cardinal 

713 compass point. 

714 

715 A variety of symbols, separators and suffixes are accepted, for example 

716 "3°37′09″W". Minutes and seconds may be omitted. 

717 

718 @arg strDMS: Degrees in any of several forms (C{str}) and types (C{float}, 

719 C{int}, other). 

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

721 @kwarg sep: Optional separator between C{deg°}, C{min′}, C{sec″}, 

722 B{C{suffix}} (C{''}). 

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

724 @kwarg s_D_M_S: Optional, alternate DMS symbol suffixes for degrees 

725 C{B{s_D}=}L{S_DEG}, minutes C{B{s_M}=}L{S_MIN} and 

726 seconds C{B{s_S}=}L{S_SEC}, see function L{pygeodesy.toDMS}. 

727 

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

729 

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

731 

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

733 and L{rangerrors<pygeodesy.rangerrors>} is C{True}. 

734 

735 @note: Unlike function L{parseDDDMMSS}, type C{float}, C{int} and other non-C{str} 

736 B{C{strDMS}} values are considered decimal (and not sexagecimal) degrees. 

737 For example, C{int(1230)} is returned as 1230.0 I{and not as 12.5} degrees 

738 and C{float(345)} as 345.0 I{and not as 3.75} degrees! 

739 

740 @see: Functions L{pygeodesy.parseDDDMMSS}, L{pygeodesy.parseDMS2}, 

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

742 ''' 

743 return _parseX(_DMS2deg, strDMS, suffix, sep, clip, s_D_M_S, strDMS=strDMS, suffix=suffix) 

744 

745 

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

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

748 

749 @arg strLat: Latitude in any of several forms (C{str} or C{degrees}). 

750 @arg strLon: Longitude in any of several forms (C{str} or C{degrees}). 

751 @kwarg sep: Optional separator between deg°, min′, sec″, suffix (C{''}). 

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

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

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

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

756 @kwarg s_D_M_S: Optional, alternate DMS symbol suffixes for degrees 

757 C{B{s_D}=}L{S_DEG}, minutes C{B{s_M}=}L{S_MIN} and 

758 seconds C{B{s_S}=}L{S_SEC}, see function L{pygeodesy.toDMS}. 

759 

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

761 

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

763 

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

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

766 and L{rangerrors<pygeodesy.rangerrors>} is C{True}. 

767 

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

769 

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

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

772 ''' 

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

774 

775 

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

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

778 ''' 

779 if wrap: 

780 _W = _MODS.utily._Wrap 

781 ll = _W.latlon(parseDMS(strLat, suffix=_NS_, **kwds), 

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

783 else: 

784 # if wrap is None: 

785 # clipLat = clipLon = 0 

786 ll = (parseDMS(strLat, suffix=_NS_, clip=clipLat, **kwds), 

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

788 return _MODS.namedTuples.LatLon2Tuple(*ll) 

789 

790 

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

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

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

794 

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

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

797 another separator. 

798 

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

800 one ends with the proper compass point. 

801 

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

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

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

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

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

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

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

809 @kwarg s_D_M_S: Optional, alternate DMS symbol suffixes for degrees 

810 C{B{s_D}=}L{S_DEG}, minutes C{B{s_M}=}L{S_MIN} and 

811 seconds C{B{s_S}=}L{S_SEC}, see function L{pygeodesy.toDMS}. 

812 

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

814 C{degrees} and C{float}. 

815 

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

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

818 and L{rangerrors<pygeodesy.rangerrors>} is C{True}. 

819 

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

821 

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

823 

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

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

826 ''' 

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

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

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

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

831 else: 

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

833 if len(ll) != 2: 

834 raise ValueError 

835 

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

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

838 a, b = b, a 

839 t = _2Tuple(a, b, clipLat, clipLon, wrap, **s_D_M_S) 

840 return t.to3Tuple(h) 

841 

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

843 

844 

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

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

847 

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

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

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

851 

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

853 

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

855 

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

857 range and L{rangerrors<pygeodesy.rangerrors>} is C{True}. 

858 ''' 

859 def _Rad(strRad, suffix, clip): 

860 try: 

861 r = float(strRad) 

862 except (TypeError, ValueError): 

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

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

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

866 

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

868 

869 

870def precision(form, prec=None): 

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

872 

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

874 L{F_D60}, L{F__E}, L{F__F}, L{F__G} or L{F_RAD} (C{str}). 

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

876 Trailing zero decimals are stripped for C{B{prec}=1} 

877 and above, but kept for negative B{C{prec}}. 

878 

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

880 

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

882 outside range C{-9..+9}. 

883 ''' 

884 try: 

885 p = _F_prec[form] 

886 except KeyError: 

887 raise _ValueError(form=form) 

888 if prec is not None: # set as default 

889 P_ = _MODS.units.Precision_ 

890 _F_prec[form] = P_(prec, low=-9, high=9) 

891 return p 

892 

893 

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

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

896 

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

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

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

900 L{F_RAD}, L{F_D_}, L{F_DM_}, L{F_DMS_}, L{F_DEG_}, L{F_MIN_}, L{F_SEC_}, 

901 L{F_D60_}, L{F__E_}, L{F__F_}, L{F__G_}, L{F_RAD_}, L{F_D__}, L{F_DM__}, 

902 L{F_DMS__}, L{F_DEG__}, L{F_MIN__}, L{F_SEC__}, L{F_D60__}, L{F__E__}, 

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

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

905 decimals are stripped for C{B{prec}=1} and above, but kept for negative 

906 B{C{prec}}. 

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

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

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

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

911 @kwarg s_D_M_S: Optional keyword arguments C{B{s_D}=}L{S_DEG}, C{B{s_M}=}L{S_MIN} 

912 C{B{s_S}=}L{S_SEC}, C{B{s_R}=}L{S_RAD} and C{B{s_DMS}=True} to 

913 override any or cancel all DMS symbol suffixes. See the B{Notes} 

914 below. 

915 

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

917 

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

919 other C{*DMS} functions by using optional keyword argments C{B{s_D}="d"}, 

920 C{B{s_M}="'"} respectively C{B{s_S}='"'}. Using B{C{s_DMS}=None} cancels 

921 all C{DMS} symbols to C{""} aka L{pygeodesy.NN}. 

922 

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

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

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

926 

927 @note: Formats B{C{F__E}}, B{C{F__F}} and B{C{F__G}} is extended with a C{D}-only 

928 symbol suffix if defined with keyword argument C{B{s_D}=}L{pygeodesy.NN}. 

929 Likewise for B{C{F_RAD}} forms with keyword argument C{B{s_R}=}L{S_RAD}. 

930 

931 @see: Function L{pygeodesy.degDMS} 

932 ''' 

933 s = form[:1] 

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

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

936 if deg < 0 and neg: 

937 t = neg + t 

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

939 t = pos + t 

940 return t 

941 

942# **) MIT License 

943# 

944# Copyright (C) 2016-2025 -- mrJean1 at Gmail -- All Rights Reserved. 

945# 

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

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

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

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

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

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

952# 

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

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

955# 

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

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

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

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

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

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

962# OTHER DEALINGS IN THE SOFTWARE.