Coverage for pygeodesy / units.py: 96%

305 statements  

« prev     ^ index     » next       coverage.py v7.14.0, created at 2026-07-07 13:30 -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 

9# from pygeodesy.angles import isAng # _MODS 

10from pygeodesy.basics import isscalar, issubclassof, signOf, typename 

11from pygeodesy.constants import EPS, EPS1, PI, PI2, PI_2, _0_0, _0_001, \ 

12 _isNAN, _umod_360 

13from pygeodesy.dms import F__F, F__F_, S_NUL, S_SEP, parseDMS, parseRad, _toDMS 

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

15# from pygeodesy.internals import typename # from .basics 

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

17 _degrees_, _degrees2_, _distance_, _E_, _easting_, \ 

18 _epoch_, _EW_, _feet_, _height_, _lam_, _lat_, _lon_, \ 

19 _meter_, _meter2_, _N_, _negative_, _northing_, _NS_, \ 

20 _NSEW_, _number_, _PERCENT_, _phi_, _precision_, \ 

21 _radians_, _radians2_, _radius_, _S_, _scalar_, \ 

22 _W_, _zone_, _std_ # PYCHOK used! 

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

24# from pygeodesy.named import _name__ # _MODS 

25from pygeodesy.props import Property_RO 

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

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

28 

29from math import degrees, radians 

30 

31__all__ = _ALL_LAZY.units 

32__version__ = '26.07.04' 

33 

34 

35class Float_(Float): 

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

37 ''' 

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

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

40 

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

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

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

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

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

46 

47 @returns: A named C{Float_}. 

48 

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

50 ''' 

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

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

53 if t: 

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

55 return self 

56 

57 

58class Int_(Int): 

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

60 ''' 

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

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

63 

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

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

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

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

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

69 

70 @returns: A named L{Int_}. 

71 

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

73 ''' 

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

75 t = _xlimits(self, low, high) 

76 if t: 

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

78 return self 

79 

80 

81class Bool(Int, _NamedUnit): 

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

83 ''' 

84 # _std_repr = True # set below 

85 _bool_True_or_False = None 

86 

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

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

89 

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

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

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

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

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

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

96 

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

98 

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

100 ''' 

101 if name_arg: 

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

103 try: 

104 b = bool(arg) 

105 except Exception as x: 

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

107 

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

109 self._bool_True_or_False = b 

110 return self 

111 

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

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

114 return self._bool_True_or_False 

115 

116 __nonzero__ = __bool__ # PYCHOK Python 2- 

117 

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

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

120 

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

122 

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

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

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

126 ''' 

127 r = repr(self._bool_True_or_False) 

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

129 

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

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

132 ''' 

133 return str(self._bool_True_or_False) 

134 

135 

136class Band(Str): 

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

138 ''' 

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

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

141 ''' 

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

143 

144 

145class Degrees(Float): 

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

147 ''' 

148 _ddd_ = 1 # default for .dms._toDMS 

149 _sep_ = S_SEP 

150 _suf_ = (S_NUL,) * 3 

151 

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

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

154 

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

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

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

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

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

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

161 or C{None} for unclipped). 

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

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

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

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

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

167 

168 @returns: A C{Degrees} instance. 

169 

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

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

172 ''' 

173 if name_arg: 

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

175 try: 

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

177 if wrap: 

178 _xcallable(wrap=wrap) 

179 arg = wrap(arg) 

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

181 except Exception as x: 

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

183 return self 

184 

185 def toDegrees(self): 

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

187 ''' 

188 return self 

189 

190 def toRadians(self): 

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

192 ''' 

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

194 

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

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

197 

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

199 the named representation (C{bool}). 

200 

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

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

203 ''' 

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

205 

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

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

208 

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

210 ''' 

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

212 p = 8 if prec is None else prec 

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

214 else: 

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

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

217 

218 

219class Degrees_(Degrees): 

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

221 ''' 

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

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

224 

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

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

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

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

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

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

231 

232 @returns: A named C{Degrees}. 

233 

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

235 ''' 

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

237 t = _xlimits(self, low, high) 

238 if t: 

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

240 return self 

241 

242 

243class Degrees2(Float): 

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

245 ''' 

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

247 '''See L{Float}. 

248 ''' 

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

250 

251 

252class Radians(Float): 

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

254 ''' 

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

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

257 

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

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

260 L{pygeodesy.parseRad}). 

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

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

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

264 or C{None} for unclipped). 

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

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

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

268 

269 @returns: A named C{Radians}. 

270 

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

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

273 ''' 

274 if name_arg: 

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

276 try: 

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

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

279 except Exception as x: 

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

281 

282 def toDegrees(self): 

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

284 ''' 

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

286 

287 def toRadians(self): 

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

289 ''' 

290 return self 

291 

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

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

294 

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

296 the named representation (C{bool}). 

297 

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

299 L{pygeodesy.toDMS} for more documentation. 

300 ''' 

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

302 

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

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

305 

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

307 ''' 

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

309 

310 

311class Radians_(Radians): 

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

313 ''' 

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

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

316 

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

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

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

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

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

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

323 

324 @returns: A named C{Radians_}. 

325 

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

327 ''' 

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

329 t = _xlimits(self, low, high) 

330 if t: 

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

332 return self 

333 

334 

335class Radians2(Float_): 

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

337 ''' 

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

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

340 ''' 

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

342 

343 

344def _Degrees_new(cls, **arg_name_suffix_clip_Error_name_arg): 

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

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

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

348 

349 

350class Azimuth(Degrees): 

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

352 ''' 

353 _ddd_ = 1 

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

355 

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

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

358 anti-clockwise, see L{Degrees}. 

359 ''' 

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

361 

362 

363class Bearing(Degrees): 

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

365 ''' 

366 _ddd_ = 1 

367 _suf_ = _N_ * 3 # always suffix N 

368 

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

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

371 ''' 

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

373 

374 

375class Bearing_(Radians): 

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

377 ''' 

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

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

380 ''' 

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

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

383 

384 

385class Distance(Float): 

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

387 ''' 

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

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

390 ''' 

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

392 

393 

394class Distance_(Float_): 

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

396 ''' 

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

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

399 ''' 

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

401 

402 

403class _EasNorBase(Float): 

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

405 ''' 

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

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

408 low = self < 0 

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

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

411 elif low and falsed: 

412 t = _COMMASPACE_(_negative_, 'falsed') 

413 else: 

414 return self 

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

416 

417 

418class Easting(_EasNorBase): 

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

420 ''' 

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

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

423 

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

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

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

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

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

429 

430 @returns: A named C{Easting}. 

431 

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

433 ''' 

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

435 

436 

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

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

439 calendar year. 

440 ''' 

441 _std_repr = False 

442 

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

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

445 ''' 

446 if name_arg: 

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

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

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

450 

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

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

453 

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

455 representation (C{bool}). 

456 

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

458 ''' 

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

460 

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

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

463 

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

465 ''' 

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

467 

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

469 

470 

471class Feet(Float): 

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

473 ''' 

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

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

476 ''' 

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

478 

479 

480class FIx(Float_): 

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

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

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

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

485 ''' 

486 _fin = 0 

487 

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

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

490 

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

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

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

494 @kwarg Error: Optional error to raise. 

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

496 

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

498 

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

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

501 

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

503 ''' 

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

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

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

507 i = int(f) 

508 r = f - float(i) 

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

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

511 elif r > EPS1: 

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

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

514 f._fin = n 

515 return f 

516 

517 @Property_RO 

518 def fin(self): 

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

520 ''' 

521 return self._fin 

522 

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

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

525 

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

527 C{other}[]). 

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

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

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

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

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

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

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

535 

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

537 

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

539 subscriptable or not closed. 

540 

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

542 

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

544 ''' 

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

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

547 

548 

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

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

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

552 if not 0 <= i < n: 

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

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

555 

556 

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

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

559 ''' 

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

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

562 ''' 

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

564 

565 

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

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

568 ''' 

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

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

571 ''' 

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

573 

574 

575class HeightX(Height): 

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

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

578 ''' 

579 pass 

580 

581 

582def _heigHt(inst, height): 

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

584 ''' 

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

586 

587 

588class Lam(Radians): 

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

590 ''' 

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

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

593 ''' 

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

595 

596 

597class Lamd(Lam): 

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

599 ''' 

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

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

602 ''' 

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

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

605 

606 

607class Lat(Degrees): 

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

609 ''' 

610 _ddd_ = 2 

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

612 

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

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

615 ''' 

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

617 

618 

619class Lat_(Degrees_): 

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

621 ''' 

622 _ddd_ = 2 

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

624 

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

626 '''See L{Degrees_}. 

627 ''' 

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

629 

630 

631def _Lat0(lat): # in .ellipsods and .triaxials.bases 

632 '''(INTERNAL) Get latitude. 

633 ''' 

634 return Lat(lat.degrees0 if _MODS.angles.isAng(lat) else lat) 

635 

636 

637class Lon(Degrees): 

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

639 ''' 

640 _ddd_ = 3 

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

642 

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

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

645 ''' 

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

647 

648 

649class Lon_(Degrees_): 

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

651 ''' 

652 _ddd_ = 3 

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

654 

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

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

657 ''' 

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

659 

660 

661class Meter(Float): 

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

663 ''' 

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

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

666 ''' 

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

668 

669 def __repr__(self): 

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

671 

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

673 

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

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

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

677 ''' 

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

679 

680 

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

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

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

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

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

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

687 

688 

689class Meter_(Float_): 

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

691 ''' 

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

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

694 ''' 

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

696 

697 

698class Meter2(Float_): 

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

700 ''' 

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

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

703 ''' 

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

705 

706 

707class Meter3(Float_): 

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

709 ''' 

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

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

712 ''' 

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

714 

715 

716class Northing(_EasNorBase): 

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

718 ''' 

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

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

721 

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

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

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

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

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

727 

728 @returns: A named C{Northing}. 

729 

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

731 ''' 

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

733 

734 

735class Number_(Int_): 

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

737 ''' 

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

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

740 ''' 

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

742 

743 

744class Phi(Radians): 

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

746 ''' 

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

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

749 ''' 

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

751 

752 

753class Phid(Phi): 

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

755 ''' 

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

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

758 ''' 

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

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

761 

762 

763class Precision_(Int_): 

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

765 ''' 

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

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

768 ''' 

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

770 

771 

772class Radius_(Float_): 

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

774 ''' 

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

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

777 ''' 

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

779 

780 

781class Scalar(Float): 

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

783 ''' 

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

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

786 ''' 

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

788 

789 

790class Scalar_(Float_): 

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

792 ''' 

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

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

795 ''' 

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

797 

798 

799class Zone(Int): 

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

801 ''' 

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

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

804 ''' 

805 # usually low=_UTMUPS_ZONE_MIN, high=_UTMUPS_ZONE_MAX 

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

807 

808 

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

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

811_Radians = (Radians, Radians_) # PYCHOK unused 

812_Radii = _Meters + (Radius, Radius_) 

813_ScalarU = Float, Float_, Scalar, Scalar_ 

814 

815 

816def _isDegrees(obj, iscalar=True): 

817 # Check for valid degrees types. 

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

819 

820 

821def _isHeight(obj, iscalar=True): 

822 # Check for valid height types. 

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

824 

825 

826def _isMeter(obj, iscalar=True): 

827 # Check for valid meter types. 

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

829 

830 

831def _isRadians(obj, iscalar=True): 

832 # Check for valid radian types. 

833 return isinstance(obj, _Radians) or (iscalar and _isScalar(obj)) 

834 

835 

836def _isRadius(obj, iscalar=True): 

837 # Check for valid earth radius types. 

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

839 

840 

841def _isScalar(obj, iscalar=True): 

842 # Check for pure scalar types. 

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

844 

845 

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

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

848 ''' 

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

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

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

852 return arg 

853 

854 

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

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

857 ''' 

858 if (low is not None) and (arg < low or _isNAN(arg)): 

859 if g: 

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

861 t = Fmt.limit(below=low) 

862 elif (high is not None) and (arg > high or _isNAN(arg)): 

863 if g: 

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

865 t = Fmt.limit(above=high) 

866 else: 

867 t = NN 

868 return t 

869 

870 

871def _std_repr(*Classes): 

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

873 ''' 

874 from pygeodesy.internals import _getenv 

875 for C in Classes: 

876 if hasattr(C, typename(_std_repr)): # noqa: F821 del 

877 env = 'PYGEODESY_%s_STD_REPR' % (typename(C).upper(),) 

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

879 C._std_repr = False 

880 

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

882del _std_repr 

883 

884__all__ += _ALL_DOCS(_NamedUnit) 

885 

886# **) MIT License 

887# 

888# Copyright (C) 2016-2026 -- mrJean1 at Gmail -- All Rights Reserved. 

889# 

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

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

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

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

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

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

896# 

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

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

899# 

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

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

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

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

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

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

906# OTHER DEALINGS IN THE SOFTWARE.