Coverage for pygeodesy/units.py: 94%

318 statements  

« prev     ^ index     » next       coverage.py v7.2.2, created at 2024-06-10 14:08 -0400

1 

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

3 

4u'''Various named units, all sub-classes of C{Float}, C{Int} or C{Str} from 

5basic C{float}, C{int} respectively C{str} to named units as L{Degrees}, 

6L{Feet}, L{Meter}, L{Radians}, etc. 

7''' 

8 

9from pygeodesy.basics import isscalar, isstr, issubclassof, signOf, _xsubclassof 

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, _toDMS 

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

14 _xattr 

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

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

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

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

19 _PERCENT_, _phi_, _precision_, _radians_, _radians2_, \ 

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

21 _std_ # PYCHOK used! 

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

23from pygeodesy.props import Property_RO 

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

25from pygeodesy.unitsBase import _Error, Float, Fmt, fstr, Int, _arg_name_arg2, \ 

26 _NamedUnit, Radius, Str # PYCHOK shared .namedTuples 

27 

28from math import degrees, radians 

29 

30__all__ = _ALL_LAZY.units 

31__version__ = '24.06.10' 

32 

33_negative_falsed_ = 'negative, falsed' 

34 

35 

36class Float_(Float): 

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

38 ''' 

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

40 '''New C{Float_} instance. 

41 

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

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

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

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

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

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

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

49 B{C{arg}} and B{C{name}} ones. 

50 

51 @returns: A C{Float_} instance. 

52 

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

54 ''' 

55 if name_arg: 

56 name, arg = _arg_name_arg2(arg, **name_arg) 

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

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

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

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

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

62 else: 

63 return self 

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

65 

66 

67class Int_(Int): 

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

69 ''' 

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

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

72 

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

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

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

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

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

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

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

80 B{C{arg}} and B{C{name}} ones. 

81 

82 @returns: An L{Int_} instance. 

83 

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

85 ''' 

86 if name_arg: 

87 name, arg = _arg_name_arg2(arg, **name_arg) 

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

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

90 txt = Fmt.limit(below=low) 

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

92 txt = Fmt.limit(above=high) 

93 else: 

94 return self 

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

96 

97 

98class Bool(Int, _NamedUnit): 

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

100 ''' 

101 # _std_repr = True # set below 

102 _bool_True_or_False = None 

103 

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

105 '''New C{Bool} instance. 

106 

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

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

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

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

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

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

113 

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

115 

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

117 ''' 

118 if name_arg: 

119 name, arg = _arg_name_arg2(arg, **name_arg) 

120 try: 

121 b = bool(arg) 

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

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

124 

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

126 self._bool_True_or_False = b 

127 return self 

128 

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

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

131 return self._bool_True_or_False 

132 

133 __nonzero__ = __bool__ # PYCHOK Python 2- 

134 

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

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

137 

138 @kwarg std: Use the standard C{repr} or the named representation (C{bool}). 

139 

140 @note: Use C{env} variable C{PYGEODESY_BOOL_STD_REPR=std} prior to C{import 

141 pygeodesy} to get the standard C{repr} or set property C{std_repr=False} 

142 to always get the named C{toRepr} representation. 

143 ''' 

144 r = repr(self._bool_True_or_False) 

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

146 

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

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

149 ''' 

150 return str(self._bool_True_or_False) 

151 

152 

153class Band(Str): 

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

155 ''' 

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

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

158 ''' 

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

160 

161 

162class Degrees(Float): 

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

164 ''' 

165 _ddd_ = 1 # default for .dms._toDMS 

166 _sep_ = S_SEP 

167 _suf_ = (S_NUL,) * 3 

168 

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

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

171 

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

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

174 by L{pygeodesy.parseDMS}). 

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

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

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

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

179 or C{None} for unclipped). 

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

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

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

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

184 

185 @returns: A C{Degrees} instance. 

186 

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

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

189 ''' 

190 if name_arg: 

191 name, arg = _arg_name_arg2(arg, **name_arg) 

192 try: 

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

194 Error=Error, name=name) 

195 if wrap: 

196 w = wrap(d) 

197 if w != d: 

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

199 except Exception as x: 

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

201 return d 

202 

203 def toDegrees(self): 

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

205 ''' 

206 return self 

207 

208 def toRadians(self): 

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

210 ''' 

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

212 

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

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

215 

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

217 the named representation (C{bool}). 

218 

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

220 L{pygeodesy.toDMS} for futher C{prec_fmt_ints} details. 

221 ''' 

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

223 

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

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

226 

227 @see: Function L{pygeodesy.toDMS} for futher details. 

228 ''' 

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

230 p = 8 if prec is None else prec 

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

232 else: 

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

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

235 

236 

237class Degrees_(Degrees): 

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

239 ''' 

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

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

242 

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

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

245 L{pygeodesy.parseDMS}). 

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

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

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

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

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

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

252 B{C{arg}} and B{C{name}} ones. 

253 

254 @returns: A C{Degrees} instance. 

255 

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

257 ''' 

258 if name_arg: 

259 name, arg = _arg_name_arg2(arg, **name_arg) 

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

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

262 txt = Fmt.limit(below=low) 

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

264 txt = Fmt.limit(above=high) 

265 else: 

266 return self 

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

268 

269 

270class Degrees2(Float): 

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

272 ''' 

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

274 '''See L{Float}. 

275 ''' 

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

277 

278 

279class Radians(Float): 

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

281 ''' 

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

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

284 

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

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

287 L{pygeodesy.parseRad}). 

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

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

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

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

292 or C{None} for unclipped). 

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

294 B{C{arg}} and B{C{name}} ones. 

295 

296 @returns: A C{Radians} instance. 

297 

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

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

300 ''' 

301 if name_arg: 

302 name, arg = _arg_name_arg2(arg, **name_arg) 

303 try: 

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

305 Error=Error, name=name) 

306 except Exception as x: 

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

308 

309 def toDegrees(self): 

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

311 ''' 

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

313 

314 def toRadians(self): 

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

316 ''' 

317 return self 

318 

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

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

321 

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

323 the named representation (C{bool}). 

324 

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

326 L{pygeodesy.toDMS} for more documentation. 

327 ''' 

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

329 

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

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

332 

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

334 ''' 

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

336 

337 

338class Radians_(Radians): 

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

340 ''' 

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

342 '''New C{Radians_} instance. 

343 

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

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

346 L{pygeodesy.parseRad}). 

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

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

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

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

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

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

353 B{C{arg}} and B{C{name}} ones. 

354 

355 @returns: A C{Radians_} instance. 

356 

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

358 ''' 

359 if name_arg: 

360 name, arg = _arg_name_arg2(arg, **name_arg) 

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

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

363 txt = Fmt.limit(below=low) 

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

365 txt = Fmt.limit(above=high) 

366 else: 

367 return self 

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

369 

370 

371class Radians2(Float_): 

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

373 ''' 

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

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

376 ''' 

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

378 

379 

380class Bearing(Degrees): 

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

382 ''' 

383 _ddd_ = 1 

384 _suf_ = _N_ * 3 # always suffix N 

385 

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

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

388 ''' 

389 if name_arg: 

390 name, arg = _arg_name_arg2(arg, **name_arg) 

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

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

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

394 

395 

396class Bearing_(Radians): 

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

398 ''' 

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

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

401 ''' 

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

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

404 

405 

406class Distance(Float): 

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

408 ''' 

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

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

411 ''' 

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

413 

414 

415class Distance_(Float_): 

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

417 ''' 

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

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

420 ''' 

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

422 

423 

424class Easting(Float): 

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

426 ''' 

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

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

429 

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

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

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

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

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

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

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

437 B{C{arg}} and B{C{name}} ones. 

438 

439 @returns: An C{Easting} instance. 

440 

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

442 ''' 

443 if name_arg: 

444 name, arg = _arg_name_arg2(arg, **name_arg) 

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

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

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

448 elif falsed and self < 0: 

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

450 return self 

451 

452 

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

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

455 calendar year. 

456 ''' 

457 _std_repr = False 

458 

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

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

461 ''' 

462 if name_arg: 

463 name, arg = _arg_name_arg2(arg, **name_arg) 

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

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

466 

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

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

469 

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

471 representation (C{bool}). 

472 

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

474 ''' 

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

476 

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

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

479 

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

481 ''' 

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

483 

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

485 

486 

487class Feet(Float): 

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

489 ''' 

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

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

492 ''' 

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

494 

495 

496class FIx(Float_): 

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

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

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

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

501 len(points)]}. 

502 ''' 

503 _fin = 0 

504 

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

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

507 

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

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

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

511 @kwarg name_Error: Optional C{B{name}=NN} (C{str}) and keyword 

512 argument C{B{Error}=UnitError}. 

513 

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

515 

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

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

518 

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

520 ''' 

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

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

523 i = int(f) 

524 r = f - float(i) 

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

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

527 elif r > EPS1: 

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

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

530 f._fin = n 

531 return f 

532 

533 @Property_RO 

534 def fin(self): 

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

536 ''' 

537 return self._fin 

538 

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

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

541 

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

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

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

545 B{C{points}} (C{bool}) or C{None} for backward 

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

547 I{averaged} lat- and longitudes. 

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

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

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

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

552 or C{None}. 

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

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

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

556 

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

558 

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

560 not subscriptable or not closed. 

561 

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

563 argument. 

564 

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

566 ''' 

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

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

569 LatLon=LatLon, Vector=Vector, **kwds) 

570 

571 

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

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

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

575 if not 0 <= i < n: 

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

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

578 

579 

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

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

582 ''' 

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

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

585 ''' 

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

587 

588 

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

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

591 ''' 

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

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

594 ''' 

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

596 

597 

598class HeightX(Height): 

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

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

601 ''' 

602 pass 

603 

604 

605def _heigHt(inst, height): 

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

607 ''' 

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

609 

610 

611class Lam(Radians): 

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

613 ''' 

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

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

616 ''' 

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

618 

619 

620class Lam_(Lam): 

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

622 ''' 

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

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

625 ''' 

626 if name_arg: 

627 name, arg = _arg_name_arg2(arg, **name_arg) 

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

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

630 

631 

632class Lat(Degrees): 

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

634 ''' 

635 _ddd_ = 2 

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

637 

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

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

640 ''' 

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

642 

643 

644class Lat_(Degrees_): 

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

646 ''' 

647 _ddd_ = 2 

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

649 

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

651 '''See L{Degrees_}. 

652 ''' 

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

654 

655 

656class Lon(Degrees): 

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

658 ''' 

659 _ddd_ = 3 

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

661 

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

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

664 ''' 

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

666 

667 

668class Lon_(Degrees_): 

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

670 ''' 

671 _ddd_ = 3 

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

673 

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

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

676 ''' 

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

678 

679 

680class Meter(Float): 

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

682 ''' 

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

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

685 ''' 

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

687 

688 def __repr__(self): 

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

690 

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

692 

693 @note: Use C{env} variable C{PYGEODESY_METER_STD_REPR=std} prior to C{import 

694 pygeodesy} to get the standard C{repr} or set property C{std_repr=False} 

695 to always get the named C{toRepr} representation. 

696 ''' 

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

698 

699 

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

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

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

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

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

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

706 

707 

708class Meter_(Float_): 

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

710 ''' 

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

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

713 ''' 

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

715 

716 

717class Meter2(Float_): 

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

719 ''' 

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

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

722 ''' 

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

724 

725 

726class Meter3(Float_): 

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

728 ''' 

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

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

731 ''' 

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

733 

734 

735class Northing(Float): 

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

737 ''' 

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

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

740 

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

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

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

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

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

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

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

748 B{C{arg}} and B{C{name}} ones. 

749 

750 @returns: A C{Northing} instance. 

751 

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

753 ''' 

754 if name_arg: 

755 name, arg = _arg_name_arg2(arg, **name_arg) 

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

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

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

759 elif falsed and self < 0: 

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

761 return self 

762 

763 

764class Number_(Int_): 

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

766 ''' 

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

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

769 ''' 

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

771 

772 

773class Phi(Radians): 

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

775 ''' 

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

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

778 ''' 

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

780 

781 

782class Phi_(Phi): 

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

784 ''' 

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

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

787 ''' 

788 if name_arg: 

789 name, arg = _arg_name_arg2(arg, **name_arg) 

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

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

792 

793 

794class Precision_(Int_): 

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

796 ''' 

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

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

799 ''' 

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

801 

802 

803class Radius_(Float_): 

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

805 ''' 

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

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

808 ''' 

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

810 

811 

812class Scalar(Float): 

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

814 ''' 

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

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

817 ''' 

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

819 

820 

821class Scalar_(Float_): 

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

823 ''' 

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

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

826 ''' 

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

828 

829 

830class Zone(Int): 

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

832 ''' 

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

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

835 ''' 

836 # usually low=_UTMUPS_ZONE_MIN, high=_UTMUPS_ZONE_MAX 

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

838 

839 

840_Scalars = Float, Float_, Scalar, Scalar_ 

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

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

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

844_Radii = _Meters + (Radius, Radius_) 

845 

846 

847def _isDegrees(obj): 

848 # Check for valid degrees types. 

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

850 

851 

852def _isHeight(obj): 

853 # Check for valid heigth types. 

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

855 

856 

857def _isMeter(obj): 

858 # Check for valid meter types. 

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

860 

861 

862def _isRadius(obj): 

863 # Check for valid earth radius types. 

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

865 

866 

867def _isScalar(obj): 

868 # Check for pure scalar types. 

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

870 

871 

872def _toUnit(Unit, arg, name=NN, **Error): 

873 '''(INTERNAL) Wrap C{arg} in a C{name}d C{Unit}. 

874 ''' 

875 if not (issubclassof(Unit, _NamedUnit) and isinstance(arg, Unit) and 

876 _xattr(arg, name=NN) == name): 

877 arg = Unit(arg, name=name, **Error) 

878 return arg 

879 

880 

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

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

883 ''' 

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

885 return _IsnotError(*r, **name_value_Error) 

886 

887 

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

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

890 ''' 

891 _xsubclassof(_NamedUnit, Base=Base) 

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

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

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

895 

896 

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

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

899 ''' 

900 _xsubclassof(_NamedUnit, Base=Base) 

901 if issubclassof(units, Base): 

902 return units 

903 elif isstr(units): 

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

905 raise _IsnotError(*(_.__name__ for _ in (Base, Str, str)), units=units) 

906 

907 

908def _std_repr(*Classes): 

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

910 ''' 

911 for C in Classes: 

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

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

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

915 C._std_repr = False 

916 

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

918del _std_repr 

919 

920__all__ += _ALL_DOCS(_NamedUnit) 

921 

922# **) MIT License 

923# 

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

925# 

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

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

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

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

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

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

932# 

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

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

935# 

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

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

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

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

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

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

942# OTHER DEALINGS IN THE SOFTWARE.