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.basics import isscalar, issubclassof, signOf

10 from pygeodesy.constants import EPS, EPS1, PI, PI2, PI_2, _umod_360, _0_0, \

11 _0_001, _0_5, INT0 # PYCHOK for .mgrs, .namedTuples

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.interns import NN, _band_, _bearing_, _COMMASPACE_, _degrees_, \

15 _degrees2_, _distance_, _E_, _easting_, _epoch_, _EW_, \

16 _feet_, _height_, _lam_, _lat_, _LatLon_, _lon_, \

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

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

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

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

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

22# from pygeodesy.named import _name__ # _MODS

23 from pygeodesy.props import Property_RO

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

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

26

27 from math import degrees, radians

28

29__all__ = _ALL_LAZY.units

30__version__ = '24.06.29'

31

32

33 class Float_(Float):

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

35 '''

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

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

38

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

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

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

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

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

44

45 @returns: A named C{Float_}.

46

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

48 '''

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

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

51 if t:

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

53 return self

54

55

56 class Int_(Int):

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

58 '''

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

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

61

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

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

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

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

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

67

68 @returns: A named L{Int_}.

69

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

71 '''

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

73 t = _xlimits(self, low, high)

74 if t:

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

76 return self

77

78

79 class Bool(Int, _NamedUnit):

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

81 '''

82 # _std_repr = True # set below

83 _bool_True_or_False = None

84

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

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

87

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

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

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

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

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

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

94

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

96

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

98 '''

99 if name_arg:

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

101 try:

102 b = bool(arg)

103 except Exception as x:

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

105

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

107 self._bool_True_or_False = b

108 return self

109

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

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

112 return self._bool_True_or_False

113

114 __nonzero__ = __bool__ # PYCHOK Python 2-

115

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

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

118

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

120

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

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

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

124 '''

125 r = repr(self._bool_True_or_False)

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

127

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

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

130 '''

131 return str(self._bool_True_or_False)

132

133

134 class Band(Str):

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

136 '''

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

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

139 '''

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

141

142

143 class Degrees(Float):

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

145 '''

146 _ddd_ = 1 # default for .dms._toDMS

147 _sep_ = S_SEP

148 _suf_ = (S_NUL,) * 3

149

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

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

152

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

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

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

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

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

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

159 or C{None} for unclipped).

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

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

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

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

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

165

166 @returns: A C{Degrees} instance.

167

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

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

170 '''

171 if name_arg:

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

173 try:

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

175 if wrap:

176 _xcallable(wrap=wrap)

177 arg = wrap(arg)

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

179 except Exception as x:

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

181 return self

182

183 def toDegrees(self):

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

185 '''

186 return self

187

188 def toRadians(self):

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

190 '''

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

192

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

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

195

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

197 the named representation (C{bool}).

198

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

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

201 '''

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

203

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

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

206

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

208 '''

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

210 p = 8 if prec is None else prec

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

212 else:

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

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

215

216

217 class Degrees_(Degrees):

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

219 '''

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

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

222

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

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

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

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

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

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

229

230 @returns: A named C{Degrees}.

231

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

233 '''

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

235 t = _xlimits(self, low, high)

236 if t:

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

238 return self

239

240

241 class Degrees2(Float):

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

243 '''

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

245 '''See L{Float}.

246 '''

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

248

249

250 class Radians(Float):

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

252 '''

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

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

255

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

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

258 L{pygeodesy.parseRad}).

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

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

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

262 or C{None} for unclipped).

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

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

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

266

267 @returns: A named C{Radians}.

268

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

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

271 '''

272 if name_arg:

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

274 try:

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

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

277 except Exception as x:

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

279

280 def toDegrees(self):

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

282 '''

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

284

285 def toRadians(self):

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

287 '''

288 return self

289

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

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

292

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

294 the named representation (C{bool}).

295

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

297 L{pygeodesy.toDMS} for more documentation.

298 '''

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

300

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

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

303

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

305 '''

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

307

308

309 class Radians_(Radians):

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

311 '''

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

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

314

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

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

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

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

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

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

321

322 @returns: A named C{Radians_}.

323

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

325 '''

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

327 t = _xlimits(self, low, high)

328 if t:

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

330 return self

331

332

333 class Radians2(Float_):

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

335 '''

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

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

338 '''

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

340

341

342 class Bearing(Degrees):

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

344 '''

345 _ddd_ = 1

346 _suf_ = _N_ * 3 # always suffix N

347

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

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

350 '''

351 d = Degrees.__new__(cls, arg=arg, name=name, suffix=_N_, clip=clip, **Error_name_arg)

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

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

354

355

356 class Bearing_(Radians):

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

358 '''

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

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

361 '''

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

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

364

365

366 class Distance(Float):

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

368 '''

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

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

371 '''

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

373

374

375 class Distance_(Float_):

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

377 '''

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

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

380 '''

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

382

383

384 class _EasNorBase(Float):

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

386 '''

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

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

389 low = self < 0

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

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

392 elif low and falsed:

393 t = _COMMASPACE_(_negative_, 'falsed')

394 else:

395 return self

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

397

398

399 class Easting(_EasNorBase):

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

401 '''

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

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

404

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

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

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

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

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

410

411 @returns: A named C{Easting}.

412

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

414 '''

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

416

417

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

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

420 calendar year.

421 '''

422 _std_repr = False

423

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

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

426 '''

427 if name_arg:

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

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

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

431

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

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

434

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

436 representation (C{bool}).

437

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

439 '''

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

441

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

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

444

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

446 '''

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

448

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

450

451

452 class Feet(Float):

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

454 '''

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

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

457 '''

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

459

460

461 class FIx(Float_):

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

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

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

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

466 '''

467 _fin = 0

468

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

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

471

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

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

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

475 @kwarg Error: Optional error to raise.

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

477

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

479

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

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

482

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

484 '''

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

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

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

488 i = int(f)

489 r = f - float(i)

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

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

492 elif r > EPS1:

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

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

495 f._fin = n

496 return f

497

498 @Property_RO

499 def fin(self):

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

501 '''

502 return self._fin

503

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

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

506

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

508 C{other}[]).

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

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

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

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

513 to return the I{intermediate}, I{fractional} point and optional,

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

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

516

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

518

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

520 subscriptable or not closed.

521

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

523

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

525 '''

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

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

528

529

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

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

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

533 if not 0 <= i < n:

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

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

536

537

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

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

540 '''

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

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

543 '''

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

545

546

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

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

549 '''

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

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

552 '''

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

554

555

556 class HeightX(Height):

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

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

559 '''

560 pass

561

562

563 def _heigHt(inst, height):

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

565 '''

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

567

568

569 class Lam(Radians):

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

571 '''

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

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

574 '''

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

576

577

578 class Lamd(Lam):

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

580 '''

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

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

583 '''

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

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

586

587

588 class Lat(Degrees):

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

590 '''

591 _ddd_ = 2

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

593

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

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

596 '''

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

598

599

600 class Lat_(Degrees_):

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

602 '''

603 _ddd_ = 2

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

605

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

607 '''See L{Degrees_}.

608 '''

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

610

611

612 class Lon(Degrees):

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

614 '''

615 _ddd_ = 3

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

617

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

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

620 '''

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

622

623

624 class Lon_(Degrees_):

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

626 '''

627 _ddd_ = 3

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

629

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

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

632 '''

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

634

635

636 class Meter(Float):

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

638 '''

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

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

641 '''

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

643

644 def __repr__(self):

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

646

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

648

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

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

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

652 '''

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

654

655

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

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

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

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

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

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

662

663

664 class Meter_(Float_):

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

666 '''

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

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

669 '''

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

671

672

673 class Meter2(Float_):

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

675 '''

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

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

678 '''

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

680

681

682 class Meter3(Float_):

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

684 '''

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

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

687 '''

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

689

690

691 class Northing(_EasNorBase):

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

693 '''

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

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

696

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

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

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

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

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

702

703 @returns: A named C{Northing}.

704

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

706 '''

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

708

709

710 class Number_(Int_):

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

712 '''

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

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

715 '''

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

717

718

719 class Phi(Radians):

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

721 '''

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

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

724 '''

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

726

727

728 class Phid(Phi):

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

730 '''

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

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

733 '''

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

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

736

737

738 class Precision_(Int_):

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

740 '''

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

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

743 '''

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

745

746

747 class Radius_(Float_):

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

749 '''

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

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

752 '''

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

754

755

756 class Scalar(Float):

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

758 '''

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

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

761 '''

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

763

764

765 class Scalar_(Float_):

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

767 '''

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

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

770 '''

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

772

773

774 class Zone(Int):

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

776 '''

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

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

779 '''

780 # usually low=_UTMUPS_ZONE_MIN, high=_UTMUPS_ZONE_MAX

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

782

783

784_ScalarU = Float, Float_, Scalar, Scalar_

785_Degrees = (Bearing, Bearing_, Degrees, Degrees_) + _ScalarU

786_Meters = (Distance, Distance_, Meter, Meter_) + _ScalarU

787_Radians = (Radians, Radians_) + _ScalarU # PYCHOK unused

788_Radii = _Meters + (Radius, Radius_)

789

790

791 def _isDegrees(obj):

792 # Check for valid degrees types.

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

794

795

796 def _isHeight(obj):

797 # Check for valid heigth types.

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

799

800

801 def _isMeter(obj):

802 # Check for valid meter types.

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

804

805

806 def _isRadius(obj):

807 # Check for valid earth radius types.

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

809

810

811 def _isScalar(obj):

812 # Check for pure scalar types.

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

814

815

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

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

818 '''

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

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

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

822 return arg

823

824

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

826 '''(INTERNAL) Check C{low <= unit <= high}.

827 '''

828 if (low is not None) and arg < low:

829 if g:

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

831 t = Fmt.limit(below=low)

832 elif (high is not None) and arg > high:

833 if g:

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

835 t = Fmt.limit(above=high)

836 else:

837 t = NN

838 return t

839

840

841 def _std_repr(*Classes):

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

843 '''

844 for C in Classes:

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

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

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

848 C._std_repr = False

849

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

851 del _std_repr

852

853__all__ += _ALL_DOCS(_NamedUnit)

854

855# **) MIT License

856#

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

858#

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

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

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

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

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

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

865#

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

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

868#

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

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

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

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

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

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

875# OTHER DEALINGS IN THE SOFTWARE.