Coverage for pygeodesy/units.py: 95%

330 statements  

« prev     ^ index     » next       coverage.py v7.2.2, created at 2024-05-15 16:36 -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 isinstanceof, isscalar, isstr, issubclassof, signOf 

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

11 _0_001, _0_5, INT0 # PYCHOK for .mgrs, .namedTuples 

12from pygeodesy.dms import F__F, F__F_, S_NUL, S_SEP, parseDMS, parseRad, \ 

13 _toDMS, toDMS 

14from pygeodesy.errors import _AssertionError, _IsnotError, TRFError, UnitError, \ 

15 _xkwds, _xkwds_item2 

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

17 _distance_, _E_, _easting_, _epoch_, _EW_, _feet_, \ 

18 _height_, _lam_, _lat_, _LatLon_, _lon_, _meter_, \ 

19 _meter2_, _N_, _northing_, _NS_, _NSEW_, _number_, \ 

20 _PERCENT_, _phi_, _precision_, _radians_, _radians2_, \ 

21 _radius_, _S_, _scalar_, _units_, _W_, _zone_, \ 

22 _std_ # PYCHOK used! 

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

24from pygeodesy.props import Property_RO 

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

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

27 Radius, Str # PYCHOK shared .namedTuples 

28 

29from math import degrees, radians 

30 

31__all__ = _ALL_LAZY.units 

32__version__ = '24.05.10' 

33 

34_negative_falsed_ = 'negative, falsed' 

35 

36 

37class Float_(Float): 

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

39 ''' 

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

41 '''New C{Float_} instance. 

42 

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

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

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

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

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

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

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

50 and B{C{arg}}. 

51 

52 @returns: A C{Float_} instance. 

53 

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

55 ''' 

56 if name_arg: 

57 name, arg = _xkwds_item2(name_arg) 

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

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

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

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

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

63 else: 

64 return self 

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

66 

67 

68class Int_(Int): 

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

70 ''' 

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

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

73 

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

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

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

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

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

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

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

81 and B{C{arg}}. 

82 

83 @returns: An L{Int_} instance. 

84 

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

86 ''' 

87 if name_arg: 

88 name, arg = _xkwds_item2(name_arg) 

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

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

91 txt = Fmt.limit(below=low) 

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

93 txt = Fmt.limit(above=high) 

94 else: 

95 return self 

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

97 

98 

99class Bool(Int, _NamedUnit): 

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

101 ''' 

102 # _std_repr = True # set below 

103 _bool_True_or_False = None 

104 

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

106 '''New C{Bool} instance. 

107 

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

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

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

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

112 L{UnitError}. 

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

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

115 

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

117 

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

119 ''' 

120 if name_arg: 

121 name, arg = _xkwds_item2(name_arg) 

122 try: 

123 b = bool(arg) 

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

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

126 

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

128 self._bool_True_or_False = b 

129 return self 

130 

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

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

133 return self._bool_True_or_False 

134 

135 __nonzero__ = __bool__ # PYCHOK Python 2- 

136 

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

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

139 

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

141 representation (C{bool}). 

142 

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

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

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

146 get the named C{toRepr} representation. 

147 ''' 

148 r = repr(self._bool_True_or_False) 

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

150 

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

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

153 ''' 

154 return str(self._bool_True_or_False) 

155 

156 

157class Band(Str): 

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

159 ''' 

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

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

162 ''' 

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

164 

165 

166class Degrees(Float): 

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

168 ''' 

169 _ddd_ = 1 # default for .dms._toDMS 

170 _sep_ = S_SEP 

171 _suf_ = (S_NUL,) * 3 

172 

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

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

175 

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

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

178 parsable by L{pygeodesy.parseDMS}). 

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

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

181 L{UnitError}. 

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

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

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

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

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

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

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

189 

190 @returns: A C{Degrees} instance. 

191 

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

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

194 ''' 

195 if name_arg: 

196 name, arg = _xkwds_item2(name_arg) 

197 try: 

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

199 Error=Error, name=name) 

200 if wrap: 

201 w = wrap(d) 

202 if w != d: 

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

204 except Exception as x: 

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

206 return d 

207 

208 def toDegrees(self): 

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

210 ''' 

211 return self 

212 

213 def toRadians(self): 

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

215 ''' 

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

217 

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

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

220 

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

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

223 

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

225 L{pygeodesy.toDMS} for more documentation. 

226 ''' 

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

228 

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

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

231 

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

233 ''' 

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

235 p = 8 if prec is None else prec 

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

237 else: 

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

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

240 

241 

242class Degrees_(Degrees): 

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

244 ''' 

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

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

247 

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

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

250 L{pygeodesy.parseDMS}). 

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

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

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

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

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

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

257 and B{C{arg}}. 

258 

259 @returns: A C{Degrees} instance. 

260 

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

262 ''' 

263 if name_arg: 

264 name, arg = _xkwds_item2(name_arg) 

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

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

267 txt = Fmt.limit(below=low) 

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

269 txt = Fmt.limit(above=high) 

270 else: 

271 return self 

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

273 

274 

275class Degrees2(Float): 

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

277 ''' 

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

279 '''See L{Float}. 

280 ''' 

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

282 

283 

284class Radians(Float): 

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

286 ''' 

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

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

289 

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

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

292 by L{pygeodesy.parseRad}). 

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

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

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

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

297 or C{None} for unclipped). 

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

299 and B{C{arg}}. 

300 

301 @returns: A C{Radians} instance. 

302 

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

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

305 ''' 

306 if name_arg: 

307 name, arg = _xkwds_item2(name_arg) 

308 try: 

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

310 Error=Error, name=name) 

311 except Exception as x: 

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

313 

314 def toDegrees(self): 

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

316 ''' 

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

318 

319 def toRadians(self): 

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

321 ''' 

322 return self 

323 

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

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

326 

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

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

329 

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

331 L{pygeodesy.toDMS} for more documentation. 

332 ''' 

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

334 

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

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

337 

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

339 ''' 

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

341 

342 

343class Radians_(Radians): 

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

345 ''' 

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

347 '''New C{Radians_} instance. 

348 

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

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

351 L{pygeodesy.parseRad}). 

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

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

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

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

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

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

358 and B{C{arg}}. 

359 

360 @returns: A C{Radians_} instance. 

361 

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

363 ''' 

364 if name_arg: 

365 name, arg = _xkwds_item2(name_arg) 

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

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

368 txt = Fmt.limit(below=low) 

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

370 txt = Fmt.limit(above=high) 

371 else: 

372 return self 

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

374 

375 

376class Radians2(Float_): 

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

378 ''' 

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

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

381 ''' 

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

383 

384 

385class Bearing(Degrees): 

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

387 ''' 

388 _ddd_ = 1 

389 _suf_ = _N_ * 3 # always suffix N 

390 

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

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

393 ''' 

394 if name_arg: 

395 name, arg = _xkwds_item2(name_arg) 

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

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

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

399 

400 

401class Bearing_(Radians): 

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

403 ''' 

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

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

406 ''' 

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

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

409 

410 

411class Distance(Float): 

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

413 ''' 

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

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

416 ''' 

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

418 

419 

420class Distance_(Float_): 

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

422 ''' 

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

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

425 ''' 

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

427 

428 

429class Easting(Float): 

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

431 ''' 

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

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

434 

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

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

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

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

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

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

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

442 and B{C{arg}}. 

443 

444 @returns: An C{Easting} instance. 

445 

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

447 ''' 

448 if name_arg: 

449 name, arg = _xkwds_item2(name_arg) 

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

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

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

453 elif falsed and self < 0: 

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

455 return self 

456 

457 

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

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

460 calendar year. 

461 ''' 

462 _std_repr = False 

463 

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

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

466 ''' 

467 if name_arg: 

468 name, arg = _xkwds_item2(name_arg) 

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

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

471 

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

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

474 

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

476 representation (C{bool}). 

477 

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

479 ''' 

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

481 

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

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

484 

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

486 ''' 

487 return fstr(self, prec=prec, fmt=Fmt.F, ints=True) 

488 

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

490 

491 

492class Feet(Float): 

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

494 ''' 

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

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

497 ''' 

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

499 

500 

501class FIx(Float_): 

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

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

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

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

506 len(points)]}. 

507 ''' 

508 _fin = 0 

509 

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

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

512 

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

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

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

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

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

518 

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

520 

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

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

523 

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

525 ''' 

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

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

528 i = int(f) 

529 r = f - float(i) 

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

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

532 elif r > EPS1: 

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

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

535 f._fin = n 

536 return f 

537 

538 @Property_RO 

539 def fin(self): 

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

541 ''' 

542 return self._fin 

543 

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

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

546 

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

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

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

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

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

552 I{averaged} lat- and longitudes. 

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

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

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

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

557 or C{None}. 

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

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

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

561 

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

563 

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

565 not subscriptable or not closed. 

566 

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

568 argument. 

569 

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

571 ''' 

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

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

574 LatLon=LatLon, Vector=Vector, **kwds) 

575 

576 

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

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

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

580 if not 0 <= i < n: 

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

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

583 

584 

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

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

587 ''' 

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

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

590 ''' 

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

592 

593 

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

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

596 ''' 

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

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

599 ''' 

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

601 

602 

603class HeightX(Height): 

604 '''Like L{Height}, used to distinguish the interpolated height 

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

606 ''' 

607 pass 

608 

609 

610def _heigHt(inst, height): 

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

612 ''' 

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

614 

615 

616class Lam(Radians): 

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

618 ''' 

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

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

621 ''' 

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

623 

624 

625class Lam_(Lam): 

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

627 ''' 

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

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

630 ''' 

631 if name_arg: 

632 name, arg = _xkwds_item2(name_arg) 

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

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

635 

636 

637class Lat(Degrees): 

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

639 ''' 

640 _ddd_ = 2 

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

642 

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

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

645 ''' 

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

647 

648 

649class Lat_(Degrees_): 

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

651 ''' 

652 _ddd_ = 2 

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

654 

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

656 '''See L{Degrees_}. 

657 ''' 

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

659 

660 

661class Lon(Degrees): 

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

663 ''' 

664 _ddd_ = 3 

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

666 

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

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

669 ''' 

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

671 

672 

673class Lon_(Degrees_): 

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

675 ''' 

676 _ddd_ = 3 

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

678 

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

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

681 ''' 

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

683 

684 

685class Meter(Float): 

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

687 ''' 

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

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

690 ''' 

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

692 

693 def __repr__(self): 

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

695 

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

697 

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

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

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

701 get the named C{toRepr} representation. 

702 ''' 

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

704 

705 

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

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

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

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

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

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

712 

713 

714class Meter_(Float_): 

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

716 ''' 

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

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

719 ''' 

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

721 

722 

723class Meter2(Float_): 

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

725 ''' 

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

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

728 ''' 

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

730 

731 

732class Meter3(Float_): 

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

734 ''' 

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

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

737 ''' 

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

739 

740 

741class Northing(Float): 

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

743 ''' 

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

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

746 

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

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

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

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

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

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

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

754 and B{C{arg}}. 

755 

756 @returns: A C{Northing} instance. 

757 

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

759 ''' 

760 if name_arg: 

761 name, arg = _xkwds_item2(name_arg) 

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

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

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

765 elif falsed and self < 0: 

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

767 return self 

768 

769 

770class Number_(Int_): 

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

772 ''' 

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

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

775 ''' 

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

777 

778 

779class Phi(Radians): 

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

781 ''' 

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

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

784 ''' 

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

786 

787 

788class Phi_(Phi): 

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

790 ''' 

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

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

793 ''' 

794 if name_arg: 

795 name, arg = _xkwds_item2(name_arg) 

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

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

798 

799 

800class Precision_(Int_): 

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

802 ''' 

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

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

805 ''' 

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

807 

808 

809class Radius_(Float_): 

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

811 ''' 

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

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

814 ''' 

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

816 

817 

818class Scalar(Float): 

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

820 ''' 

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

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

823 ''' 

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

825 

826 

827class Scalar_(Float_): 

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

829 ''' 

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

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

832 ''' 

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

834 

835 

836class Zone(Int): 

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

838 ''' 

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

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

841 ''' 

842 # usually low=_UTMUPS_ZONE_MIN, high=_UTMUPS_ZONE_MAX 

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

844 

845 

846_Scalars = Float, Float_, Scalar, Scalar_ 

847_Degrees = (Bearing, Bearing_, Degrees, Degrees_) + _Scalars 

848_Meters = (Distance, Distance_, Meter, Meter_) + _Scalars 

849_Radians = (Radians, Radians_) + _Scalars # PYCHOK unused 

850_Radii = _Meters + (Radius, Radius_) 

851 

852 

853def _isDegrees(obj): 

854 # Check for valid degrees types. 

855 return isinstance(obj, _Degrees) or _isScalar(obj) 

856 

857 

858def _isHeight(obj): 

859 # Check for valid heigth types. 

860 return isinstance(obj, _Meters) or _isScalar(obj) 

861 

862 

863def _isMeter(obj): 

864 # Check for valid meter types. 

865 return isinstance(obj, _Meters) or _isScalar(obj) 

866 

867 

868def _isRadius(obj): 

869 # Check for valid earth radius types. 

870 return isinstance(obj, _Radii) or _isScalar(obj) 

871 

872 

873def _isScalar(obj): 

874 # Check for pure scalar types. 

875 return isscalar(obj) and not isinstance(obj, _NamedUnit) 

876 

877 

878def _toDegrees(s, *xs, **toDMS_kwds): 

879 '''(INTERNAL) Convert C{xs} from C{Radians} to C{Degrees} or C{toDMS}. 

880 ''' 

881 if toDMS_kwds: 

882 toDMS_kwds = _xkwds(toDMS_kwds, ddd=1, pos=NN) 

883 

884 for x in xs: 

885 if not isinstanceof(x, Degrees, Degrees_): 

886 s = None 

887 x = x.toDegrees() 

888 yield toDMS(x, **toDMS_kwds) if toDMS_kwds else x 

889 yield None if toDMS_kwds else s 

890 

891 

892def _toRadians(s, *xs): 

893 '''(INTERNAL) Convert C{xs} from C{Degrees} to C{Radians}. 

894 ''' 

895 for x in xs: 

896 if not isinstanceof(x, Radians, Radians_): 

897 s = None 

898 x = x.toRadians() 

899 yield x 

900 yield s 

901 

902 

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

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

905 ''' 

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

907 return _IsnotError(*r, **name_value_Error) 

908 

909 

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

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

912 ''' 

913 if not issubclassof(Base, _NamedUnit): 

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

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

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

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

918 

919 

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

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

922 ''' 

923 if not issubclassof(Base, _NamedUnit): 

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

925 elif issubclassof(units, Base): 

926 return units 

927 elif isstr(units): 

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

929 else: 

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

931 

932 

933def _std_repr(*Classes): 

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

935 ''' 

936 for C in Classes: 

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

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

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

940 C._std_repr = False 

941 

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

943del _std_repr 

944 

945__all__ += _ALL_DOCS(_NamedUnit) 

946 

947# **) MIT License 

948# 

949# Copyright (C) 2016-2024 -- mrJean1 at Gmail -- All Rights Reserved. 

950# 

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

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

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

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

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

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

957# 

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

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

960# 

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

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

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

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

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

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

967# OTHER DEALINGS IN THE SOFTWARE.