Coverage for pygeodesy/units.py: 96%

301 statements  

« prev     ^ index     » next       coverage.py v7.6.1, created at 2025-04-25 13:15 -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, issubclassof, signOf, typename 

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 

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

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

16 _degrees_, _degrees2_, _distance_, _E_, _easting_, \ 

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

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

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

20 _radians_, _radians2_, _radius_, _S_, _scalar_, \ 

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

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

23# from pygeodesy.named import _name__ # _MODS 

24from pygeodesy.props import Property_RO 

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

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

27 

28from math import degrees, isnan, radians 

29 

30__all__ = _ALL_LAZY.units 

31__version__ = '25.04.14' 

32 

33 

34class Float_(Float): 

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

36 ''' 

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

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

39 

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

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

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

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

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

45 

46 @returns: A named C{Float_}. 

47 

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

49 ''' 

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

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

52 if t: 

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

54 return self 

55 

56 

57class Int_(Int): 

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

59 ''' 

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

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

62 

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

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

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

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

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

68 

69 @returns: A named L{Int_}. 

70 

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

72 ''' 

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

74 t = _xlimits(self, low, high) 

75 if t: 

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

77 return self 

78 

79 

80class Bool(Int, _NamedUnit): 

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

82 ''' 

83 # _std_repr = True # set below 

84 _bool_True_or_False = None 

85 

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

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

88 

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

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

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

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

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

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

95 

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

97 

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

99 ''' 

100 if name_arg: 

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

102 try: 

103 b = bool(arg) 

104 except Exception as x: 

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

106 

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

108 self._bool_True_or_False = b 

109 return self 

110 

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

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

113 return self._bool_True_or_False 

114 

115 __nonzero__ = __bool__ # PYCHOK Python 2- 

116 

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

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

119 

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

121 

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

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

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

125 ''' 

126 r = repr(self._bool_True_or_False) 

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

128 

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

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

131 ''' 

132 return str(self._bool_True_or_False) 

133 

134 

135class Band(Str): 

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

137 ''' 

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

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

140 ''' 

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

142 

143 

144class Degrees(Float): 

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

146 ''' 

147 _ddd_ = 1 # default for .dms._toDMS 

148 _sep_ = S_SEP 

149 _suf_ = (S_NUL,) * 3 

150 

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

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

153 

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

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

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

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

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

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

160 or C{None} for unclipped). 

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

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

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

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

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

166 

167 @returns: A C{Degrees} instance. 

168 

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

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

171 ''' 

172 if name_arg: 

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

174 try: 

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

176 if wrap: 

177 _xcallable(wrap=wrap) 

178 arg = wrap(arg) 

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

180 except Exception as x: 

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

182 return self 

183 

184 def toDegrees(self): 

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

186 ''' 

187 return self 

188 

189 def toRadians(self): 

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

191 ''' 

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

193 

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

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

196 

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

198 the named representation (C{bool}). 

199 

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

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

202 ''' 

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

204 

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

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

207 

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

209 ''' 

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

211 p = 8 if prec is None else prec 

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

213 else: 

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

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

216 

217 

218class Degrees_(Degrees): 

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

220 ''' 

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

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

223 

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

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

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

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

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

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

230 

231 @returns: A named C{Degrees}. 

232 

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

234 ''' 

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

236 t = _xlimits(self, low, high) 

237 if t: 

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

239 return self 

240 

241 

242class Degrees2(Float): 

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

244 ''' 

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

246 '''See L{Float}. 

247 ''' 

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

249 

250 

251class Radians(Float): 

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

253 ''' 

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

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

256 

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

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

259 L{pygeodesy.parseRad}). 

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

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

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

263 or C{None} for unclipped). 

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

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

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

267 

268 @returns: A named C{Radians}. 

269 

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

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

272 ''' 

273 if name_arg: 

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

275 try: 

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

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

278 except Exception as x: 

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

280 

281 def toDegrees(self): 

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

283 ''' 

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

285 

286 def toRadians(self): 

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

288 ''' 

289 return self 

290 

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

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

293 

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

295 the named representation (C{bool}). 

296 

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

298 L{pygeodesy.toDMS} for more documentation. 

299 ''' 

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

301 

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

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

304 

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

306 ''' 

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

308 

309 

310class Radians_(Radians): 

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

312 ''' 

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

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

315 

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

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

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

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

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

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

322 

323 @returns: A named C{Radians_}. 

324 

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

326 ''' 

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

328 t = _xlimits(self, low, high) 

329 if t: 

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

331 return self 

332 

333 

334class Radians2(Float_): 

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

336 ''' 

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

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

339 ''' 

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

341 

342 

343def _Degrees_new(cls, **arg_name_suffix_clip_Error_name_arg): 

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

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

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

347 

348 

349class Azimuth(Degrees): 

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

351 ''' 

352 _ddd_ = 1 

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

354 

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

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

357 anti-clockwise, see L{Degrees}. 

358 ''' 

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

360 

361 

362class Bearing(Degrees): 

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

364 ''' 

365 _ddd_ = 1 

366 _suf_ = _N_ * 3 # always suffix N 

367 

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

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

370 ''' 

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

372 

373 

374class Bearing_(Radians): 

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

376 ''' 

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

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

379 ''' 

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

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

382 

383 

384class Distance(Float): 

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

386 ''' 

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

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

389 ''' 

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

391 

392 

393class Distance_(Float_): 

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

395 ''' 

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

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

398 ''' 

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

400 

401 

402class _EasNorBase(Float): 

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

404 ''' 

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

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

407 low = self < 0 

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

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

410 elif low and falsed: 

411 t = _COMMASPACE_(_negative_, 'falsed') 

412 else: 

413 return self 

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

415 

416 

417class Easting(_EasNorBase): 

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

419 ''' 

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

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

422 

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

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

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

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

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

428 

429 @returns: A named C{Easting}. 

430 

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

432 ''' 

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

434 

435 

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

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

438 calendar year. 

439 ''' 

440 _std_repr = False 

441 

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

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

444 ''' 

445 if name_arg: 

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

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

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

449 

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

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

452 

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

454 representation (C{bool}). 

455 

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

457 ''' 

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

459 

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

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

462 

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

464 ''' 

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

466 

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

468 

469 

470class Feet(Float): 

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

472 ''' 

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

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

475 ''' 

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

477 

478 

479class FIx(Float_): 

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

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

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

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

484 ''' 

485 _fin = 0 

486 

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

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

489 

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

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

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

493 @kwarg Error: Optional error to raise. 

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

495 

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

497 

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

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

500 

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

502 ''' 

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

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

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

506 i = int(f) 

507 r = f - float(i) 

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

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

510 elif r > EPS1: 

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

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

513 f._fin = n 

514 return f 

515 

516 @Property_RO 

517 def fin(self): 

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

519 ''' 

520 return self._fin 

521 

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

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

524 

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

526 C{other}[]). 

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

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

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

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

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

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

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

534 

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

536 

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

538 subscriptable or not closed. 

539 

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

541 

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

543 ''' 

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

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

546 

547 

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

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

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

551 if not 0 <= i < n: 

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

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

554 

555 

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

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

558 ''' 

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

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

561 ''' 

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

563 

564 

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

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

567 ''' 

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

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

570 ''' 

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

572 

573 

574class HeightX(Height): 

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

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

577 ''' 

578 pass 

579 

580 

581def _heigHt(inst, height): 

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

583 ''' 

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

585 

586 

587class Lam(Radians): 

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

589 ''' 

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

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

592 ''' 

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

594 

595 

596class Lamd(Lam): 

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

598 ''' 

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

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

601 ''' 

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

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

604 

605 

606class Lat(Degrees): 

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

608 ''' 

609 _ddd_ = 2 

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

611 

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

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

614 ''' 

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

616 

617 

618class Lat_(Degrees_): 

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

620 ''' 

621 _ddd_ = 2 

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

623 

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

625 '''See L{Degrees_}. 

626 ''' 

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

628 

629 

630class Lon(Degrees): 

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

632 ''' 

633 _ddd_ = 3 

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

635 

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

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

638 ''' 

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

640 

641 

642class Lon_(Degrees_): 

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

644 ''' 

645 _ddd_ = 3 

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

647 

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

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

650 ''' 

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

652 

653 

654class Meter(Float): 

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

656 ''' 

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

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

659 ''' 

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

661 

662 def __repr__(self): 

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

664 

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

666 

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

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

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

670 ''' 

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

672 

673 

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

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

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

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

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

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

680 

681 

682class Meter_(Float_): 

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

684 ''' 

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

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

687 ''' 

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

689 

690 

691class Meter2(Float_): 

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

693 ''' 

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

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

696 ''' 

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

698 

699 

700class Meter3(Float_): 

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

702 ''' 

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

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

705 ''' 

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

707 

708 

709class Northing(_EasNorBase): 

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

711 ''' 

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

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

714 

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

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

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

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

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

720 

721 @returns: A named C{Northing}. 

722 

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

724 ''' 

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

726 

727 

728class Number_(Int_): 

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

730 ''' 

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

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

733 ''' 

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

735 

736 

737class Phi(Radians): 

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

739 ''' 

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

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

742 ''' 

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

744 

745 

746class Phid(Phi): 

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

748 ''' 

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

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

751 ''' 

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

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

754 

755 

756class Precision_(Int_): 

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

758 ''' 

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

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

761 ''' 

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

763 

764 

765class Radius_(Float_): 

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

767 ''' 

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

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

770 ''' 

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

772 

773 

774class Scalar(Float): 

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

776 ''' 

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

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

779 ''' 

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

781 

782 

783class Scalar_(Float_): 

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

785 ''' 

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

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

788 ''' 

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

790 

791 

792class Zone(Int): 

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

794 ''' 

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

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

797 ''' 

798 # usually low=_UTMUPS_ZONE_MIN, high=_UTMUPS_ZONE_MAX 

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

800 

801 

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

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

804_Radians = (Radians, Radians_) # PYCHOK unused 

805_Radii = _Meters + (Radius, Radius_) 

806_ScalarU = Float, Float_, Scalar, Scalar_ 

807 

808 

809def _isDegrees(obj, iscalar=True): 

810 # Check for valid degrees types. 

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

812 

813 

814def _isHeight(obj, iscalar=True): 

815 # Check for valid height types. 

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

817 

818 

819def _isMeter(obj, iscalar=True): 

820 # Check for valid meter types. 

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

822 

823 

824def _isRadius(obj, iscalar=True): 

825 # Check for valid earth radius types. 

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

827 

828 

829def _isScalar(obj, iscalar=True): 

830 # Check for pure scalar types. 

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

832 

833 

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

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

836 ''' 

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

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

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

840 return arg 

841 

842 

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

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

845 ''' 

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

847 if g: 

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

849 t = Fmt.limit(below=low) 

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

851 if g: 

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

853 t = Fmt.limit(above=high) 

854 else: 

855 t = NN 

856 return t 

857 

858 

859def _std_repr(*Classes): 

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

861 ''' 

862 from pygeodesy.internals import _getenv 

863 for C in Classes: 

864 if hasattr(C, typename(_std_repr)): # PYCHOK del _std_repr 

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

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

867 C._std_repr = False 

868 

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

870del _std_repr 

871 

872__all__ += _ALL_DOCS(_NamedUnit) 

873 

874# **) MIT License 

875# 

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

877# 

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

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

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

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

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

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

884# 

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

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

887# 

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

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

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

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

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

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

894# OTHER DEALINGS IN THE SOFTWARE.