1

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

3

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

5

6 Functions to parse and format bearing, compass, lat- and longitudes in various forms of

7 degrees, minutes and seconds with or without degrees, minute and second symbols plus a

8 compass point suffix, including parsing of C{decimal} and C{sexagecimal} degrees.

9

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

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

12 U{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

61 from pygeodesy.basics import copysign0, isodd, issequence, isstr, map2, \

62 neg as _neg # in .ups

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

64 from pygeodesy.errors import ParseError, _parseX, RangeError, rangerrors, _TypeError, \

65 _ValueError, _xkwds, _xkwds_get

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

67 _0_, _e_, _E_, _EW_, _f_, _F_, _g_, _MINUS_, _N_, _NE_, \

68 _NS_, _NSEW_, _NW_, _PERCENTDOTSTAR_, _PLUS_, _PLUSMINUS_, \

69 _QUOTE1_, _QUOTE2_, _radians_, _S_, _SE_, _SPACE_, _SW_, _W_

70 from pygeodesy.lazily import _ALL_LAZY, _ALL_MODS as _MODS

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

72# from pygeodesy.utily import _Wrap # _MODS

73

74 from math import fabs, modf, radians

75 try:

76 from string import letters as _LETTERS

77 except ImportError: # Python 3+

78 from string import ascii_letters as _LETTERS

79

80__all__ = _ALL_LAZY.dms

81__version__ = '23.05.04'

82

83_beyond_ = 'beyond'

84_DDDMMSS_ = 'DDDMMSS'

85_deg_min_ = 'deg+min'

86_keyword_ = 'keyword'

87_of_ = 'of'

88_SDIGITS_ = '-0123456789+'

89_sexagecimal_ = 'sexagecimal'

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

91

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

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

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

95 _MINUS_, _) for _ in _F_s)

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

97 _PLUS_, _) for _ in _F_s)

98 del _F_s

99

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

101 F_DM: F_DM, F_MIN: F_DM, _deg_min_: F_DM,

102 F_D60: F_D60, F_RAD: F_RAD, _radians_: F_RAD,

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

104

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

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

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

108

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

110

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

112 S_MIN = _MINUTES_ = '′' # PRIME

113 S_SEC = _SECONDS_ = '″' # DOUBLE_PRIME

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

115 S_DMS = True # include DMS symbols

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

117 S_NUL = NN # empty string, kept INTERNAL

118

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

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

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

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

123

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

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

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

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

128

129

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

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

132 '''

133 if s_DMS:

134 M = sep if s_M is None else s_M

135 return s_D, (M or S_NUL), s_S

136 else: # no overriden symbols

137 return _DOT_, sep, S_NUL

138

139

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

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

142 '''

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

144

145

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

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

148 '''

149 d, s = divmod(round(d * _3600_0, p), _3600_0)

150 m, s = divmod(s, _60_0)

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

152 _0wpF( 2, 0, m),

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

154

155

156 def _fstrzs(t, **unused):

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

158 '''

159 return t

160

161

162 def _split3(strDMS, suffix=_NSEW_):

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

164 '''

165 t = strDMS.strip()

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

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

168 t = t.lstrip(_PLUSMINUS_).rstrip(suffix).strip()

169 return s, t, P

170

171

172 def _toDMS(deg, form, prec, sep, ddd, suff, s_D_M_S): # MCCABE 13 in .units

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

174 '''

175 try:

176 deg = float(deg)

177 except (TypeError, ValueError) as x:

178 raise _ValueError(deg=deg, form=form, prec=prec, cause=x)

179

180 if form[:1] in _PLUSMINUS_: # signed

181 sign = _MINUS_ if deg < 0 else (

182 _PLUS_ if deg > 0 and form[:1] == _PLUS_ else NN)

183 form = form.lstrip(_PLUSMINUS_)

184 suff = NN # no suffix if signed

185 else: # suffixed

186 sign = NN # no sign if suffixed

187 if suff and sep: # no sep if no suffix

188 suff = NN(sep, suff)

189 try:

190 F = _F_case[form] # .strip()

191 except KeyError:

192 form = form.lower() # .strip()

193 F = _F_case.get(form, F_DMS)

194

195 if prec is None:

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

197 else:

198 z = int(prec)

199 p = abs(z)

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

201 z = fstrzs if z > 1 else _fstrzs

202 d = fabs(deg)

203

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

205 D, M, S = _DMS3(form, **s_D_M_S)

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

207 t = NN(sign, d, D, sep,

208 m, M, sep,

209 z(s), S, suff)

210

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

212 D, M, _ = _DMS3(form, **s_D_M_S)

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

214 t = NN(sign, _0wpF(ddd, 0, d), D, sep,

215 z(_0wpF(w+2, p, m)), M, suff)

216

217 elif F is F_D: # 'deg'

218 D, _, _ = _DMS3(form, **s_D_M_S)

219 t = NN(sign, z(_0wpF(w+ddd, p, d)), D, suff)

220

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

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

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

224 t = z(s).split(_DOT_) + [S, suff]

225 t = NN(sign, d, D, m, M, *t)

226

227 elif F is F_RAD:

228 R = _xkwds_get(s_D_M_S, s_R=S_RAD)

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

230 t = NN(sign, z(r), R, suff)

231

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

233 D = _xkwds_get(s_D_M_S, s_D=S_NUL)

234 d = NN(_PERCENTDOTSTAR_, F) % (p, d) # XXX form?

235 t = NN(sign, z(d, ap1z=F is F__G), D, suff)

236

237 return t # NOT unicode in Python 2-

238

239

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

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

242

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

259 respectively L{S_SEC}.

260

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

262

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

264 '''

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

266

267

268 def _clip(angle, limit, units):

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

270 '''

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

272 if c != angle and rangerrors():

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

274 copysign0(limit, angle), units)

275 raise RangeError(t, txt=None)

276 return c

277

278

279 def clipDegrees(deg, limit):

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

281

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

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

284

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

286

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

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

289 '''

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

291

292

293 def clipRadians(rad, limit):

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

295

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

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

298

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

300

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

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

303 '''

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

305

306

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

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

309

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

326 respectively L{S_SEC}.

327

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

329

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

331 '''

332 b = _umod_360(bearing)

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

334

335

336 def compassPoint(bearing, prec=3):

337 '''Convert bearing to a compass point.

338

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

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

341 1 for cardinal or basic winds,

342 2 for intercardinal or ordinal or principal winds,

343 3 for secondary-intercardinal or half-winds or

344 4 for quarter-winds).

345

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

347

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

349

350 @see: U{Dms.compassPoint

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

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

353

354 @example:

355

356 >>> p = compassPoint(24, 1) # 'N'

357 >>> p = compassPoint(24, 2) # 'NE'

358 >>> p = compassPoint(24, 3) # 'NNE'

359 >>> p = compassPoint(24) # 'NNE'

360 >>> p = compassPoint(11, 4) # 'NbE'

361 >>> p = compassPoint(30, 4) # 'NEbN'

362

363 >>> p = compassPoint(11.249) # 'N'

364 >>> p = compassPoint(11.25) # 'NNE'

365 >>> p = compassPoint(-11.25) # 'N'

366 >>> p = compassPoint(348.749) # 'NNW'

367 '''

368 try: # like .streprs.enstr2

369 m = 2 << prec

370 if m in (4, 8, 16, 32):

371 w = 32 // m

372 # not round(), i.e. half-even rounding in Python 3+,

373 # but round-away-from-zero as int(b + 0.5) iff b is

374 # non-negative, otherwise int(b + copysign0(_0_5, b))

375 w *= int(_umod_360(bearing) * m / _360_0 + _0_5) % m

376 return _WINDS[w]

377

378 raise ValueError

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

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

381

382

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

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

385

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

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

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

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

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

391 @kwarg s_M: M symbol for minutes (C{str}) or C{""}.

392 @kwarg s_S: S symbol for seconds (C{str}) or C{""}.

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

394 @kwarg pos: Optional sign for positive (C{''}).

395

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

397

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

399 '''

400 try:

401 deg = float(deg)

402 except (TypeError, ValueError) as x:

403 raise _ValueError(deg=deg, prec=prec, cause=x)

404

405 d, s = fabs(deg), s_D

406 if d < 1:

407 if s_M:

408 d *= _60_0

409 if d < 1 and s_S:

410 d *= _60_0

411 s = s_S

412 else:

413 s = s_M

414 elif s_S:

415 d *= _3600_0

416 s = s_S

417

418 z = int(prec)

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

420 if z > 1:

421 t = fstrzs(t)

422 n = neg if deg < 0 else pos

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

424

425

426 def latDMS(deg, form=F_DMS, prec=None, sep=S_SEP, **s_D_M_S):

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

428

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

445 respectively L{S_SEC}.

446

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

448

449 @see: Functions L{pygeodesy.toDMS} and L{pygeodesy.lonDMS}.

450 '''

451 p = _S_ if deg < 0 else _N_

452 return _toDMS(deg, form, prec, sep, 2, p, s_D_M_S)

453

454

455 def latlonDMS(lls, **m_form_prec_sep_s_D_M_S):

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

457

458 @arg lls: Single or list, sequence, tuple, etc. (C{LatLon}s).

459 @kwarg m_form_prec_sep_s_D_M_S: Optional C{B{m}eter}, C{B{form}at},

460 C{B{prec}ision}, I{DEPRECATED} B{C{sep}}, B{C{s_D}}, B{C{s_M}},

461 B{C{s_S}} and B{C{s_DMS}} keyword arguments, see method

462 C{LatLon.toStr} and functions L{pygeodesy.latDMS} and

463 L{pygeodesy.lonDMS}.

464

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

466 tuple, etc. of C{LatLon} instances or a single C{str}

467 if B{C{lls}} is a single C{LatLon}.

468

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

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

471 C{LatLon.toStr}.

472

473 @note: Keyword argument C{B{sep}=None} to return a C{str}ing

474 instead of the C{tuple}, has been I{DEPRECATED}, use

475 C{B{sep}.join(B{latlonDMS_}(...))}.

476 '''

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

478 if isinstance(lls, _MODS.latlonBase.LatLonBase):

479 t = lls.toStr(**kwds)

480 elif issequence(lls):

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

482 if sep: # XXX TO BE REMOVED

483 t = sep.join(t)

484 else:

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

486 return t

487

488

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

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

491

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

493 @kwarg m_form_prec_sep_s_D_M_S: Optional C{B{m}eter}, C{B{form}at},

494 C{B{prec}ision}, I{DEPRECATED} B{C{sep}}, B{C{s_D}}, B{C{s_M}},

495 B{C{s_S}} and B{C{s_DMS}} keyword arguments, see method

496 C{LatLon.toStr} and functions L{pygeodesy.latDMS} and

497 L{pygeodesy.lonDMS}.

498

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

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

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

502

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

504

505 @note: Keyword argument C{B{sep}=None} to return a C{str}ing

506 instead of the C{tuple}, has been I{DEPRECATED}, use

507 C{B{sep}.join(B{latlonDMS_}(...))}.

508 '''

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

510 if not lls:

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

512 elif len(lls) < 2:

513 lls, sep = lls[0], None

514 t = latlonDMS(lls, **kwds)

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

516

517

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

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

520 if sep:

521 k = _SPACE_(_keyword_, _arg_, Fmt.EQUAL(sep=repr(sep)), _of_)

522 n = where.__name__

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

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

525 return sep, kwds

526

527

528 def lonDMS(deg, form=F_DMS, prec=None, sep=S_SEP, **s_D_M_S):

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

530

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

547 respectively L{S_SEC}.

548

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

550

551 @see: Functions L{pygeodesy.toDMS} and L{pygeodesy.latDMS}.

552 '''

553 p = _W_ if deg < 0 else _E_

554 return _toDMS(deg, form, prec, sep, 3, p, s_D_M_S)

555

556

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

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

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

560

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

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

563 the default DMS symbols). Use C{B{norm}=""} to

564 remove all DMS symbols.

565 @kwarg s_D_M_S: Optional, alternate DMS symbols C{B{s_D}=str},

566 C{B{s_M}=str}, C{B{s_S}=str} and/or C{B{s_R}=str}

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

568

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

570 '''

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

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

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

574 if s:

575 yield s, S

576

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

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

579 # thru _s2S2 and replacing the DMS symbols in strDMS

580

581 if norm is None: # back to default DMS

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

583 if s != S:

584 strDMS = strDMS.replace(s, S)

585

586 else: # replace or remove all DMS

587 n = norm or NN

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

589 if s != n:

590 strDMS = strDMS.replace(s, n)

591 if n:

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

593

594 return strDMS # NOT unicode in Python 2-

595

596

597 def parseDDDMMSS(strDDDMMSS, suffix=_NSEW_, sep=S_SEP, clip=0, sexagecimal=False): # MCCABE 14

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

599

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

601 C{int}, other).

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

603 @kwarg sep: Optional separator between "[D]DD", "MM", "SS", B{C{suffix}} (L{S_SEP}).

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

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

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

607 L{F_D60_} and L{F_D60__}.

608

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

610

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

612 B{C{strDDDMMSS}} is incompatible with the suffixed or

613 B{C{suffix}} compass point.

614

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

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

617

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

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

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

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

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

623

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

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

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

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

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

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

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

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

632 C{B{sexagecimal}=True}.

633

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

635 L{pygeodesy.parse3llh}.

636 '''

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

638 S = suffix.upper()

639 if isstr(strDDDMMSS):

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

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

642 f = t.split(_DOT_)

643 n = len(f[0])

644 f = NN.join(f)

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

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

647 (P.isdigit() and s in _SDIGITS_ # PYCHOK indent

648 and S in _WINDS)):

649 # check [D]DDMMSS form and compass point

650 X = _EW_ if isodd(n) else _NS_

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

652 t = _DDDMMSS_[int(X is _NS_):(n | 1)], _DASH_.join(X)

653 raise ParseError('form %s applies %s' % t)

654 elif not sexagecimal: # try other forms

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

656

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

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

659 if n < 6:

660 raise ParseError('%s digits (%s)' % (_sexagecimal_, n))

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

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

663 f = _0_0 # fraction

664

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

666 f, m = float(strDDDMMSS), 0

667 if sexagecimal:

668 f *= _SEXAGECIMUL

669 m = 6

670 s = P = _0_ # anything except NN, _S_, _SW_, _W_

671 if f < 0:

672 f = -f

673 s = _MINUS_

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

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

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

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

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

679 # ... match the given compass point

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

681 t = _0_ + t

682 # P = S

683 # elif n > 1:

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

685 n = len(t)

686

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

688 t = (float(t) + f),

689 else:

690 f += float(t[n-2:])

691 if n < 6: # [D]DDMM[.mmm]

692 t = float(t[:n-2]), f

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

694 t = float(t[:n-4]), float(t[n-4:n-2]), f

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

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

697

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

699 strDDDMMSS=strDDDMMSS, suffix=suffix, sexagecimal=sexagecimal)

700

701

702 def _dms2deg(s, P, deg, min=_0_0, sec=_0_0):

703 '''(INTERNAL) Helper for C{parseDDDMMSS} and C{_DMS2deg}.

704 '''

705 deg += (min + (sec / _60_0)) / _60_0

706 if s == _MINUS_ or (P and P in _SW_):

707 deg = _neg(deg)

708 return deg

709

710

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

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

713 '''

714 try:

715 d = float(strDMS)

716

717 except (TypeError, ValueError):

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

719 if sep: # remove all DMS symbols

720 t = t.replace(sep, _SPACE_)

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

722 else: # replace all DMS symbols

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

724 t = map2(float, t.strip().split())

725 d = _dms2deg(s, P, *t[:3])

726

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

728

729

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

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

732

733 This is very flexible on formats, allowing signed decimal

734 degrees, degrees and minutes or degrees minutes and seconds

735 optionally suffixed by a cardinal compass point.

736

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

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

739

740 @arg strDMS: Degrees in any of several forms (C{str}) and

741 types (C{float}, C{int}, other).

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

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

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

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

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

747

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

749

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

751

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

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

754

755 @note: Unlike function L{parseDDDMMSS}, type C{float}, C{int} and other

756 non-C{str} B{C{strDMS}} values are considered decimal (and not

757 sexagecimal) degrees. For example, C{int(1230)} is returned

758 as 1230.0 I{and not as 12.5} degrees and C{float(345)} as 345.0

759 I{and not as 3.75} degrees!

760

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

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

763 '''

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

765

766

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

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

769

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

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

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

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

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

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

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

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

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

779

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

781

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

783

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

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

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

787

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

789

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

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

792 '''

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

794

795

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

797 '''(INTERNAL) Helper for C{parseDMS2} and C{parsellh3}.

798 '''

799 if wrap:

800 _W = _MODS.utily._Wrap

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

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

803 else:

804 # if wrap is None:

805 # clipLat = clipLon = 0

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

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

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

809

810

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

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

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

814

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

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

817 another separator.

818

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

820 one ends with the proper compass point.

821

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

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

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

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

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

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

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

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

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

831

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

833 C{degrees} and C{float}.

834

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

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

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

838

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

840

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

842

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

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

845

846 @example:

847

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

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

850 '''

851

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

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

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

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

856 else:

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

858 if len(ll) != 2:

859 raise ValueError

860

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

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

863 a, b = b, a

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

865

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

867

868

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

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

871

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

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

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

875

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

877

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

879

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

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

882 '''

883 def _Rad(strRad, suffix, clip):

884 try:

885 r = float(strRad)

886

887 except (TypeError, ValueError):

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

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

890

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

892

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

894

895

896 def precision(form, prec=None):

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

898

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

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

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

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

903 default). Trailing zero decimals are stripped

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

905 for negative B{C{prec}}.

906

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

908

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

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

911 '''

912 try:

913 p = _F_prec[form]

914 except KeyError:

915 raise _ValueError(form=form)

916

917 if prec is not None: # set as default

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

919

920 return p

921

922

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

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

925

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

946

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

948

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

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

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

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

953 C{B{S_NUL}=NN}.

954

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

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

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

958

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

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

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

962

963 @see: Function L{pygeodesy.degDMS}

964 '''

965 s = form[:1]

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

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

968 if deg < 0 and neg:

969 t = neg + t

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

971 t = pos + t

972 return t

973

974# **) MIT License

975#

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

977#

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

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

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

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

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

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

984#

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

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

987#

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

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

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

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

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

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

994# OTHER DEALINGS IN THE SOFTWARE.