1

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

3

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

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

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

7'''

8

9 from pygeodesy.basics import isinstanceof, isscalar, isstr, 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, \

13 _toDMS, toDMS

14 from pygeodesy.errors import _AssertionError, _IsnotError, TRFError, UnitError, \

15 _xkwds, _xkwds_item2

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

17 _distance_, _E_, _easting_, _epoch_, _EW_, _feet_, \

18 _height_, _lam_, _lat_, _LatLon_, _lon_, _meter_, \

19 _meter2_, _N_, _northing_, _NS_, _NSEW_, _number_, \

20 _PERCENT_, _phi_, _precision_, _radians_, _radians2_, \

21 _radius_, _S_, _scalar_, _units_, _W_, _zone_, \

22 _std_ # PYCHOK used!

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

24 from pygeodesy.props import Property_RO

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

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

27 Radius, Str # PYCHOK shared .namedTuples

28

29 from math import degrees, radians

30

31__all__ = _ALL_LAZY.units

32__version__ = '24.05.10'

33

34_negative_falsed_ = 'negative, falsed'

35

36

37 class Float_(Float):

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

39 '''

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

41 '''New C{Float_} instance.

42

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

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

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

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

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

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

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

50 and B{C{arg}}.

51

52 @returns: A C{Float_} instance.

53

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

55 '''

56 if name_arg:

57 name, arg = _xkwds_item2(name_arg)

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

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

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

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

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

63 else:

64 return self

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

66

67

68 class Int_(Int):

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

70 '''

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

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

73

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

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

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

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

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

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

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

81 and B{C{arg}}.

82

83 @returns: An L{Int_} instance.

84

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

86 '''

87 if name_arg:

88 name, arg = _xkwds_item2(name_arg)

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

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

91 txt = Fmt.limit(below=low)

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

93 txt = Fmt.limit(above=high)

94 else:

95 return self

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

97

98

99 class Bool(Int, _NamedUnit):

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

101 '''

102 # _std_repr = True # set below

103 _bool_True_or_False = None

104

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

106 '''New C{Bool} instance.

107

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

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

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

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

112 L{UnitError}.

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

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

115

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

117

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

119 '''

120 if name_arg:

121 name, arg = _xkwds_item2(name_arg)

122 try:

123 b = bool(arg)

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

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

126

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

128 self._bool_True_or_False = b

129 return self

130

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

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

133 return self._bool_True_or_False

134

135 __nonzero__ = __bool__ # PYCHOK Python 2-

136

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

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

139

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

141 representation (C{bool}).

142

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

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

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

146 get the named C{toRepr} representation.

147 '''

148 r = repr(self._bool_True_or_False)

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

150

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

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

153 '''

154 return str(self._bool_True_or_False)

155

156

157 class Band(Str):

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

159 '''

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

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

162 '''

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

164

165

166 class Degrees(Float):

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

168 '''

169 _ddd_ = 1 # default for .dms._toDMS

170 _sep_ = S_SEP

171 _suf_ = (S_NUL,) * 3

172

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

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

175

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

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

178 parsable by L{pygeodesy.parseDMS}).

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

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

181 L{UnitError}.

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

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

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

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

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

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

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

189

190 @returns: A C{Degrees} instance.

191

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

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

194 '''

195 if name_arg:

196 name, arg = _xkwds_item2(name_arg)

197 try:

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

199 Error=Error, name=name)

200 if wrap:

201 w = wrap(d)

202 if w != d:

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

204 except Exception as x:

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

206 return d

207

208 def toDegrees(self):

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

210 '''

211 return self

212

213 def toRadians(self):

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

215 '''

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

217

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

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

220

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

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

223

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

225 L{pygeodesy.toDMS} for more documentation.

226 '''

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

228

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

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

231

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

233 '''

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

235 p = 8 if prec is None else prec

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

237 else:

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

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

240

241

242 class Degrees_(Degrees):

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

244 '''

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

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

247

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

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

250 L{pygeodesy.parseDMS}).

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

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

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

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

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

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

257 and B{C{arg}}.

258

259 @returns: A C{Degrees} instance.

260

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

262 '''

263 if name_arg:

264 name, arg = _xkwds_item2(name_arg)

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

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

267 txt = Fmt.limit(below=low)

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

269 txt = Fmt.limit(above=high)

270 else:

271 return self

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

273

274

275 class Degrees2(Float):

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

277 '''

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

279 '''See L{Float}.

280 '''

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

282

283

284 class Radians(Float):

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

286 '''

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

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

289

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

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

292 by L{pygeodesy.parseRad}).

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

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

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

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

297 or C{None} for unclipped).

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

299 and B{C{arg}}.

300

301 @returns: A C{Radians} instance.

302

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

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

305 '''

306 if name_arg:

307 name, arg = _xkwds_item2(name_arg)

308 try:

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

310 Error=Error, name=name)

311 except Exception as x:

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

313

314 def toDegrees(self):

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

316 '''

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

318

319 def toRadians(self):

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

321 '''

322 return self

323

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

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

326

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

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

329

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

331 L{pygeodesy.toDMS} for more documentation.

332 '''

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

334

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

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

337

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

339 '''

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

341

342

343 class Radians_(Radians):

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

345 '''

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

347 '''New C{Radians_} instance.

348

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

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

351 L{pygeodesy.parseRad}).

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

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

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

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

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

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

358 and B{C{arg}}.

359

360 @returns: A C{Radians_} instance.

361

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

363 '''

364 if name_arg:

365 name, arg = _xkwds_item2(name_arg)

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

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

368 txt = Fmt.limit(below=low)

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

370 txt = Fmt.limit(above=high)

371 else:

372 return self

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

374

375

376 class Radians2(Float_):

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

378 '''

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

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

381 '''

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

383

384

385 class Bearing(Degrees):

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

387 '''

388 _ddd_ = 1

389 _suf_ = _N_ * 3 # always suffix N

390

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

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

393 '''

394 if name_arg:

395 name, arg = _xkwds_item2(name_arg)

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

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

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

399

400

401 class Bearing_(Radians):

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

403 '''

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

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

406 '''

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

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

409

410

411 class Distance(Float):

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

413 '''

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

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

416 '''

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

418

419

420 class Distance_(Float_):

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

422 '''

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

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

425 '''

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

427

428

429 class Easting(Float):

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

431 '''

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

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

434

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

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

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

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

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

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

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

442 and B{C{arg}}.

443

444 @returns: An C{Easting} instance.

445

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

447 '''

448 if name_arg:

449 name, arg = _xkwds_item2(name_arg)

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

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

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

453 elif falsed and self < 0:

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

455 return self

456

457

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

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

460 calendar year.

461 '''

462 _std_repr = False

463

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

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

466 '''

467 if name_arg:

468 name, arg = _xkwds_item2(name_arg)

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

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

471

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

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

474

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

476 representation (C{bool}).

477

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

479 '''

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

481

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

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

484

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

486 '''

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

488

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

490

491

492 class Feet(Float):

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

494 '''

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

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

497 '''

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

499

500

501 class FIx(Float_):

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

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

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

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

506 len(points)]}.

507 '''

508 _fin = 0

509

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

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

512

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

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

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

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

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

518

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

520

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

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

523

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

525 '''

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

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

528 i = int(f)

529 r = f - float(i)

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

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

532 elif r > EPS1:

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

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

535 f._fin = n

536 return f

537

538 @Property_RO

539 def fin(self):

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

541 '''

542 return self._fin

543

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

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

546

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

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

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

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

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

552 I{averaged} lat- and longitudes.

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

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

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

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

557 or C{None}.

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

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

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

561

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

563

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

565 not subscriptable or not closed.

566

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

568 argument.

569

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

571 '''

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

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

574 LatLon=LatLon, Vector=Vector, **kwds)

575

576

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

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

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

580 if not 0 <= i < n:

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

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

583

584

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

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

587 '''

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

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

590 '''

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

592

593

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

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

596 '''

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

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

599 '''

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

601

602

603 class HeightX(Height):

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

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

606 '''

607 pass

608

609

610 def _heigHt(inst, height):

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

612 '''

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

614

615

616 class Lam(Radians):

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

618 '''

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

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

621 '''

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

623

624

625 class Lam_(Lam):

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

627 '''

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

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

630 '''

631 if name_arg:

632 name, arg = _xkwds_item2(name_arg)

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

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

635

636

637 class Lat(Degrees):

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

639 '''

640 _ddd_ = 2

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

642

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

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

645 '''

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

647

648

649 class Lat_(Degrees_):

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

651 '''

652 _ddd_ = 2

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

654

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

656 '''See L{Degrees_}.

657 '''

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

659

660

661 class Lon(Degrees):

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

663 '''

664 _ddd_ = 3

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

666

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

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

669 '''

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

671

672

673 class Lon_(Degrees_):

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

675 '''

676 _ddd_ = 3

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

678

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

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

681 '''

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

683

684

685 class Meter(Float):

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

687 '''

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

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

690 '''

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

692

693 def __repr__(self):

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

695

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

697

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

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

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

701 get the named C{toRepr} representation.

702 '''

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

704

705

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

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

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

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

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

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

712

713

714 class Meter_(Float_):

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

716 '''

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

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

719 '''

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

721

722

723 class Meter2(Float_):

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

725 '''

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

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

728 '''

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

730

731

732 class Meter3(Float_):

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

734 '''

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

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

737 '''

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

739

740

741 class Northing(Float):

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

743 '''

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

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

746

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

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

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

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

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

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

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

754 and B{C{arg}}.

755

756 @returns: A C{Northing} instance.

757

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

759 '''

760 if name_arg:

761 name, arg = _xkwds_item2(name_arg)

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

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

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

765 elif falsed and self < 0:

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

767 return self

768

769

770 class Number_(Int_):

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

772 '''

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

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

775 '''

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

777

778

779 class Phi(Radians):

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

781 '''

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

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

784 '''

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

786

787

788 class Phi_(Phi):

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

790 '''

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

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

793 '''

794 if name_arg:

795 name, arg = _xkwds_item2(name_arg)

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

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

798

799

800 class Precision_(Int_):

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

802 '''

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

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

805 '''

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

807

808

809 class Radius_(Float_):

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

811 '''

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

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

814 '''

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

816

817

818 class Scalar(Float):

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

820 '''

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

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

823 '''

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

825

826

827 class Scalar_(Float_):

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

829 '''

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

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

832 '''

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

834

835

836 class Zone(Int):

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

838 '''

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

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

841 '''

842 # usually low=_UTMUPS_ZONE_MIN, high=_UTMUPS_ZONE_MAX

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

844

845

846_Scalars = Float, Float_, Scalar, Scalar_

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

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

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

850_Radii = _Meters + (Radius, Radius_)

851

852

853 def _isDegrees(obj):

854 # Check for valid degrees types.

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

856

857

858 def _isHeight(obj):

859 # Check for valid heigth types.

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

861

862

863 def _isMeter(obj):

864 # Check for valid meter types.

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

866

867

868 def _isRadius(obj):

869 # Check for valid earth radius types.

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

871

872

873 def _isScalar(obj):

874 # Check for pure scalar types.

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

876

877

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

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

880 '''

881 if toDMS_kwds:

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

883

884 for x in xs:

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

886 s = None

887 x = x.toDegrees()

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

889 yield None if toDMS_kwds else s

890

891

892 def _toRadians(s, *xs):

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

894 '''

895 for x in xs:

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

897 s = None

898 x = x.toRadians()

899 yield x

900 yield s

901

902

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

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

905 '''

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

907 return _IsnotError(*r, **name_value_Error)

908

909

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

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

912 '''

913 if not issubclassof(Base, _NamedUnit):

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

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

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

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

918

919

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

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

922 '''

923 if not issubclassof(Base, _NamedUnit):

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

925 elif issubclassof(units, Base):

926 return units

927 elif isstr(units):

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

929 else:

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

931

932

933 def _std_repr(*Classes):

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

935 '''

936 for C in Classes:

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

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

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

940 C._std_repr = False

941

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

943 del _std_repr

944

945__all__ += _ALL_DOCS(_NamedUnit)

946

947# **) MIT License

948#

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

950#

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

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

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

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

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

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

957#

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

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

960#

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

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

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

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

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

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

967# OTHER DEALINGS IN THE SOFTWARE.