Coverage for pygeodesy/units.py: 96%

301 statements  

« prev     ^ index     » next       coverage.py v7.6.1, created at 2025-01-10 16:55 -0500

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

13from pygeodesy.errors import _AssertionError, TRFError, UnitError, _xattr, _xcallable 

14from pygeodesy.interns import NN, _azimuth_, _band_, _bearing_, _COMMASPACE_, \ 

15 _degrees_, _degrees2_, _distance_, _E_, _easting_, \ 

16 _epoch_, _EW_, _feet_, _height_, _lam_, _lat_, _LatLon_, \ 

17 _lon_, _meter_, _meter2_, _N_, _negative_, _northing_, \ 

18 _NS_, _NSEW_, _number_, _PERCENT_, _phi_, _precision_, \ 

19 _radians_, _radians2_, _radius_, _S_, _scalar_, \ 

20 _units_, _W_, _zone_, _std_ # PYCHOK used! 

21from pygeodesy.lazily import _ALL_DOCS, _ALL_LAZY, _ALL_MODS as _MODS 

22# from pygeodesy.named import _name__ # _MODS 

23from pygeodesy.props import Property_RO 

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

25from pygeodesy.unitsBase import Float, Int, _NamedUnit, Radius, Str, Fmt, fstr 

26 

27from math import degrees, isnan, radians 

28 

29__all__ = _ALL_LAZY.units 

30__version__ = '24.11.14' 

31 

32 

33class Float_(Float): 

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

35 ''' 

36 def __new__(cls, arg=None, name=NN, low=EPS, high=None, **Error_name_arg): 

37 '''New, named C{Float_}, see L{Float}. 

38 

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

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

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

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

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

44 

45 @returns: A named C{Float_}. 

46 

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

48 ''' 

49 self = Float.__new__(cls, arg=arg, name=name, **Error_name_arg) 

50 t = _xlimits(self, low, high, g=True) 

51 if t: 

52 raise _NamedUnit._Error(cls, arg, name, txt=t, **Error_name_arg) 

53 return self 

54 

55 

56class Int_(Int): 

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

58 ''' 

59 def __new__(cls, arg=None, name=NN, low=0, high=None, **Error_name_arg): 

60 '''New, named C{int} instance with limits, see L{Int}. 

61 

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

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

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

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

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

67 

68 @returns: A named L{Int_}. 

69 

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

71 ''' 

72 self = Int.__new__(cls, arg=arg, name=name, **Error_name_arg) 

73 t = _xlimits(self, low, high) 

74 if t: 

75 raise _NamedUnit._Error(cls, arg, name, txt=t, **Error_name_arg) 

76 return self 

77 

78 

79class Bool(Int, _NamedUnit): 

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

81 ''' 

82 # _std_repr = True # set below 

83 _bool_True_or_False = None 

84 

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

86 '''New, named C{Bool}. 

87 

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

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

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

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

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

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

94 

95 @returns: A named L{Bool}, C{bool}-like. 

96 

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

98 ''' 

99 if name_arg: 

100 name, arg = _NamedUnit._arg_name_arg2(arg, **name_arg) 

101 try: 

102 b = bool(arg) 

103 except Exception as x: 

104 raise _NamedUnit._Error(cls, arg, name, Error, cause=x) 

105 

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

107 self._bool_True_or_False = b 

108 return self 

109 

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

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

112 return self._bool_True_or_False 

113 

114 __nonzero__ = __bool__ # PYCHOK Python 2- 

115 

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

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

118 

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

120 

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

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

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

124 ''' 

125 r = repr(self._bool_True_or_False) 

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

127 

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

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

130 ''' 

131 return str(self._bool_True_or_False) 

132 

133 

134class Band(Str): 

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

136 ''' 

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

138 '''New, named L{Band}, see L{Str}. 

139 ''' 

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

141 

142 

143class Degrees(Float): 

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

145 ''' 

146 _ddd_ = 1 # default for .dms._toDMS 

147 _sep_ = S_SEP 

148 _suf_ = (S_NUL,) * 3 

149 

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

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

152 

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

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

155 function L{parseDMS<pygeodesy.dms.parseDMS>}). 

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

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

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

159 or C{None} for unclipped). 

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

161 L{wrap180<pygeodesy.wrap180>} or L{wrap360<pygeodesy.wrap360>}). 

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

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

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

165 

166 @returns: A C{Degrees} instance. 

167 

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

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

170 ''' 

171 if name_arg: 

172 name, arg = _NamedUnit._arg_name_arg2(arg, name, **name_arg) 

173 try: 

174 arg = parseDMS(arg, suffix=suffix, clip=clip) 

175 if wrap: 

176 _xcallable(wrap=wrap) 

177 arg = wrap(arg) 

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

179 except Exception as x: 

180 raise _NamedUnit._Error(cls, arg, name, Error, cause=x) 

181 return self 

182 

183 def toDegrees(self): 

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

185 ''' 

186 return self 

187 

188 def toRadians(self): 

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

190 ''' 

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

192 

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

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

195 

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

197 the named representation (C{bool}). 

198 

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

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

201 ''' 

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

203 

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

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

206 

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

208 ''' 

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

210 p = 8 if prec is None else prec 

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

212 else: 

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

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

215 

216 

217class Degrees_(Degrees): 

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

219 ''' 

220 def __new__(cls, arg=None, name=_degrees_, low=None, high=None, **suffix_Error_name_arg): 

221 '''New, named C{Degrees_}, see L{Degrees} and L{Float}. 

222 

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

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

225 function L{parseDMS<pygeodesy.dms.parseDMS>}). 

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

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

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

229 

230 @returns: A named C{Degrees}. 

231 

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

233 ''' 

234 self = Degrees.__new__(cls, arg=arg, name=name, clip=0, **suffix_Error_name_arg) 

235 t = _xlimits(self, low, high) 

236 if t: 

237 raise _NamedUnit._Error(cls, arg, name, txt=t, **suffix_Error_name_arg) 

238 return self 

239 

240 

241class Degrees2(Float): 

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

243 ''' 

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

245 '''See L{Float}. 

246 ''' 

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

248 

249 

250class Radians(Float): 

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

252 ''' 

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

254 '''New, named C{Radians}, see L{Float}. 

255 

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

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

258 L{pygeodesy.parseRad}). 

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

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

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

262 or C{None} for unclipped). 

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

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

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

266 

267 @returns: A named C{Radians}. 

268 

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

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

271 ''' 

272 if name_arg: 

273 name, arg = _NamedUnit._arg_name_arg2(arg, name, **name_arg) 

274 try: 

275 arg = parseRad(arg, suffix=suffix, clip=clip) 

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

277 except Exception as x: 

278 raise _NamedUnit._Error(cls, arg, name, Error, cause=x) 

279 

280 def toDegrees(self): 

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

282 ''' 

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

284 

285 def toRadians(self): 

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

287 ''' 

288 return self 

289 

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

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

292 

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

294 the named representation (C{bool}). 

295 

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

297 L{pygeodesy.toDMS} for more documentation. 

298 ''' 

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

300 

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

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

303 

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

305 ''' 

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

307 

308 

309class Radians_(Radians): 

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

311 ''' 

312 def __new__(cls, arg=None, name=_radians_, low=_0_0, high=PI2, **suffix_Error_name_arg): 

313 '''New, named C{Radians_}, see L{Radians}. 

314 

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

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

317 function L{parseRad<pygeodesy.dms.parseRad>}). 

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

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

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

321 

322 @returns: A named C{Radians_}. 

323 

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

325 ''' 

326 self = Radians.__new__(cls, arg=arg, name=name, **suffix_Error_name_arg) 

327 t = _xlimits(self, low, high) 

328 if t: 

329 raise _NamedUnit._Error(cls, arg, name, txt=t, **suffix_Error_name_arg) 

330 return self 

331 

332 

333class Radians2(Float_): 

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

335 ''' 

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

337 '''New, named L{Radians2}, see L{Float_}. 

338 ''' 

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

340 

341 

342def _Degrees_new(cls, **arg_name_suffix_clip_Error_name_arg): 

343 d = Degrees.__new__(cls, **arg_name_suffix_clip_Error_name_arg) 

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

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

346 

347 

348class Azimuth(Degrees): 

349 '''Named C{float} representing an azimuth in compass C{degrees} from (true) North. 

350 ''' 

351 _ddd_ = 1 

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

353 

354 def __new__(cls, arg=None, name=_azimuth_, **clip_Error_name_arg): 

355 '''New, named L{Azimuth} with optional suffix 'E' for clockwise or 'W' for 

356 anti-clockwise, see L{Degrees}. 

357 ''' 

358 return _Degrees_new(cls, arg=arg, name=name, suffix=_EW_, **clip_Error_name_arg) 

359 

360 

361class Bearing(Degrees): 

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

363 ''' 

364 _ddd_ = 1 

365 _suf_ = _N_ * 3 # always suffix N 

366 

367 def __new__(cls, arg=None, name=_bearing_, **clip_Error_name_arg): 

368 '''New, named L{Bearing}, see L{Degrees}. 

369 ''' 

370 return _Degrees_new(cls, arg=arg, name=name, suffix=_N_, **clip_Error_name_arg) 

371 

372 

373class Bearing_(Radians): 

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

375 ''' 

376 def __new__(cls, arg=None, **name_clip_Error_name_arg): 

377 '''New, named L{Bearing_}, see L{Bearing} and L{Radians}. 

378 ''' 

379 d = Bearing.__new__(cls, arg=arg, **name_clip_Error_name_arg) 

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

381 

382 

383class Distance(Float): 

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

385 ''' 

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

387 '''New, named L{Distance}, see L{Float}. 

388 ''' 

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

390 

391 

392class Distance_(Float_): 

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

394 ''' 

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

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

397 ''' 

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

399 

400 

401class _EasNorBase(Float): 

402 '''(INTERNAL) L{Easting} and L{Northing} base class. 

403 ''' 

404 def __new__(cls, arg, name, falsed, high, **Error_name_arg): 

405 self = Float.__new__(cls, arg=arg, name=name, **Error_name_arg) 

406 low = self < 0 

407 if (high is not None) and (low or self > high): # like Veness 

408 t = _negative_ if low else Fmt.limit(above=high) 

409 elif low and falsed: 

410 t = _COMMASPACE_(_negative_, 'falsed') 

411 else: 

412 return self 

413 raise _NamedUnit._Error(cls, arg, name, txt=t, **Error_name_arg) 

414 

415 

416class Easting(_EasNorBase): 

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

418 ''' 

419 def __new__(cls, arg=None, name=_easting_, falsed=False, high=None, **Error_name_arg): 

420 '''New, named C{Easting} or C{Easting of Point}, see L{Float}. 

421 

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

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

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

425 @kwarg falsed: If C{True}, the B{C{arg}} value is falsed (C{bool}). 

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

427 

428 @returns: A named C{Easting}. 

429 

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

431 ''' 

432 return _EasNorBase.__new__(cls, arg, name, falsed, high, **Error_name_arg) 

433 

434 

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

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

437 calendar year. 

438 ''' 

439 _std_repr = False 

440 

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

442 '''New, named L{Epoch}, see L{Float_}. 

443 ''' 

444 if name_arg: 

445 name, arg = _NamedUnit._arg_name_arg2(arg, name, **name_arg) 

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

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

448 

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

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

451 

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

453 representation (C{bool}). 

454 

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

456 ''' 

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

458 

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

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

461 

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

463 ''' 

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

465 

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

467 

468 

469class Feet(Float): 

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

471 ''' 

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

473 '''New, named L{Feet}, see L{Float}. 

474 ''' 

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

476 

477 

478class FIx(Float_): 

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

480 or C{tuple} of C{points}, typically. A C{float} I{Fractional Index} 

481 C{fi} represents a location on the edge between C{points[int(fi)]} and 

482 C{points[(int(fi) + 1) % len(points)]}. 

483 ''' 

484 _fin = 0 

485 

486 def __new__(cls, fi, fin=None, Error=UnitError, **name): 

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

488 

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

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

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

492 @kwarg Error: Optional error to raise. 

493 @kwarg name: Optional C{B{name}=NN} (C{str}). 

494 

495 @return: A named B{C{fi}} (L{FIx}). 

496 

497 @note: The returned B{C{fi}} may exceed the B{C{len}}, number of 

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

499 

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

501 ''' 

502 _ = _MODS.named._name__(name) if name else NN # check error 

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

504 f = Float_.__new__(cls, fi, low=_0_0, high=n, Error=Error, **name) 

505 i = int(f) 

506 r = f - float(i) 

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

508 f = Float_.__new__(cls, i, low=_0_0, Error=Error, **name) 

509 elif r > EPS1: 

510 f = Float_.__new__(cls, i + 1, high=n, Error=Error, **name) 

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

512 f._fin = n 

513 return f 

514 

515 @Property_RO 

516 def fin(self): 

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

518 ''' 

519 return self._fin 

520 

521 def fractional(self, points, wrap=None, **LatLon_or_Vector_and_kwds): 

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

523 

524 @arg points: The points (C{LatLon}[], L{Numpy2LatLon}[], L{Tuple2LatLon}[] or 

525 C{other}[]). 

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

527 (C{bool}) or C{None} for backward compatible L{LatLon2Tuple} or 

528 B{C{LatLon}} with I{averaged} lat- and longitudes. 

529 @kwarg LatLon_or_Vector_and_kwds: Optional C{B{LatLon}=None} I{or} C{B{Vector}=None} 

530 to return the I{intermediate}, I{fractional} point and optionally, 

531 additional B{C{LatLon}} I{or} B{C{Vector}} keyword arguments, see 

532 function L{fractional<pygeodesy.points.fractional>}. 

533 

534 @return: See function L{fractional<pygeodesy.points.fractional>}. 

535 

536 @raise IndexError: In fractional index invalid or B{C{points}} not 

537 subscriptable or not closed. 

538 

539 @raise TypeError: Invalid B{C{LatLon}}, B{C{Vector}} or B{C{kwds}} argument. 

540 

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

542 ''' 

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

544 return _MODS.points.fractional(points, self, wrap=wrap, **LatLon_or_Vector_and_kwds) 

545 

546 

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

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

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

550 if not 0 <= i < n: 

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

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

553 

554 

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

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

557 ''' 

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

559 '''New, named L{Height}, see L{Float}. 

560 ''' 

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

562 

563 

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

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

566 ''' 

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

568 '''New, named L{Height}, see L{Float}. 

569 ''' 

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

571 

572 

573class HeightX(Height): 

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

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

576 ''' 

577 pass 

578 

579 

580def _heigHt(inst, height): 

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

582 ''' 

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

584 

585 

586class Lam(Radians): 

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

588 ''' 

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

590 '''New, named L{Lam}, see L{Radians}. 

591 ''' 

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

593 

594 

595class Lamd(Lam): 

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

597 ''' 

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

599 '''New, named L{Lamd}, see L{Lam} and L{Radians}. 

600 ''' 

601 d = Degrees(arg=arg, name=name, clip=clip, **Error_name_arg) 

602 return Lam.__new__(cls, radians(d), clip=radians(clip), name=d.name) 

603 

604 

605class Lat(Degrees): 

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

607 ''' 

608 _ddd_ = 2 

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

610 

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

612 '''New, named L{Lat}, see L{Degrees}. 

613 ''' 

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

615 

616 

617class Lat_(Degrees_): 

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

619 ''' 

620 _ddd_ = 2 

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

622 

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

624 '''See L{Degrees_}. 

625 ''' 

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

627 

628 

629class Lon(Degrees): 

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

631 ''' 

632 _ddd_ = 3 

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

634 

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

636 '''New, named L{Lon}, see L{Degrees}. 

637 ''' 

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

639 

640 

641class Lon_(Degrees_): 

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

643 ''' 

644 _ddd_ = 3 

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

646 

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

648 '''New, named L{Lon_}, see L{Lon} and L{Degrees_}. 

649 ''' 

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

651 

652 

653class Meter(Float): 

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

655 ''' 

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

657 '''New, named L{Meter}, see L{Float}. 

658 ''' 

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

660 

661 def __repr__(self): 

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

663 

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

665 

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

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

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

669 ''' 

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

671 

672 

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

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

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

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

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

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

679 

680 

681class Meter_(Float_): 

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

683 ''' 

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

685 '''New, named L{Meter_}, see L{Meter} and L{Float_}. 

686 ''' 

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

688 

689 

690class Meter2(Float_): 

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

692 ''' 

693 def __new__(cls, arg=None, name=_meter2_, **Error_name_arg): 

694 '''New, named L{Meter2}, see L{Float_}. 

695 ''' 

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

697 

698 

699class Meter3(Float_): 

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

701 ''' 

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

703 '''New, named L{Meter3}, see L{Float_}. 

704 ''' 

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

706 

707 

708class Northing(_EasNorBase): 

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

710 ''' 

711 def __new__(cls, arg=None, name=_northing_, falsed=False, high=None, **Error_name_arg): 

712 '''New, named C{Northing} or C{Northing of point}, see L{Float}. 

713 

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

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

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

717 @kwarg falsed: If C{True}, the B{C{arg}} value is falsed (C{bool}). 

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

719 

720 @returns: A named C{Northing}. 

721 

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

723 ''' 

724 return _EasNorBase.__new__(cls, arg, name, falsed, high, **Error_name_arg) 

725 

726 

727class Number_(Int_): 

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

729 ''' 

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

731 '''New, named L{Number_}, see L{Int_}. 

732 ''' 

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

734 

735 

736class Phi(Radians): 

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

738 ''' 

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

740 '''New, named L{Phi}, see L{Radians}. 

741 ''' 

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

743 

744 

745class Phid(Phi): 

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

747 ''' 

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

749 '''New, named L{Phid}, see L{Phi} and L{Radians}. 

750 ''' 

751 d = Degrees(arg=arg, name=name, clip=clip, **Error_name_arg) 

752 return Phi.__new__(cls, arg=radians(d), clip=radians(clip), name=d.name) 

753 

754 

755class Precision_(Int_): 

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

757 ''' 

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

759 '''New, named L{Precision_}, see L{Int_}. 

760 ''' 

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

762 

763 

764class Radius_(Float_): 

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

766 ''' 

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

768 '''New, named L{Radius_}, see L{Radius} and L{Float}. 

769 ''' 

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

771 

772 

773class Scalar(Float): 

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

775 ''' 

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

777 '''New, named L{Scalar}, see L{Float}. 

778 ''' 

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

780 

781 

782class Scalar_(Float_): 

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

784 ''' 

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

786 '''New, named L{Scalar_}, see L{Scalar} and L{Float_}. 

787 ''' 

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

789 

790 

791class Zone(Int): 

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

793 ''' 

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

795 '''New, named L{Zone}, see L{Int} 

796 ''' 

797 # usually low=_UTMUPS_ZONE_MIN, high=_UTMUPS_ZONE_MAX 

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

799 

800 

801_Degrees = (Azimuth, Bearing, Bearing_, Degrees, Degrees_) 

802_Meters = (Distance, Distance_, Meter, Meter_) 

803_Radians = (Radians, Radians_) # PYCHOK unused 

804_Radii = _Meters + (Radius, Radius_) 

805_ScalarU = Float, Float_, Scalar, Scalar_ 

806 

807 

808def _isDegrees(obj, iscalar=True): 

809 # Check for valid degrees types. 

810 return isinstance(obj, _Degrees) or (iscalar and _isScalar(obj)) 

811 

812 

813def _isHeight(obj, iscalar=True): 

814 # Check for valid height types. 

815 return isinstance(obj, _Meters) or (iscalar and _isScalar(obj)) 

816 

817 

818def _isMeter(obj, iscalar=True): 

819 # Check for valid meter types. 

820 return isinstance(obj, _Meters) or (iscalar and _isScalar(obj)) 

821 

822 

823def _isRadius(obj, iscalar=True): 

824 # Check for valid earth radius types. 

825 return isinstance(obj, _Radii) or (iscalar and _isScalar(obj)) 

826 

827 

828def _isScalar(obj, iscalar=True): 

829 # Check for pure scalar types. 

830 return isinstance(obj, _ScalarU) or (iscalar and isscalar(obj) and not isinstance(obj, _NamedUnit)) 

831 

832 

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

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

835 ''' 

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

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

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

839 return arg 

840 

841 

842def _xlimits(arg, low, high, g=False): 

843 '''(INTERNAL) Check C{low <= arg <= high}. 

844 ''' 

845 if (low is not None) and (arg < low or isnan(arg)): 

846 if g: 

847 low = Fmt.g(low, prec=6, ints=isinstance(arg, Epoch)) 

848 t = Fmt.limit(below=low) 

849 elif (high is not None) and (arg > high or isnan(arg)): 

850 if g: 

851 high = Fmt.g(high, prec=6, ints=isinstance(arg, Epoch)) 

852 t = Fmt.limit(above=high) 

853 else: 

854 t = NN 

855 return t 

856 

857 

858def _std_repr(*Classes): 

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

860 ''' 

861 from pygeodesy.internals import _getenv 

862 for C in Classes: 

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

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

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

866 C._std_repr = False 

867 

868_std_repr(Azimuth, Bearing, Bool, Degrees, Epoch, Float, Int, Meter, Radians, Str) # PYCHOK expected 

869del _std_repr 

870 

871__all__ += _ALL_DOCS(_NamedUnit) 

872 

873# **) MIT License 

874# 

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

876# 

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

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

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

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

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

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

883# 

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

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

886# 

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

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

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

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

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

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

893# OTHER DEALINGS IN THE SOFTWARE.