Coverage for pygeodesy/units.py: 95%

330 statements  

« prev     ^ index     » next       coverage.py v7.2.2, created at 2024-05-06 16:50 -0400

1 

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

3 

4u'''Various units, all sub-classes of C{Float}, C{Int} and 

5C{Str} from basic C{float}, C{int} respectively C{str} to 

6named units as L{Degrees}, L{Feet}, L{Meter}, L{Radians}, etc. 

7''' 

8 

9from pygeodesy.basics import isinstanceof, isscalar, isstr, issubclassof, \ 

10 signOf 

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

12 _umod_360, _0_0, _0_001, \ 

13 _0_5, INT0 # PYCHOK for .mgrs, .namedTuples 

14from pygeodesy.dms import F__F, F__F_, parseDMS, parseRad, \ 

15 S_NUL, S_SEP, _toDMS, toDMS 

16from pygeodesy.errors import _AssertionError, _IsnotError, TRFError, \ 

17 UnitError, _xkwds, _xkwds_item2 

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

19 _distance_, _E_, _easting_, _epoch_, _EW_, \ 

20 _feet_, _height_, _lam_, _lat_, \ 

21 _LatLon_, _lon_, _meter_, _meter2_, _N_, \ 

22 _northing_, _NS_, _NSEW_, _number_, _PERCENT_, \ 

23 _phi_, _precision_, _radians_, _radians2_, \ 

24 _radius_, _S_, _scalar_, _units_, \ 

25 _W_, _zone_, _std_ # PYCHOK used! 

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

27from pygeodesy.props import Property_RO 

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

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

30 Radius, Str # PYCHOK shared .namedTuples 

31from math import degrees, radians 

32 

33__all__ = _ALL_LAZY.units 

34__version__ = '24.02.20' 

35 

36_negative_falsed_ = 'negative, falsed' 

37 

38 

39class Float_(Float): 

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

41 ''' 

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

43 '''New C{Float_} instance. 

44 

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

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

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

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

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

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

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

52 and B{C{arg}}. 

53 

54 @returns: A C{Float_} instance. 

55 

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

57 ''' 

58 if name_arg: 

59 name, arg = _xkwds_item2(name_arg) 

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

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

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

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

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

65 else: 

66 return self 

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

68 

69 

70class Int_(Int): 

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

72 ''' 

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

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

75 

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

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

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

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

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

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

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

83 and B{C{arg}}. 

84 

85 @returns: An L{Int_} instance. 

86 

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

88 ''' 

89 if name_arg: 

90 name, arg = _xkwds_item2(name_arg) 

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

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

93 txt = Fmt.limit(below=low) 

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

95 txt = Fmt.limit(above=high) 

96 else: 

97 return self 

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

99 

100 

101class Bool(Int, _NamedUnit): 

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

103 ''' 

104 # _std_repr = True # set below 

105 _bool_True_or_False = None 

106 

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

108 '''New C{Bool} instance. 

109 

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

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

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

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

114 L{UnitError}. 

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

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

117 

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

119 

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

121 ''' 

122 if name_arg: 

123 name, arg = _xkwds_item2(name_arg) 

124 try: 

125 b = bool(arg) 

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

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

128 

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

130 self._bool_True_or_False = b 

131 return self 

132 

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

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

135 return self._bool_True_or_False 

136 

137 __nonzero__ = __bool__ # PYCHOK Python 2- 

138 

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

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

141 

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

143 representation (C{bool}). 

144 

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

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

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

148 get the named C{toRepr} representation. 

149 ''' 

150 r = repr(self._bool_True_or_False) 

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

152 

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

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

155 ''' 

156 return str(self._bool_True_or_False) 

157 

158 

159class Band(Str): 

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

161 ''' 

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

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

164 ''' 

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

166 

167 

168class Degrees(Float): 

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

170 ''' 

171 _ddd_ = 1 # default for .dms._toDMS 

172 _sep_ = S_SEP 

173 _suf_ = (S_NUL,) * 3 

174 

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

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

177 

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

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

180 parsable by L{pygeodesy.parseDMS}). 

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

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

183 L{UnitError}. 

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

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

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

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

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

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

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

191 

192 @returns: A C{Degrees} instance. 

193 

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

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

196 ''' 

197 if name_arg: 

198 name, arg = _xkwds_item2(name_arg) 

199 try: 

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

201 Error=Error, name=name) 

202 if wrap: 

203 w = wrap(d) 

204 if w != d: 

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

206 except Exception as x: 

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

208 return d 

209 

210 def toDegrees(self): 

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

212 ''' 

213 return self 

214 

215 def toRadians(self): 

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

217 ''' 

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

219 

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

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

222 

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

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

225 

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

227 L{pygeodesy.toDMS} for more documentation. 

228 ''' 

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

230 

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

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

233 

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

235 ''' 

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

237 p = 8 if prec is None else prec 

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

239 else: 

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

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

242 

243 

244class Degrees_(Degrees): 

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

246 ''' 

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

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

249 

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

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

252 L{pygeodesy.parseDMS}). 

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

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

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

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

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

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

259 and B{C{arg}}. 

260 

261 @returns: A C{Degrees} instance. 

262 

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

264 ''' 

265 if name_arg: 

266 name, arg = _xkwds_item2(name_arg) 

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

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

269 txt = Fmt.limit(below=low) 

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

271 txt = Fmt.limit(above=high) 

272 else: 

273 return self 

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

275 

276 

277class Degrees2(Float): 

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

279 ''' 

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

281 '''See L{Float}. 

282 ''' 

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

284 

285 

286class Radians(Float): 

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

288 ''' 

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

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

291 

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

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

294 by L{pygeodesy.parseRad}). 

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

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

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

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

299 or C{None} for unclipped). 

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

301 and B{C{arg}}. 

302 

303 @returns: A C{Radians} instance. 

304 

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

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

307 ''' 

308 if name_arg: 

309 name, arg = _xkwds_item2(name_arg) 

310 try: 

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

312 Error=Error, name=name) 

313 except Exception as x: 

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

315 

316 def toDegrees(self): 

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

318 ''' 

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

320 

321 def toRadians(self): 

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

323 ''' 

324 return self 

325 

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

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

328 

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

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

331 

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

333 L{pygeodesy.toDMS} for more documentation. 

334 ''' 

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

336 

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

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

339 

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

341 ''' 

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

343 

344 

345class Radians_(Radians): 

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

347 ''' 

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

349 '''New C{Radians_} instance. 

350 

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

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

353 L{pygeodesy.parseRad}). 

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

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

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

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

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

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

360 and B{C{arg}}. 

361 

362 @returns: A C{Radians_} instance. 

363 

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

365 ''' 

366 if name_arg: 

367 name, arg = _xkwds_item2(name_arg) 

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

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

370 txt = Fmt.limit(below=low) 

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

372 txt = Fmt.limit(above=high) 

373 else: 

374 return self 

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

376 

377 

378class Radians2(Float_): 

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

380 ''' 

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

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

383 ''' 

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

385 

386 

387class Bearing(Degrees): 

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

389 ''' 

390 _ddd_ = 1 

391 _suf_ = _N_ * 3 # always suffix N 

392 

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

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

395 ''' 

396 if name_arg: 

397 name, arg = _xkwds_item2(name_arg) 

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

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

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

401 

402 

403class Bearing_(Radians): 

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

405 ''' 

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

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

408 ''' 

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

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

411 

412 

413class Distance(Float): 

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

415 ''' 

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

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

418 ''' 

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

420 

421 

422class Distance_(Float_): 

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

424 ''' 

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

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

427 ''' 

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

429 

430 

431class Easting(Float): 

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

433 ''' 

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

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

436 

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

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

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

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

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

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

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

444 and B{C{arg}}. 

445 

446 @returns: An C{Easting} instance. 

447 

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

449 ''' 

450 if name_arg: 

451 name, arg = _xkwds_item2(name_arg) 

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

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

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

455 elif falsed and self < 0: 

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

457 return self 

458 

459 

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

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

462 calendar year. 

463 ''' 

464 _std_repr = False 

465 

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

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

468 ''' 

469 if name_arg: 

470 name, arg = _xkwds_item2(name_arg) 

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

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

473 

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

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

476 

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

478 representation (C{bool}). 

479 

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

481 ''' 

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

483 

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

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

486 

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

488 ''' 

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

490 

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

492 

493 

494class Feet(Float): 

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

496 ''' 

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

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

499 ''' 

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

501 

502 

503class FIx(Float_): 

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

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

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

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

508 len(points)]}. 

509 ''' 

510 _fin = 0 

511 

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

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

514 

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

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

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

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

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

520 

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

522 

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

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

525 

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

527 ''' 

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

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

530 i = int(f) 

531 r = f - float(i) 

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

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

534 elif r > EPS1: 

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

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

537 f._fin = n 

538 return f 

539 

540 @Property_RO 

541 def fin(self): 

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

543 ''' 

544 return self._fin 

545 

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

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

548 

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

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

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

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

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

554 I{averaged} lat- and longitudes. 

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

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

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

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

559 or C{None}. 

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

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

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

563 

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

565 

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

567 not subscriptable or not closed. 

568 

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

570 argument. 

571 

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

573 ''' 

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

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

576 LatLon=LatLon, Vector=Vector, **kwds) 

577 

578 

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

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

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

582 if not 0 <= i < n: 

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

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

585 

586 

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

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

589 ''' 

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

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

592 ''' 

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

594 

595 

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

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

598 ''' 

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

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

601 ''' 

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

603 

604 

605class HeightX(Height): 

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

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

608 ''' 

609 pass 

610 

611 

612def _heigHt(inst, height): 

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

614 ''' 

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

616 

617 

618class Lam(Radians): 

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

620 ''' 

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

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

623 ''' 

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

625 

626 

627class Lam_(Lam): 

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

629 ''' 

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

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

632 ''' 

633 if name_arg: 

634 name, arg = _xkwds_item2(name_arg) 

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

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

637 

638 

639class Lat(Degrees): 

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

641 ''' 

642 _ddd_ = 2 

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

644 

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

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

647 ''' 

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

649 

650 

651class Lat_(Degrees_): 

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

653 ''' 

654 _ddd_ = 2 

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

656 

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

658 '''See L{Degrees_}. 

659 ''' 

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

661 

662 

663class Lon(Degrees): 

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

665 ''' 

666 _ddd_ = 3 

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

668 

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

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

671 ''' 

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

673 

674 

675class Lon_(Degrees_): 

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

677 ''' 

678 _ddd_ = 3 

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

680 

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

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

683 ''' 

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

685 

686 

687class Meter(Float): 

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

689 ''' 

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

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

692 ''' 

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

694 

695 def __repr__(self): 

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

697 

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

699 

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

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

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

703 get the named C{toRepr} representation. 

704 ''' 

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

706 

707 

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

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

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

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

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

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

714 

715 

716class Meter_(Float_): 

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

718 ''' 

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

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

721 ''' 

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

723 

724 

725class Meter2(Float_): 

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

727 ''' 

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

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

730 ''' 

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

732 

733 

734class Meter3(Float_): 

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

736 ''' 

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

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

739 ''' 

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

741 

742 

743class Northing(Float): 

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

745 ''' 

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

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

748 

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

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

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

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

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

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

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

756 and B{C{arg}}. 

757 

758 @returns: A C{Northing} instance. 

759 

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

761 ''' 

762 if name_arg: 

763 name, arg = _xkwds_item2(name_arg) 

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

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

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

767 elif falsed and self < 0: 

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

769 return self 

770 

771 

772class Number_(Int_): 

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

774 ''' 

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

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

777 ''' 

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

779 

780 

781class Phi(Radians): 

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

783 ''' 

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

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

786 ''' 

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

788 

789 

790class Phi_(Phi): 

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

792 ''' 

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

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

795 ''' 

796 if name_arg: 

797 name, arg = _xkwds_item2(name_arg) 

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

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

800 

801 

802class Precision_(Int_): 

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

804 ''' 

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

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

807 ''' 

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

809 

810 

811class Radius_(Float_): 

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

813 ''' 

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

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

816 ''' 

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

818 

819 

820class Scalar(Float): 

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

822 ''' 

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

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

825 ''' 

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

827 

828 

829class Scalar_(Float_): 

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

831 ''' 

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

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

834 ''' 

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

836 

837 

838class Zone(Int): 

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

840 ''' 

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

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

843 ''' 

844 # usually low=_UTMUPS_ZONE_MIN, high=_UTMUPS_ZONE_MAX 

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

846 

847 

848_Scalars = Float, Float_, Scalar, Scalar_ 

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

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

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

852_Radii = _Meters + (Radius, Radius_) 

853 

854 

855def _isDegrees(obj): 

856 # Check for valid degrees types. 

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

858 

859 

860def _isHeight(obj): 

861 # Check for valid heigth types. 

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

863 

864 

865def _isMeter(obj): 

866 # Check for valid meter types. 

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

868 

869 

870def _isRadius(obj): 

871 # Check for valid earth radius types. 

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

873 

874 

875def _isScalar(obj): 

876 # Check for pure scalar types. 

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

878 

879 

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

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

882 ''' 

883 if toDMS_kwds: 

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

885 

886 for x in xs: 

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

888 s = None 

889 x = x.toDegrees() 

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

891 yield None if toDMS_kwds else s 

892 

893 

894def _toRadians(s, *xs): 

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

896 ''' 

897 for x in xs: 

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

899 s = None 

900 x = x.toRadians() 

901 yield x 

902 yield s 

903 

904 

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

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

907 ''' 

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

909 return _IsnotError(*r, **name_value_Error) 

910 

911 

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

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

914 ''' 

915 if not issubclassof(Base, _NamedUnit): 

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

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

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

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

920 

921 

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

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

924 ''' 

925 if not issubclassof(Base, _NamedUnit): 

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

927 elif issubclassof(units, Base): 

928 return units 

929 elif isstr(units): 

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

931 else: 

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

933 

934 

935def _std_repr(*Classes): 

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

937 ''' 

938 for C in Classes: 

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

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

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

942 C._std_repr = False 

943 

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

945del _std_repr 

946 

947__all__ += _ALL_DOCS(_NamedUnit) 

948 

949# **) MIT License 

950# 

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

952# 

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

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

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

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

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

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

959# 

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

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

962# 

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

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

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

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

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

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

969# OTHER DEALINGS IN THE SOFTWARE.