Coverage for pygeodesy/units.py: 94%

299 statements  

« prev     ^ index     » next       coverage.py v7.2.2, created at 2023-10-04 12:08 -0400

1 

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

3 

4u'''Various units, all sub-classes of C{Float}, C{Int} and 

5C{Str} from basic C{float}, C{int} respectively C{str} to 

6named units as L{Degrees}, L{Feet}, L{Meter}, L{Radians}, etc. 

7''' 

8 

9from pygeodesy.basics import isstr, issubclassof, signOf 

10from pygeodesy.constants import EPS, EPS1, PI, PI2, PI_2, \ 

11 _umod_360, _0_0, _0_001, \ 

12 _0_5, INT0 # PYCHOK for .mgrs, .namedTuples 

13from pygeodesy.dms import F__F, F__F_, parseDMS, parseRad, \ 

14 S_NUL, S_SEP, _toDMS 

15from pygeodesy.errors import _AssertionError, _IsnotError, TRFError, \ 

16 UnitError, _xkwds_popitem 

17from pygeodesy.interns import NN, _band_, _bearing_, _degrees_, _degrees2_, \ 

18 _distance_, _E_, _easting_, _epoch_, _EW_, \ 

19 _feet_, _height_, _lam_, _lat_, \ 

20 _LatLon_, _lon_, _meter_, _meter2_, _N_, \ 

21 _northing_, _NS_, _NSEW_, _number_, _PERCENT_, \ 

22 _phi_, _precision_, _radians_, _radians2_, \ 

23 _radius_, _S_, _scalar_, _units_, \ 

24 _W_, _zone_, _std_ # PYCHOK used! 

25from pygeodesy.lazily import _ALL_DOCS, _ALL_LAZY, _ALL_MODS as _MODS, _getenv 

26from pygeodesy.props import Property_RO 

27# from pygeodesy.streprs import Fmt, fstr # from .unitsBase 

28from pygeodesy.unitsBase import _Error, Float, Fmt, fstr, Int, _NamedUnit, \ 

29 Radius, Str # PYCHOK shared .namedTuples 

30from math import degrees, radians 

31 

32__all__ = _ALL_LAZY.units 

33__version__ = '23.05.12' 

34 

35_negative_falsed_ = 'negative, falsed' 

36 

37 

38class Float_(Float): 

39 '''Named C{float} with optional C{low} and C{high} limit. 

40 ''' 

41 def __new__(cls, arg=None, name=NN, Error=UnitError, low=EPS, high=None, **name_arg): 

42 '''New C{Float_} instance. 

43 

44 @arg cls: This class (C{Float_} or sub-class). 

45 @kwarg arg: The value (any C{type} convertable to C{float}). 

46 @kwarg name: Optional instance name (C{str}). 

47 @kwarg Error: Optional error to raise, overriding the default L{UnitError}. 

48 @kwarg low: Optional lower B{C{arg}} limit (C{float} or C{None}). 

49 @kwarg high: Optional upper B{C{arg}} limit (C{float} or C{None}). 

50 @kwarg name_arg: Optional C{name=arg} keyword argument, inlieu of B{C{name}} 

51 and B{C{arg}}. 

52 

53 @returns: A C{Float_} instance. 

54 

55 @raise Error: Invalid B{C{arg}} or B{C{arg}} below B{C{low}} or above B{C{high}}. 

56 ''' 

57 if name_arg: 

58 name, arg = _xkwds_popitem(name_arg) 

59 self = Float.__new__(cls, arg=arg, name=name, Error=Error) 

60 if (low is not None) and self < low: 

61 txt = Fmt.limit(below=Fmt.g(low, prec=6, ints=isinstance(self, Epoch))) 

62 elif (high is not None) and self > high: 

63 txt = Fmt.limit(above=Fmt.g(high, prec=6, ints=isinstance(self, Epoch))) 

64 else: 

65 return self 

66 raise _Error(cls, arg, name, Error, txt=txt) 

67 

68 

69class Int_(Int): 

70 '''Named C{int} with optional limits C{low} and C{high}. 

71 ''' 

72 def __new__(cls, arg=None, name=NN, Error=UnitError, low=0, high=None, **name_arg): 

73 '''New named C{int} instance with limits. 

74 

75 @kwarg cls: This class (C{Int_} or sub-class). 

76 @arg arg: The value (any C{type} convertable to C{int}). 

77 @kwarg name: Optional instance name (C{str}). 

78 @kwarg Error: Optional error to raise, overriding the default C{UnitError}. 

79 @kwarg low: Optional lower B{C{arg}} limit (C{int} or C{None}). 

80 @kwarg high: Optional upper B{C{arg}} limit (C{int} or C{None}). 

81 @kwarg name_arg: Optional C{name=arg} keyword argument, inlieu of B{C{name}} 

82 and B{C{arg}}. 

83 

84 @returns: An L{Int_} instance. 

85 

86 @raise Error: Invalid B{C{arg}} or B{C{arg}} below B{C{low}} or above B{C{high}}. 

87 ''' 

88 if name_arg: 

89 name, arg = _xkwds_popitem(name_arg) 

90 self = Int.__new__(cls, arg=arg, name=name, Error=Error) 

91 if (low is not None) and self < low: 

92 txt = Fmt.limit(below=low) 

93 elif (high is not None) and self > high: 

94 txt = Fmt.limit(above=high) 

95 else: 

96 return self 

97 raise _Error(cls, arg, name, Error, txt=txt) 

98 

99 

100class Bool(Int, _NamedUnit): 

101 '''Named C{bool}, a sub-class of C{int} like Python's C{bool}. 

102 ''' 

103 # _std_repr = True # set below 

104 _bool_True_or_False = None 

105 

106 def __new__(cls, arg=None, name=NN, Error=UnitError, **name_arg): 

107 '''New C{Bool} instance. 

108 

109 @kwarg cls: This class (C{Bool} or sub-class). 

110 @kwarg arg: The value (any C{type} convertable to C{bool}). 

111 @kwarg name: Optional instance name (C{str}). 

112 @kwarg Error: Optional error to raise, overriding the default 

113 L{UnitError}. 

114 @kwarg name_arg: Optional C{name=arg} keyword argument, inlieu 

115 of B{C{name}} and B{C{arg}}. 

116 

117 @returns: A L{Bool}, a C{bool}-like instance. 

118 

119 @raise Error: Invalid B{C{arg}}. 

120 ''' 

121 if name_arg: 

122 name, arg = _xkwds_popitem(name_arg) 

123 try: 

124 b = bool(arg) 

125 except Exception as x: # XXX not ... as x: 

126 raise _Error(cls, arg, name, Error, x=x) 

127 

128 self = Int.__new__(cls, b, name=name, Error=Error) 

129 self._bool_True_or_False = b 

130 return self 

131 

132 # <https://StackOverflow.com/questions/9787890/assign-class-boolean-value-in-python> 

133 def __bool__(self): # PYCHOK Python 3+ 

134 return self._bool_True_or_False 

135 

136 __nonzero__ = __bool__ # PYCHOK Python 2- 

137 

138 def toRepr(self, std=False, **unused): # PYCHOK **unused 

139 '''Return a representation of this C{Bool}. 

140 

141 @kwarg std: Use the standard C{repr} or the named 

142 representation (C{bool}). 

143 

144 @note: Use C{env} variable C{PYGEODESY_BOOL_STD_REPR=std} 

145 prior to C{import pygeodesy} to get the standard 

146 C{repr} or set property C{std_repr=False} to always 

147 get the named C{toRepr} representation. 

148 ''' 

149 r = repr(self._bool_True_or_False) 

150 return r if std else self._toRepr(r) 

151 

152 def toStr(self, **unused): # PYCHOK **unused 

153 '''Return this C{Bool} as standard C{str}. 

154 ''' 

155 return str(self._bool_True_or_False) 

156 

157 

158class Band(Str): 

159 '''Named C{str} representing a UTM/UPS band letter, unchecked. 

160 ''' 

161 def __new__(cls, arg=None, name=_band_, **Error_name_arg): 

162 '''New L{Band} instance, see L{Str}. 

163 ''' 

164 return Str.__new__(cls, arg=arg, name=name, **Error_name_arg) 

165 

166 

167class Degrees(Float): 

168 '''Named C{float} representing a coordinate in C{degrees}, optionally clipped. 

169 ''' 

170 _ddd_ = 1 # default for .dms._toDMS 

171 _sep_ = S_SEP 

172 _suf_ = (S_NUL,) * 3 

173 

174 def __new__(cls, arg=None, name=_degrees_, Error=UnitError, suffix=_NSEW_, clip=0, wrap=None, **name_arg): 

175 '''New C{Degrees} instance, see L{Float}. 

176 

177 @arg cls: This class (C{Degrees} or sub-class). 

178 @kwarg arg: The value (any scalar C{type} convertable to C{float} or 

179 parsable by L{pygeodesy.parseDMS}). 

180 @kwarg name: Optional instance name (C{str}). 

181 @kwarg Error: Optional error to raise, overriding the default 

182 L{UnitError}. 

183 @kwarg suffix: Optional, valid compass direction suffixes (C{NSEW}). 

184 @kwarg clip: Optional B{C{arg}} range B{C{-clip..+clip}} 

185 (C{degrees} or C{0} or C{None} for unclipped). 

186 @kwarg wrap: Optionally adjust the B{C{arg}} value (L{pygeodesy.wrap90}, 

187 L{pygeodesy.wrap180} or L{pygeodesy.wrap360}). 

188 @kwarg name_arg: Optional C{name=arg} keyword argument, inlieu of 

189 B{C{name}} and B{C{arg}}. 

190 

191 @returns: A C{Degrees} instance. 

192 

193 @raise Error: Invalid B{C{arg}} or B{C{abs(arg)}} outside the B{C{clip}} 

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

195 ''' 

196 if name_arg: 

197 name, arg = _xkwds_popitem(name_arg) 

198 try: 

199 d = Float.__new__(cls, parseDMS(arg, suffix=suffix, clip=clip), 

200 Error=Error, name=name) 

201 if wrap: 

202 w = wrap(d) 

203 if w != d: 

204 d = Float.__new__(cls, arg=w, name=name, Error=Error) 

205 except Exception as x: 

206 raise _Error(cls, arg, name, Error, x=x) 

207 return d 

208 

209 def toDegrees(self): 

210 '''Convert C{Degrees} to C{Degrees}. 

211 ''' 

212 return self 

213 

214 def toRadians(self): 

215 '''Convert C{Degrees} to C{Radians}. 

216 ''' 

217 return Radians(radians(self), name=self.name) 

218 

219 def toRepr(self, std=False, **prec_fmt_ints): # PYCHOK prec=8, ... 

220 '''Return a representation of this C{Degrees}. 

221 

222 @kwarg std: If C{True} return the standard C{repr}, 

223 otherwise the named representation (C{bool}). 

224 

225 @see: Methods L{Degrees.toStr}, L{Float.toRepr} and function 

226 L{pygeodesy.toDMS} for more documentation. 

227 ''' 

228 return Float.toRepr(self, std=std, **prec_fmt_ints) 

229 

230 def toStr(self, prec=None, fmt=F__F_, ints=False, **s_D_M_S): # PYCHOK prec=8, ... 

231 '''Return this C{Degrees} as standard C{str}. 

232 

233 @see: Function L{pygeodesy.toDMS} for keyword argument details. 

234 ''' 

235 if fmt.startswith(_PERCENT_): # use regular formatting 

236 p = 8 if prec is None else prec 

237 return fstr(self, prec=p, fmt=fmt, ints=ints, sep=self._sep_) 

238 else: 

239 s = self._suf_[signOf(self) + 1] 

240 return _toDMS(self, fmt, prec, self._sep_, self._ddd_, s, s_D_M_S) 

241 

242 

243class Degrees_(Degrees): 

244 '''Named C{Degrees} representing a coordinate in C{degrees} with optional limits C{low} and C{high}. 

245 ''' 

246 def __new__(cls, arg=None, name=_degrees_, Error=UnitError, suffix=_NSEW_, low=None, high=None, **name_arg): 

247 '''New C{Degrees_} instance, see L{Degrees} and L{Float}.. 

248 

249 @arg cls: This class (C{Degrees_} or sub-class). 

250 @kwarg arg: The value (any C{type} convertable to C{float} or parsable by 

251 L{pygeodesy.parseDMS}). 

252 @kwarg name: Optional instance name (C{str}). 

253 @kwarg Error: Optional error to raise, overriding the default L{UnitError}. 

254 @kwarg suffix: Optional, valid compass direction suffixes (C{NSEW}). 

255 @kwarg low: Optional lower B{C{arg}} limit (C{float} or C{None}). 

256 @kwarg high: Optional upper B{C{arg}} limit (C{float} or C{None}). 

257 @kwarg name_arg: Optional C{name=arg} keyword argument, inlieu of B{C{name}} 

258 and B{C{arg}}. 

259 

260 @returns: A C{Degrees} instance. 

261 

262 @raise Error: Invalid B{C{arg}} or B{C{arg}} below B{C{low}} or above B{C{high}}. 

263 ''' 

264 if name_arg: 

265 name, arg = _xkwds_popitem(name_arg) 

266 self = Degrees.__new__(cls, arg=arg, name=name, Error=Error, suffix=suffix, clip=0) 

267 if (low is not None) and self < low: 

268 txt = Fmt.limit(below=low) 

269 elif (high is not None) and self > high: 

270 txt = Fmt.limit(above=high) 

271 else: 

272 return self 

273 raise _Error(cls, arg, name, Error, txt=txt) 

274 

275 

276class Degrees2(Float): 

277 '''Named C{float} representing a distance in C{degrees squared}. 

278 ''' 

279 def __new__(cls, arg=None, name=_degrees2_, **Error_name_arg): 

280 '''See L{Float}. 

281 ''' 

282 return Float.__new__(cls, arg=arg, name=name, **Error_name_arg) 

283 

284 

285class Radians(Float): 

286 '''Named C{float} representing a coordinate in C{radians}, optionally clipped. 

287 ''' 

288 def __new__(cls, arg=None, name=_radians_, Error=UnitError, suffix=_NSEW_, clip=0, **name_arg): 

289 '''New C{Radians} instance, see L{Float}. 

290 

291 @arg cls: This class (C{Radians} or sub-class). 

292 @kwarg arg: The value (any C{type} convertable to C{float} or parsable 

293 by L{pygeodesy.parseRad}). 

294 @kwarg name: Optional instance name (C{str}). 

295 @kwarg Error: Optional error to raise, overriding the default L{UnitError}. 

296 @kwarg suffix: Optional, valid compass direction suffixes (C{NSEW}). 

297 @kwarg clip: Optional B{C{arg}} range B{C{-clip..+clip}} (C{radians} or C{0} 

298 or C{None} for unclipped). 

299 @kwarg name_arg: Optional C{name=arg} keyword argument, inlieu of B{C{name}} 

300 and B{C{arg}}. 

301 

302 @returns: A C{Radians} instance. 

303 

304 @raise Error: Invalid B{C{arg}} or B{C{abs(arg)}} outside the B{C{clip}} 

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

306 ''' 

307 if name_arg: 

308 name, arg = _xkwds_popitem(name_arg) 

309 try: 

310 return Float.__new__(cls, parseRad(arg, suffix=suffix, clip=clip), 

311 Error=Error, name=name) 

312 except Exception as x: 

313 raise _Error(cls, arg, name, Error, x=x) 

314 

315 def toDegrees(self): 

316 '''Convert C{Radians} to C{Degrees}. 

317 ''' 

318 return Degrees(degrees(self), name=self.name) 

319 

320 def toRadians(self): 

321 '''Convert C{Radians} to C{Radians}. 

322 ''' 

323 return self 

324 

325 def toRepr(self, std=False, **prec_fmt_ints): # PYCHOK prec=8, ... 

326 '''Return a representation of this C{Radians}. 

327 

328 @kwarg std: If C{True} return the standard C{repr}, 

329 otherwise the named representation (C{bool}). 

330 

331 @see: Methods L{Radians.toStr}, L{Float.toRepr} and function 

332 L{pygeodesy.toDMS} for more documentation. 

333 ''' 

334 return Float.toRepr(self, std=std, **prec_fmt_ints) 

335 

336 def toStr(self, prec=8, fmt=F__F, ints=False): # PYCHOK prec=8, ... 

337 '''Return this C{Radians} as standard C{str}. 

338 

339 @see: Function L{pygeodesy.fstr} for keyword argument details. 

340 ''' 

341 return fstr(self, prec=prec, fmt=fmt, ints=ints) 

342 

343 

344class Radians_(Radians): 

345 '''Named C{float} representing a coordinate in C{radians} with optional limits C{low} and C{high}. 

346 ''' 

347 def __new__(cls, arg=None, name=_radians_, Error=UnitError, suffix=_NSEW_, low=_0_0, high=PI2, **name_arg): 

348 '''New C{Radians_} instance. 

349 

350 @arg cls: This class (C{Radians_} or sub-class). 

351 @kwarg arg: The value (any C{type} convertable to C{float} or parsable by 

352 L{pygeodesy.parseRad}). 

353 @kwarg name: Optional instance name (C{str}). 

354 @kwarg Error: Optional error to raise, overriding the default L{UnitError}. 

355 @kwarg suffix: Optional, valid compass direction suffixes (C{NSEW}). 

356 @kwarg low: Optional lower B{C{arg}} limit (C{float} or C{None}). 

357 @kwarg high: Optional upper B{C{arg}} limit (C{float} or C{None}). 

358 @kwarg name_arg: Optional C{name=arg} keyword argument, inlieu of B{C{name}} 

359 and B{C{arg}}. 

360 

361 @returns: A C{Radians_} instance. 

362 

363 @raise Error: Invalid B{C{arg}} or B{C{arg}} below B{C{low}} or above B{C{high}}. 

364 ''' 

365 if name_arg: 

366 name, arg = _xkwds_popitem(name_arg) 

367 self = Radians.__new__(cls, arg=arg, name=name, Error=Error, suffix=suffix, clip=0) 

368 if (low is not None) and self < low: 

369 txt = Fmt.limit(below=low) 

370 elif (high is not None) and self > high: 

371 txt = Fmt.limit(above=high) 

372 else: 

373 return self 

374 raise _Error(cls, arg, name, Error, txt=txt) 

375 

376 

377class Radians2(Float_): 

378 '''Named C{float} representing a distance in C{radians squared}. 

379 ''' 

380 def __new__(cls, arg=None, name=_radians2_, **Error_name_arg): 

381 '''New L{Radians2} instance, see L{Float_}. 

382 ''' 

383 return Float_.__new__(cls, arg=arg, name=name, low=_0_0, **Error_name_arg) 

384 

385 

386class Bearing(Degrees): 

387 '''Named C{float} representing a bearing in compass C{degrees} from (true) North. 

388 ''' 

389 _ddd_ = 1 

390 _suf_ = _N_ * 3 # always suffix N 

391 

392 def __new__(cls, arg=None, name=_bearing_, Error=UnitError, clip=0, **name_arg): 

393 '''New L{Bearing} instance, see L{Degrees}. 

394 ''' 

395 if name_arg: 

396 name, arg = _xkwds_popitem(name_arg) 

397 d = Degrees.__new__(cls, arg=arg, name=name, Error=Error, suffix=_N_, clip=clip) 

398 b = _umod_360(d) # 0 <= b < 360 

399 return d if b == d else Degrees.__new__(cls, arg=b, name=name, Error=Error) 

400 

401 

402class Bearing_(Radians): 

403 '''Named C{float} representing a bearing in C{radians} from compass C{degrees} from (true) North. 

404 ''' 

405 def __new__(cls, arg=None, name=_bearing_, clip=0, **Error_name_arg): 

406 '''New L{Bearing_} instance, see L{Bearing} and L{Radians}. 

407 ''' 

408 d = Bearing.__new__(cls, arg=arg, name=name, clip=clip, **Error_name_arg) 

409 return Radians.__new__(cls, radians(d), name=name) 

410 

411 

412class Distance(Float): 

413 '''Named C{float} representing a distance, conventionally in C{meter}. 

414 ''' 

415 def __new__(cls, arg=None, name=_distance_, **Error_name_arg): 

416 '''New L{Distance} instance, see L{Float}. 

417 ''' 

418 return Float.__new__(cls, arg=arg, name=name, **Error_name_arg) 

419 

420 

421class Distance_(Float_): 

422 '''Named C{float} with optional C{low} and C{high} limits representing a distance, conventionally in C{meter}. 

423 ''' 

424 def __new__(cls, arg=None, name=_distance_, **low_high_Error_name_arg): 

425 '''New L{Distance_} instance, see L{Float}. 

426 ''' 

427 return Float_.__new__(cls, arg=arg, name=name, **low_high_Error_name_arg) 

428 

429 

430class Easting(Float): 

431 '''Named C{float} representing an easting, conventionally in C{meter}. 

432 ''' 

433 def __new__(cls, arg=None, name=_easting_, Error=UnitError, falsed=False, high=None, **name_arg): 

434 '''New named C{Easting} or C{Easting of Point} instance. 

435 

436 @arg cls: This class (C{Easting} or sub-class). 

437 @kwarg arg: The value (any C{type} convertable to C{float}). 

438 @kwarg name: Optional instance name (C{str}). 

439 @kwarg Error: Optional error to raise, overriding the default L{UnitError}. 

440 @kwarg falsed: The B{C{arg}} value includes false origin (C{bool}). 

441 @kwarg high: Optional upper B{C{arg}} easting limit (C{scalar} or C{None}). 

442 @kwarg name_arg: Optional C{name=arg} keyword argument, inlieu of B{C{name}} 

443 and B{C{arg}}. 

444 

445 @returns: An C{Easting} instance. 

446 

447 @raise Error: Invalid B{C{arg}}, above B{C{high}} or negative, falsed B{C{arg}}. 

448 ''' 

449 if name_arg: 

450 name, arg = _xkwds_popitem(name_arg) 

451 self = Float.__new__(cls, arg=arg, name=name, Error=Error) 

452 if high and (self < 0 or self > high): # like Veness 

453 raise _Error(cls, arg, name, Error) 

454 elif falsed and self < 0: 

455 raise _Error(cls, arg, name, Error, txt=_negative_falsed_) 

456 return self 

457 

458 

459class Epoch(Float_): # in .ellipsoidalBase, .trf 

460 '''Named C{epoch} with optional C{low} and C{high} limits representing a fractional 

461 calendar year. 

462 ''' 

463 _std_repr = False 

464 

465 def __new__(cls, arg=None, name=_epoch_, Error=TRFError, low=1900, high=9999, **name_arg): 

466 '''New L{Epoch} instance, see L{Float_}. 

467 ''' 

468 if name_arg: 

469 name, arg = _xkwds_popitem(name_arg) 

470 return arg if isinstance(arg, Epoch) else Float_.__new__(cls, 

471 arg=arg, name=name, Error=Error, low=low, high=high) 

472 

473 def toRepr(self, std=False, **unused): # PYCHOK prec=3, fmt=Fmt.F, ints=True 

474 '''Return a representation of this C{Epoch}. 

475 

476 @kwarg std: Use the standard C{repr} or the named 

477 representation (C{bool}). 

478 

479 @see: Method L{Float.toRepr} for more documentation. 

480 ''' 

481 return Float_.toRepr(self, std=std) # prec=-3, fmt=Fmt.F, ints=True 

482 

483 def toStr(self, **unused): # PYCHOK prec=3, fmt=Fmt.F, ints=True 

484 '''Format this C{Epoch} as C{str}. 

485 

486 @see: Function L{pygeodesy.fstr} for more documentation. 

487 ''' 

488 return fstr(self, prec=-3, fmt=Fmt.F, ints=True) 

489 

490 __str__ = toStr # PYCHOK default '%.3F', with trailing zeros and decimal point 

491 

492 

493class Feet(Float): 

494 '''Named C{float} representing a distance or length in C{feet}. 

495 ''' 

496 def __new__(cls, arg=None, name=_feet_, **Error_name_arg): 

497 '''New L{Feet} instance, see L{Float}. 

498 ''' 

499 return Float.__new__(cls, arg=arg, name=name, **Error_name_arg) 

500 

501 

502class FIx(Float_): 

503 '''A named I{Fractional Index}, an C{int} or C{float} index into 

504 a C{list} or C{tuple} of C{points}, typically. A C{float} 

505 I{Fractional Index} C{fi} represents a location on the edge 

506 between C{points[int(fi)]} and C{points[(int(fi) + 1) % 

507 len(points)]}. 

508 ''' 

509 _fin = 0 

510 

511 def __new__(cls, fi, fin=None, **name_Error): 

512 '''New I{Fractional Index} in a C{list} or C{tuple} of points. 

513 

514 @arg fi: The fractional index (C{float} or C{int}). 

515 @kwarg fin: Optional C{len}, the number of C{points}, the index 

516 C{[n]} wrapped to C{[0]} (C{int} or C{None}). 

517 @kwarg name_Error: Optional keyword argument C{B{name}=NN} 

518 and C{B{Error}=UnitError}. 

519 

520 @return: The B{C{fi}} (named L{FIx}). 

521 

522 @note: The returned B{C{fi}} may exceed the B{C{flen}} of 

523 the original C{points} in certain open/closed cases. 

524 

525 @see: Method L{fractional} or function L{pygeodesy.fractional}. 

526 ''' 

527 n = Int_(fin=fin, low=0) if fin else None 

528 f = Float_.__new__(cls, fi, low=_0_0, high=n, **name_Error) 

529 i = int(f) 

530 r = f - float(i) 

531 if r < EPS: # see .points._fractional 

532 f = Float_.__new__(cls, i, low=_0_0) 

533 elif r > EPS1: 

534 f = Float_.__new__(cls, i + 1, high=n, **name_Error) 

535 if n: # non-zero and non-None 

536 f._fin = n 

537 return f 

538 

539 @Property_RO 

540 def fin(self): 

541 '''Get the given C{len}, the index C{[n]} wrapped to C{[0]} (C{int}). 

542 ''' 

543 return self._fin 

544 

545 def fractional(self, points, wrap=None, LatLon=None, Vector=None, **kwds): 

546 '''Return the point at this I{Fractional Index}. 

547 

548 @arg points: The points (C{LatLon}[], L{Numpy2LatLon}[], 

549 L{Tuple2LatLon}[] or C{other}[]). 

550 @kwarg wrap: If C{True}, wrap or I{normalize} and unroll the 

551 B{C{points}} (C{bool}) C{None} for backward 

552 compatible L{LatLon2Tuple} or B{C{LatLon}} with 

553 I{averaged} lat- and longitudes. 

554 @kwarg LatLon: Optional class to return the I{intermediate}, 

555 I{fractional} point (C{LatLon}) or C{None}. 

556 @kwarg Vector: Optional class to return the I{intermediate}, 

557 I{fractional} point (C{Cartesian}, C{Vector3d}) 

558 or C{None}. 

559 @kwarg kwds: Optional, additional B{C{LatLon}} I{or} B{C{Vector}} 

560 keyword arguments, ignored if both C{B{LatLon}} and 

561 C{B{Vector}} are C{None}. 

562 

563 @return: See function L{pygeodesy.fractional}. 

564 

565 @raise IndexError: This fractional index invalid or B{C{points}} 

566 not subscriptable or not closed. 

567 

568 @raise TypeError: Invalid B{C{LatLon}}, B{C{Vector}} or B{C{kwds}} 

569 argument. 

570 

571 @see: Function L{pygeodesy.fractional}. 

572 ''' 

573 # fi = 0 if self == self.fin else self 

574 return _MODS.points.fractional(points, self, wrap=wrap, 

575 LatLon=LatLon, Vector=Vector, **kwds) 

576 

577 

578def _fi_j2(f, n): # PYCHOK in .ellipsoidalBaseDI, .vector3d 

579 # Get 2-tuple (C{fi}, C{j}) 

580 i = int(f) # like L{FIx} 

581 if not 0 <= i < n: 

582 raise _AssertionError(i=i, n=n, f=f, r=f - float(i)) 

583 return FIx(fi=f, fin=n), (i + 1) % n 

584 

585 

586class Height(Float): # here to avoid circular import 

587 '''Named C{float} representing a height, conventionally in C{meter}. 

588 ''' 

589 def __new__(cls, arg=None, name=_height_, **Error_name_arg): 

590 '''New L{Height} instance, see L{Float}. 

591 ''' 

592 return Float.__new__(cls, arg=arg, name=name, **Error_name_arg) 

593 

594 

595class Height_(Float_): # here to avoid circular import 

596 '''Named C{float} with optional C{low} and C{high} limits representing a height, conventionally in C{meter}. 

597 ''' 

598 def __new__(cls, arg=None, name=_height_, **low_high_Error_name_arg): 

599 '''New L{Height} instance, see L{Float}. 

600 ''' 

601 return Float_.__new__(cls, arg=arg, name=name, **low_high_Error_name_arg) 

602 

603 

604class HeightX(Height): 

605 '''Like L{Height} but to distinguish the interpolated height 

606 at a clip intersection from an original L{Height}. 

607 ''' 

608 pass 

609 

610 

611def _heigHt(inst, height): 

612 '''(INTERNAL) Override the C{inst}ance' height. 

613 ''' 

614 return inst.height if height is None else Height(height) 

615 

616 

617class Lam(Radians): 

618 '''Named C{float} representing a longitude in C{radians}. 

619 ''' 

620 def __new__(cls, arg=None, name=_lam_, clip=PI, **Error_name_arg): 

621 '''New L{Lam} instance, see L{Radians}. 

622 ''' 

623 return Radians.__new__(cls, arg=arg, name=name, suffix=_EW_, clip=clip, **Error_name_arg) 

624 

625 

626class Lam_(Lam): 

627 '''Named C{float} representing a longitude in C{radians} converted from C{degrees}. 

628 ''' 

629 def __new__(cls, arg=None, name=_lon_, Error=UnitError, clip=180, **name_arg): 

630 '''New L{Lam_} instance, see L{Lam} and L{Radians}. 

631 ''' 

632 if name_arg: 

633 name, arg = _xkwds_popitem(name_arg) 

634 d = Lam.__new__(cls, arg=arg, name=name, Error=Error, clip=clip) 

635 return Radians.__new__(cls, radians(d), name=name, Error=Error) 

636 

637 

638class Lat(Degrees): 

639 '''Named C{float} representing a latitude in C{degrees}. 

640 ''' 

641 _ddd_ = 2 

642 _suf_ = _S_, S_NUL, _N_ # no zero suffix 

643 

644 def __new__(cls, arg=None, name=_lat_, clip=90, **Error_name_arg): 

645 '''New L{Lat} instnace, see L{Degrees}. 

646 ''' 

647 return Degrees.__new__(cls, arg=arg, name=name, suffix=_NS_, clip=clip, **Error_name_arg) 

648 

649 

650class Lat_(Degrees_): 

651 '''Named C{float} representing a latitude in C{degrees} within limits C{low} and C{high}. 

652 ''' 

653 _ddd_ = 2 

654 _suf_ = _S_, S_NUL, _N_ # no zero suffix 

655 

656 def __new__(cls, arg=None, name=_lat_, low=-90, high=90, **Error_name_arg): 

657 '''See L{Degrees_}. 

658 ''' 

659 return Degrees_.__new__(cls, arg=arg, name=name, suffix=_NS_, low=low, high=high, **Error_name_arg) 

660 

661 

662class Lon(Degrees): 

663 '''Named C{float} representing a longitude in C{degrees}. 

664 ''' 

665 _ddd_ = 3 

666 _suf_ = _W_, S_NUL, _E_ # no zero suffix 

667 

668 def __new__(cls, arg=None, name=_lon_, clip=180, **Error_name_arg): 

669 '''New L{Lon} instance, see L{Degrees}. 

670 ''' 

671 return Degrees.__new__(cls, arg=arg, name=name, suffix=_EW_, clip=clip, **Error_name_arg) 

672 

673 

674class Lon_(Degrees_): 

675 '''Named C{float} representing a longitude in C{degrees} within limits C{low} and C{high}. 

676 ''' 

677 _ddd_ = 3 

678 _suf_ = _W_, S_NUL, _E_ # no zero suffix 

679 

680 def __new__(cls, arg=None, name=_lon_, low=-180, high=180, **Error_name_arg): 

681 '''New L{Lon_} instance, see L{Lon} and L{Degrees_}. 

682 ''' 

683 return Degrees_.__new__(cls, arg=arg, name=name, suffix=_EW_, low=low, high=high, **Error_name_arg) 

684 

685 

686class Meter(Float): 

687 '''Named C{float} representing a distance or length in C{meter}. 

688 ''' 

689 def __new__(cls, arg=None, name=_meter_, **Error_name_arg): 

690 '''New L{Meter} instance, see L{Float}. 

691 ''' 

692 return Float.__new__(cls, arg=arg, name=name, **Error_name_arg) 

693 

694 def __repr__(self): 

695 '''Return a representation of this C{Meter}. 

696 

697 @see: Method C{Str.toRepr} and property C{Str.std_repr}. 

698 

699 @note: Use C{env} variable C{PYGEODESY_METER_STD_REPR=std} 

700 prior to C{import pygeodesy} to get the standard 

701 C{repr} or set property C{std_repr=False} to always 

702 get the named C{toRepr} representation. 

703 ''' 

704 return self.toRepr(std=self._std_repr) 

705 

706 

707# _1Å = Meter( _Å= 1e-10) # PyCHOK 1 Ångstrōm 

708_1um = Meter( _1um= 1.e-6) # PYCHOK 1 micrometer in .mgrs 

709_10um = Meter( _10um= 1.e-5) # PYCHOK 10 micrometer in .osgr 

710_1mm = Meter( _1mm=_0_001) # PYCHOK 1 millimeter in .ellipsoidal... 

711_100km = Meter( _100km= 1.e+5) # PYCHOK 100 kilometer in .formy, .mgrs, .osgr 

712_2000km = Meter(_2000km= 2.e+6) # PYCHOK 2,000 kilometer in .mgrs 

713 

714 

715class Meter_(Float_): 

716 '''Named C{float} representing a distance or length in C{meter}. 

717 ''' 

718 def __new__(cls, arg=None, name=_meter_, low=_0_0, **high_Error_name_arg): 

719 '''New L{Meter_} instance, see L{Meter} and L{Float_}. 

720 ''' 

721 return Float_.__new__(cls, arg=arg, name=name, low=low, **high_Error_name_arg) 

722 

723 

724class Meter2(Float_): 

725 '''Named C{float} representing an area in C{meter squared}. 

726 ''' 

727 def __new__(cls, arg=None, name=_meter2_, Error=UnitError, **name_arg): 

728 '''New L{Meter2} instance, see L{Float_}. 

729 ''' 

730 return Float_.__new__(cls, arg=arg, name=name, Error=Error, low=_0_0, **name_arg) 

731 

732 

733class Meter3(Float_): 

734 '''Named C{float} representing a volume in C{meter cubed}. 

735 ''' 

736 def __new__(cls, arg=None, name='meter3', **Error_name_arg): 

737 '''New L{Meter3} instance, see L{Float_}. 

738 ''' 

739 return Float_.__new__(cls, arg=arg, name=name, low=_0_0, **Error_name_arg) 

740 

741 

742class Northing(Float): 

743 '''Named C{float} representing a northing, conventionally in C{meter}. 

744 ''' 

745 def __new__(cls, arg=None, name=_northing_, Error=UnitError, falsed=False, high=None, **name_arg): 

746 '''New C{Northing} or C{Northing of point} instance. 

747 

748 @arg cls: This class (C{Northing} or sub-class). 

749 @kwarg arg: The value (any C{type} convertable to C{float}). 

750 @kwarg name: Optional instance name (C{str}). 

751 @kwarg Error: Optional error to raise, overriding the default L{UnitError}. 

752 @kwarg falsed: The B{C{arg}} value includes false origin (C{bool}). 

753 @kwarg high: Optional upper B{C{arg}} northing limit (C{scalar} or C{None}). 

754 @kwarg name_arg: Optional C{name=arg} keyword argument, inlieu of B{C{name}} 

755 and B{C{arg}}. 

756 

757 @returns: A C{Northing} instance. 

758 

759 @raise Error: Invalid B{C{arg}}, above B{C{high}} or negative, falsed B{C{arg}}. 

760 ''' 

761 if name_arg: 

762 name, arg = _xkwds_popitem(name_arg) 

763 self = Float.__new__(cls, arg=arg, name=name, Error=Error) 

764 if high and (self < 0 or self > high): 

765 raise _Error(cls, arg, name, Error) 

766 elif falsed and self < 0: 

767 raise _Error(cls, arg, name, Error, txt=_negative_falsed_) 

768 return self 

769 

770 

771class Number_(Int_): 

772 '''Named C{int} representing a non-negative number. 

773 ''' 

774 def __new__(cls, arg=None, name=_number_, **low_high_Error_name_arg): 

775 '''New L{Number_} instance, see L{Int_}. 

776 ''' 

777 return Int_.__new__(cls, arg=arg, name=name, **low_high_Error_name_arg) 

778 

779 

780class Phi(Radians): 

781 '''Named C{float} representing a latitude in C{radians}. 

782 ''' 

783 def __new__(cls, arg=None, name=_phi_, clip=PI_2, **Error_name_arg): 

784 '''New L{Phi} instance, see L{Radians}. 

785 ''' 

786 return Radians.__new__(cls, arg=arg, name=name, suffix=_NS_, clip=clip, **Error_name_arg) 

787 

788 

789class Phi_(Phi): 

790 '''Named C{float} representing a latitude in C{radians} converted from C{degrees}. 

791 ''' 

792 def __new__(cls, arg=None, name=_lat_, Error=UnitError, clip=90, **name_arg): 

793 '''New L{Phi_} instance, see L{Phi} and L{Radians}. 

794 ''' 

795 if name_arg: 

796 name, arg = _xkwds_popitem(name_arg) 

797 d = Phi.__new__(cls, arg=arg, name=name, Error=Error, clip=clip) 

798 return Radians.__new__(cls, arg=radians(d), name=name, Error=Error) 

799 

800 

801class Precision_(Int_): 

802 '''Named C{int} with optional C{low} and C{high} limits representing a precision. 

803 ''' 

804 def __new__(cls, arg=None, name=_precision_, **low_high_Error_name_arg): 

805 '''New L{Precision_} instance, see L{Int_}. 

806 ''' 

807 return Int_.__new__(cls, arg=arg, name=name, **low_high_Error_name_arg) 

808 

809 

810class Radius_(Float_): 

811 '''Named C{float} with optional C{low} and C{high} limits representing a radius, conventionally in C{meter}. 

812 ''' 

813 def __new__(cls, arg=None, name=_radius_, **low_high_Error_name_arg): 

814 '''New L{Radius_} instance, see L{Radius} and L{Float}. 

815 ''' 

816 return Float_.__new__(cls, arg=arg, name=name, **low_high_Error_name_arg) 

817 

818 

819class Scalar(Float): 

820 '''Named C{float} representing a factor, fraction, scale, etc. 

821 ''' 

822 def __new__(cls, arg=None, name=_scalar_, **Error_name_arg): 

823 '''New L{Scalar} instance, see L{Float}. 

824 ''' 

825 return Float.__new__(cls, arg=arg, name=name, **Error_name_arg) 

826 

827 

828class Scalar_(Float_): 

829 '''Named C{float} with optional C{low} and C{high} limits representing a factor, fraction, scale, etc. 

830 ''' 

831 def __new__(cls, arg=None, name=_scalar_, low=_0_0, **high_Error_name_arg): 

832 '''New L{Scalar_} instance, see L{Scalar} and L{Float_}. 

833 ''' 

834 return Float_.__new__(cls, arg=arg, name=name, low=low, **high_Error_name_arg) 

835 

836 

837class Zone(Int): 

838 '''Named C{int} representing a UTM/UPS zone number. 

839 ''' 

840 def __new__(cls, arg=None, name=_zone_, **Error_name_arg): 

841 '''New L{Zone} instance, see L{Int} 

842 ''' 

843 # usually low=_UTMUPS_ZONE_MIN, high=_UTMUPS_ZONE_MAX 

844 return Int_.__new__(cls, arg=arg, name=name, **Error_name_arg) 

845 

846 

847def _xStrError(*Refs, **name_value_Error): 

848 '''(INTERNAL) Create a C{TypeError} for C{Garef}, C{Geohash}, C{Wgrs}. 

849 ''' 

850 r = tuple(r.__name__ for r in Refs) + (Str.__name__, _LatLon_, 'LatLon*Tuple') 

851 return _IsnotError(*r, **name_value_Error) 

852 

853 

854def _xUnit(units, Base): # in .frechet, .hausdorff 

855 '''(INTERNAL) Get C{Unit} from C{Unit} or C{name}, ortherwise C{Base}. 

856 ''' 

857 if not issubclassof(Base, _NamedUnit): 

858 raise _IsnotError(_NamedUnit.__name__, Base=Base) 

859 U = globals().get(units.capitalize(), Base) if isstr(units) else ( 

860 units if issubclassof(units, Base) else Base) 

861 return U if issubclassof(U, Base) else Base 

862 

863 

864def _xUnits(units, Base=_NamedUnit): # in .frechet, .hausdorff 

865 '''(INTERNAL) Set property C{units} as C{Unit} or C{Str}. 

866 ''' 

867 if not issubclassof(Base, _NamedUnit): 

868 raise _IsnotError(_NamedUnit.__name__, Base=Base) 

869 elif issubclassof(units, Base): 

870 return units 

871 elif isstr(units): 

872 return Str(units, name=_units_) # XXX Str to _Pass and for backward compatibility 

873 else: 

874 raise _IsnotError(Base.__name__, Str.__name__, str.__name__, units=units) 

875 

876 

877def _std_repr(*Classes): 

878 '''(INTERNAL) Use standard C{repr} or named C{toRepr}. 

879 ''' 

880 for C in Classes: 

881 if hasattr(C, _std_repr.__name__): # PYCHOK del _std_repr 

882 env = 'PYGEODESY_%s_STD_REPR' % (C.__name__.upper(),) 

883 if _getenv(env, _std_).lower() != _std_: 

884 C._std_repr = False 

885 

886_std_repr(Bearing, Bool, Degrees, Float, Int, Meter, Radians, Str) # PYCHOK expected 

887del _std_repr 

888 

889__all__ += _ALL_DOCS(_NamedUnit) 

890 

891# **) MIT License 

892# 

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

894# 

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

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

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

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

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

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

901# 

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

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

904# 

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

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

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

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

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

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

911# OTHER DEALINGS IN THE SOFTWARE.