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, \

10 signOf

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

12 _umod_360, _0_0, _0_001, \

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

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

15 S_NUL, S_SEP, _toDMS, toDMS

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

17 UnitError, _xkwds, _xkwds_popitem

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

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

20 _feet_, _height_, _lam_, _lat_, \

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

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

23 _phi_, _precision_, _radians_, _radians2_, \

24 _radius_, _S_, _scalar_, _units_, \

25 _W_, _zone_, _std_ # PYCHOK used!

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

27 from pygeodesy.props import Property_RO

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

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

30 Radius, Str # PYCHOK shared .namedTuples

31 from math import degrees, radians

32

33__all__ = _ALL_LAZY.units

34__version__ = '24.01.10'

35

36_negative_falsed_ = 'negative, falsed'

37

38

39 class Float_(Float):

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

41 '''

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

43 '''New C{Float_} instance.

44

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

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

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

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

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

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

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

52 and B{C{arg}}.

53

54 @returns: A C{Float_} instance.

55

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

57 '''

58 if name_arg:

59 name, arg = _xkwds_popitem(name_arg)

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

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

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

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

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

65 else:

66 return self

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

68

69

70 class Int_(Int):

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

72 '''

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

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

75

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

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

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

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

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

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

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

83 and B{C{arg}}.

84

85 @returns: An L{Int_} instance.

86

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

88 '''

89 if name_arg:

90 name, arg = _xkwds_popitem(name_arg)

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

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

93 txt = Fmt.limit(below=low)

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

95 txt = Fmt.limit(above=high)

96 else:

97 return self

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

99

100

101 class Bool(Int, _NamedUnit):

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

103 '''

104 # _std_repr = True # set below

105 _bool_True_or_False = None

106

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

108 '''New C{Bool} instance.

109

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

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

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

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

114 L{UnitError}.

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

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

117

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

119

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

121 '''

122 if name_arg:

123 name, arg = _xkwds_popitem(name_arg)

124 try:

125 b = bool(arg)

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

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

128

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

130 self._bool_True_or_False = b

131 return self

132

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

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

135 return self._bool_True_or_False

136

137 __nonzero__ = __bool__ # PYCHOK Python 2-

138

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

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

141

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

143 representation (C{bool}).

144

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

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

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

148 get the named C{toRepr} representation.

149 '''

150 r = repr(self._bool_True_or_False)

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

152

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

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

155 '''

156 return str(self._bool_True_or_False)

157

158

159 class Band(Str):

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

161 '''

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

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

164 '''

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

166

167

168 class Degrees(Float):

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

170 '''

171 _ddd_ = 1 # default for .dms._toDMS

172 _sep_ = S_SEP

173 _suf_ = (S_NUL,) * 3

174

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

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

177

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

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

180 parsable by L{pygeodesy.parseDMS}).

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

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

183 L{UnitError}.

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

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

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

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

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

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

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

191

192 @returns: A C{Degrees} instance.

193

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

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

196 '''

197 if name_arg:

198 name, arg = _xkwds_popitem(name_arg)

199 try:

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

201 Error=Error, name=name)

202 if wrap:

203 w = wrap(d)

204 if w != d:

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

206 except Exception as x:

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

208 return d

209

210 def toDegrees(self):

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

212 '''

213 return self

214

215 def toRadians(self):

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

217 '''

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

219

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

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

222

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

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

225

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

227 L{pygeodesy.toDMS} for more documentation.

228 '''

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

230

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

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

233

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

235 '''

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

237 p = 8 if prec is None else prec

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

239 else:

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

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

242

243

244 class Degrees_(Degrees):

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

246 '''

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

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

249

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

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

252 L{pygeodesy.parseDMS}).

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

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

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

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

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

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

259 and B{C{arg}}.

260

261 @returns: A C{Degrees} instance.

262

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

264 '''

265 if name_arg:

266 name, arg = _xkwds_popitem(name_arg)

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

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

269 txt = Fmt.limit(below=low)

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

271 txt = Fmt.limit(above=high)

272 else:

273 return self

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

275

276

277 class Degrees2(Float):

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

279 '''

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

281 '''See L{Float}.

282 '''

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

284

285

286 class Radians(Float):

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

288 '''

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

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

291

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

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

294 by L{pygeodesy.parseRad}).

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

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

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

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

299 or C{None} for unclipped).

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

301 and B{C{arg}}.

302

303 @returns: A C{Radians} instance.

304

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

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

307 '''

308 if name_arg:

309 name, arg = _xkwds_popitem(name_arg)

310 try:

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

312 Error=Error, name=name)

313 except Exception as x:

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

315

316 def toDegrees(self):

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

318 '''

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

320

321 def toRadians(self):

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

323 '''

324 return self

325

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

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

328

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

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

331

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

333 L{pygeodesy.toDMS} for more documentation.

334 '''

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

336

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

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

339

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

341 '''

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

343

344

345 class Radians_(Radians):

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

347 '''

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

349 '''New C{Radians_} instance.

350

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

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

353 L{pygeodesy.parseRad}).

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

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

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

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

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

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

360 and B{C{arg}}.

361

362 @returns: A C{Radians_} instance.

363

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

365 '''

366 if name_arg:

367 name, arg = _xkwds_popitem(name_arg)

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

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

370 txt = Fmt.limit(below=low)

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

372 txt = Fmt.limit(above=high)

373 else:

374 return self

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

376

377

378 class Radians2(Float_):

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

380 '''

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

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

383 '''

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

385

386

387 class Bearing(Degrees):

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

389 '''

390 _ddd_ = 1

391 _suf_ = _N_ * 3 # always suffix N

392

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

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

395 '''

396 if name_arg:

397 name, arg = _xkwds_popitem(name_arg)

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

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

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

401

402

403 class Bearing_(Radians):

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

405 '''

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

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

408 '''

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

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

411

412

413 class Distance(Float):

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

415 '''

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

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

418 '''

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

420

421

422 class Distance_(Float_):

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

424 '''

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

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

427 '''

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

429

430

431 class Easting(Float):

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

433 '''

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

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

436

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

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

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

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

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

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

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

444 and B{C{arg}}.

445

446 @returns: An C{Easting} instance.

447

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

449 '''

450 if name_arg:

451 name, arg = _xkwds_popitem(name_arg)

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

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

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

455 elif falsed and self < 0:

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

457 return self

458

459

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

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

462 calendar year.

463 '''

464 _std_repr = False

465

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

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

468 '''

469 if name_arg:

470 name, arg = _xkwds_popitem(name_arg)

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

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

473

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

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

476

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

478 representation (C{bool}).

479

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

481 '''

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

483

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

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

486

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

488 '''

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

490

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

492

493

494 class Feet(Float):

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

496 '''

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

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

499 '''

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

501

502

503 class FIx(Float_):

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

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

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

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

508 len(points)]}.

509 '''

510 _fin = 0

511

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

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

514

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

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

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

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

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

520

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

522

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

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

525

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

527 '''

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

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

530 i = int(f)

531 r = f - float(i)

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

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

534 elif r > EPS1:

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

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

537 f._fin = n

538 return f

539

540 @Property_RO

541 def fin(self):

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

543 '''

544 return self._fin

545

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

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

548

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

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

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

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

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

554 I{averaged} lat- and longitudes.

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

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

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

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

559 or C{None}.

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

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

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

563

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

565

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

567 not subscriptable or not closed.

568

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

570 argument.

571

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

573 '''

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

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

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

577

578

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

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

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

582 if not 0 <= i < n:

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

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

585

586

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

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

589 '''

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

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

592 '''

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

594

595

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

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

598 '''

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

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

601 '''

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

603

604

605 class HeightX(Height):

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

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

608 '''

609 pass

610

611

612 def _heigHt(inst, height):

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

614 '''

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

616

617

618 class Lam(Radians):

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

620 '''

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

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

623 '''

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

625

626

627 class Lam_(Lam):

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

629 '''

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

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

632 '''

633 if name_arg:

634 name, arg = _xkwds_popitem(name_arg)

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

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

637

638

639 class Lat(Degrees):

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

641 '''

642 _ddd_ = 2

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

644

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

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

647 '''

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

649

650

651 class Lat_(Degrees_):

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

653 '''

654 _ddd_ = 2

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

656

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

658 '''See L{Degrees_}.

659 '''

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

661

662

663 class Lon(Degrees):

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

665 '''

666 _ddd_ = 3

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

668

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

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

671 '''

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

673

674

675 class Lon_(Degrees_):

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

677 '''

678 _ddd_ = 3

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

680

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

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

683 '''

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

685

686

687 class Meter(Float):

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

689 '''

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

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

692 '''

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

694

695 def __repr__(self):

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

697

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

699

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

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

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

703 get the named C{toRepr} representation.

704 '''

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

706

707

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

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

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

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

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

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

714

715

716 class Meter_(Float_):

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

718 '''

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

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

721 '''

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

723

724

725 class Meter2(Float_):

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

727 '''

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

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

730 '''

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

732

733

734 class Meter3(Float_):

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

736 '''

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

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

739 '''

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

741

742

743 class Northing(Float):

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

745 '''

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

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

748

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

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

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

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

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

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

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

756 and B{C{arg}}.

757

758 @returns: A C{Northing} instance.

759

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

761 '''

762 if name_arg:

763 name, arg = _xkwds_popitem(name_arg)

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

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

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

767 elif falsed and self < 0:

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

769 return self

770

771

772 class Number_(Int_):

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

774 '''

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

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

777 '''

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

779

780

781 class Phi(Radians):

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

783 '''

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

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

786 '''

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

788

789

790 class Phi_(Phi):

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

792 '''

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

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

795 '''

796 if name_arg:

797 name, arg = _xkwds_popitem(name_arg)

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

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

800

801

802 class Precision_(Int_):

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

804 '''

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

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

807 '''

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

809

810

811 class Radius_(Float_):

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

813 '''

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

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

816 '''

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

818

819

820 class Scalar(Float):

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

822 '''

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

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

825 '''

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

827

828

829 class Scalar_(Float_):

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

831 '''

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

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

834 '''

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

836

837

838 class Zone(Int):

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

840 '''

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

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

843 '''

844 # usually low=_UTMUPS_ZONE_MIN, high=_UTMUPS_ZONE_MAX

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

846

847

848_Scalars = Float, Float_, Scalar, Scalar_

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

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

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

852_Radii = _Meters + (Radius, Radius_)

853

854

855 def _isDegrees(obj):

856 # Check for valid degrees types.

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

858

859

860 def _isHeight(obj):

861 # Check for valid heigth types.

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

863

864

865 def _isMeter(obj):

866 # Check for valid meter types.

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

868

869

870 def _isRadius(obj):

871 # Check for valid earth radius types.

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

873

874

875 def _isScalar(obj):

876 # Check for pure scalar types.

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

878

879

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

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

882 '''

883 if toDMS_kwds:

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

885

886 for x in xs:

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

888 s = None

889 x = x.toDegrees()

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

891 yield None if toDMS_kwds else s

892

893

894 def _toRadians(s, *xs):

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

896 '''

897 for x in xs:

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

899 s = None

900 x = x.toRadians()

901 yield x

902 yield s

903

904

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

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

907 '''

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

909 return _IsnotError(*r, **name_value_Error)

910

911

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

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

914 '''

915 if not issubclassof(Base, _NamedUnit):

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

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

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

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

920

921

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

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

924 '''

925 if not issubclassof(Base, _NamedUnit):

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

927 elif issubclassof(units, Base):

928 return units

929 elif isstr(units):

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

931 else:

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

933

934

935 def _std_repr(*Classes):

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

937 '''

938 for C in Classes:

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

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

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

942 C._std_repr = False

943

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

945 del _std_repr

946

947__all__ += _ALL_DOCS(_NamedUnit)

948

949# **) MIT License

950#

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

952#

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

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

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

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

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

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

959#

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

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

962#

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

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

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

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

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

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

969# OTHER DEALINGS IN THE SOFTWARE.