Coverage for pygeodesy/units.py: 96%

1

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

3

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

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

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

7'''

8

9# from pygeodesy.angles import isAng # _MODS

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

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

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

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

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

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

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

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

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

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

20 _radians2_, _radius_, _S_, _scalar_, \

21 _W_, _zone_, _std_ # PYCHOK used!

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

23# from pygeodesy.named import _name__ # _MODS

24 from pygeodesy.props import Property_RO

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

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

27

28 from math import degrees, isnan, radians

29

30 __all__ = _ALL_LAZY.units

31 __version__ = '26.03.12'

32

33

34 class 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

57 class 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

80 class 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

135 class 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

144 class 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

218 class 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

242 class 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

251 class 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

310 class 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

334 class 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

343 def _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

349 class 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

362 class 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

374 class 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

384 class 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

393 class 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

402 class _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

417 class 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

436 class 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

470 class 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

479 class 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

548 def _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

556 class 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

565 class 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

574 class 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

581 def _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

587 class 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

596 class 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

606 class 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

618 class 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

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

631 '''(INTERNAL) Get latitude.

632 '''

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

634

635

636 class Lon(Degrees):

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

638 '''

639 _ddd_ = 3

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

641

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

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

644 '''

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

646

647

648 class Lon_(Degrees_):

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

650 '''

651 _ddd_ = 3

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

653

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

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

656 '''

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

658

659

660 class Meter(Float):

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

662 '''

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

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

665 '''

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

667

668 def __repr__(self):

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

670

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

672

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

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

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

676 '''

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

678

679

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

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

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

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

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

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

686

687

688 class Meter_(Float_):

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

690 '''

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

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

693 '''

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

695

696

697 class Meter2(Float_):

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

699 '''

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

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

702 '''

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

704

705

706 class Meter3(Float_):

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

708 '''

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

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

711 '''

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

713

714

715 class Northing(_EasNorBase):

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

717 '''

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

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

720

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

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

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

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

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

726

727 @returns: A named C{Northing}.

728

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

730 '''

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

732

733

734 class Number_(Int_):

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

736 '''

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

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

739 '''

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

741

742

743 class Phi(Radians):

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

745 '''

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

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

748 '''

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

750

751

752 class Phid(Phi):

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

754 '''

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

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

757 '''

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

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

760

761

762 class Precision_(Int_):

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

764 '''

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

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

767 '''

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

769

770

771 class Radius_(Float_):

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

773 '''

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

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

776 '''

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

778

779

780 class Scalar(Float):

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

782 '''

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

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

785 '''

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

787

788

789 class Scalar_(Float_):

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

791 '''

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

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

794 '''

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

796

797

798 class Zone(Int):

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

800 '''

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

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

803 '''

804 # usually low=_UTMUPS_ZONE_MIN, high=_UTMUPS_ZONE_MAX

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

806

807

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

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

810 _Radians = (Radians, Radians_) # PYCHOK unused

811 _Radii = _Meters + (Radius, Radius_)

812 _ScalarU = Float, Float_, Scalar, Scalar_

813

814

815 def _isDegrees(obj, iscalar=True):

816 # Check for valid degrees types.

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

818

819

820 def _isHeight(obj, iscalar=True):

821 # Check for valid height types.

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

823

824

825 def _isMeter(obj, iscalar=True):

826 # Check for valid meter types.

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

828

829

830 def _isRadians(obj, iscalar=True):

831 # Check for valid radian types.

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

833

834

835 def _isRadius(obj, iscalar=True):

836 # Check for valid earth radius types.

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

838

839

840 def _isScalar(obj, iscalar=True):

841 # Check for pure scalar types.

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

843

844

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

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

847 '''

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

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

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

851 return arg

852

853

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

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

856 '''

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

858 if g:

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

860 t = Fmt.limit(below=low)

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

862 if g:

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

864 t = Fmt.limit(above=high)

865 else:

866 t = NN

867 return t

868

869

870 def _std_repr(*Classes):

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

872 '''

873 from pygeodesy.internals import _getenv

874 for C in Classes:

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

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

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

878 C._std_repr = False

879

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

881 del _std_repr

882

883 __all__ += _ALL_DOCS(_NamedUnit)

884

885# **) MIT License

886#

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

888#

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

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

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

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

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

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

895#

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

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

898#

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

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

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

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

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

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

905# OTHER DEALINGS IN THE SOFTWARE.