Coverage for pygeodesy/units.py: 95%
330 statements
« prev ^ index » next coverage.py v7.2.2, created at 2024-05-25 12:04 -0400
« prev ^ index » next coverage.py v7.2.2, created at 2024-05-25 12:04 -0400
2# -*- coding: utf-8 -*-
4u'''Various units, all sub-classes of C{Float}, C{Int} and
5C{Str} from basic C{float}, C{int} respectively C{str} to
6named units as L{Degrees}, L{Feet}, L{Meter}, L{Radians}, etc.
7'''
9from pygeodesy.basics import isinstanceof, isscalar, isstr, issubclassof, signOf
10from pygeodesy.constants import EPS, EPS1, PI, PI2, PI_2, _umod_360, _0_0, \
11 _0_001, _0_5, INT0 # PYCHOK for .mgrs, .namedTuples
12from pygeodesy.dms import F__F, F__F_, S_NUL, S_SEP, parseDMS, parseRad, \
13 _toDMS, toDMS
14from pygeodesy.errors import _AssertionError, _IsnotError, TRFError, UnitError, \
15 _xkwds
16from 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!
23from pygeodesy.lazily import _ALL_DOCS, _ALL_LAZY, _ALL_MODS as _MODS, _getenv
24from pygeodesy.props import Property_RO
25# from pygeodesy.streprs import Fmt, fstr # from .unitsBase
26from pygeodesy.unitsBase import _Error, Float, Fmt, fstr, Int, _arg_name_arg2, \
27 _NamedUnit, Radius, Str # PYCHOK shared .namedTuples
29from math import degrees, radians
31__all__ = _ALL_LAZY.units
32__version__ = '24.05.20'
34_negative_falsed_ = 'negative, falsed'
37class 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.
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 separate
50 B{C{arg}} and B{C{name}} ones.
52 @returns: A C{Float_} instance.
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 = _arg_name_arg2(arg, **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)
68class 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.
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 separate
81 B{C{arg}} and B{C{name}} ones.
83 @returns: An L{Int_} instance.
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 = _arg_name_arg2(arg, **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)
99class 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
105 def __new__(cls, arg=None, name=NN, Error=UnitError, **name_arg):
106 '''New C{Bool} instance.
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}}.
116 @returns: A L{Bool}, a C{bool}-like instance.
118 @raise Error: Invalid B{C{arg}}.
119 '''
120 if name_arg:
121 name, arg = _arg_name_arg2(arg, **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)
127 self = Int.__new__(cls, b, name=name, Error=Error)
128 self._bool_True_or_False = b
129 return self
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
135 __nonzero__ = __bool__ # PYCHOK Python 2-
137 def toRepr(self, std=False, **unused): # PYCHOK **unused
138 '''Return a representation of this C{Bool}.
140 @kwarg std: Use the standard C{repr} or the named
141 representation (C{bool}).
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)
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)
157class 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)
166class 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
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}.
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}}.
190 @returns: A C{Degrees} instance.
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 = _arg_name_arg2(arg, **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
208 def toDegrees(self):
209 '''Convert C{Degrees} to C{Degrees}.
210 '''
211 return self
213 def toRadians(self):
214 '''Convert C{Degrees} to C{Radians}.
215 '''
216 return Radians(radians(self), name=self.name)
218 def toRepr(self, std=False, **prec_fmt_ints): # PYCHOK prec=8, ...
219 '''Return a representation of this C{Degrees}.
221 @kwarg std: If C{True} return the standard C{repr},
222 otherwise the named representation (C{bool}).
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)
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}.
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)
242class 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}..
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 separate
257 B{C{arg}} and B{C{name}} ones.
259 @returns: A C{Degrees} instance.
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 = _arg_name_arg2(arg, **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)
275class 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)
284class 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}.
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 separate
299 B{C{arg}} and B{C{name}} ones.
301 @returns: A C{Radians} instance.
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 = _arg_name_arg2(arg, **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)
314 def toDegrees(self):
315 '''Convert C{Radians} to C{Degrees}.
316 '''
317 return Degrees(degrees(self), name=self.name)
319 def toRadians(self):
320 '''Convert C{Radians} to C{Radians}.
321 '''
322 return self
324 def toRepr(self, std=False, **prec_fmt_ints): # PYCHOK prec=8, ...
325 '''Return a representation of this C{Radians}.
327 @kwarg std: If C{True} return the standard C{repr}, otherwise
328 the named representation (C{bool}).
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)
335 def toStr(self, prec=8, fmt=F__F, ints=False): # PYCHOK prec=8, ...
336 '''Return this C{Radians} as standard C{str}.
338 @see: Function L{pygeodesy.fstr} for keyword argument details.
339 '''
340 return fstr(self, prec=prec, fmt=fmt, ints=ints)
343class 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.
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 separate
358 B{C{arg}} and B{C{name}} ones.
360 @returns: A C{Radians_} instance.
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 = _arg_name_arg2(arg, **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)
376class 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)
385class 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
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 = _arg_name_arg2(arg, **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)
401class 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)
411class 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)
420class 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)
429class 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.
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 separate
442 B{C{arg}} and B{C{name}} ones.
444 @returns: An C{Easting} instance.
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 = _arg_name_arg2(arg, **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
458class 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
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 = _arg_name_arg2(arg, **name_arg)
469 return arg if isinstance(arg, Epoch) else Float_.__new__(cls,
470 arg=arg, name=name, Error=Error, low=low, high=high)
472 def toRepr(self, prec=3, std=False, **unused): # PYCHOK fmt=Fmt.F, ints=True
473 '''Return a representation of this C{Epoch}.
475 @kwarg std: Use the standard C{repr} or the named
476 representation (C{bool}).
478 @see: Method L{Float.toRepr} for more documentation.
479 '''
480 return Float_.toRepr(self, prec=prec, std=std) # fmt=Fmt.F, ints=True
482 def toStr(self, prec=3, **unused): # PYCHOK fmt=Fmt.F, ints=True
483 '''Format this C{Epoch} as C{str}.
485 @see: Function L{pygeodesy.fstr} for more documentation.
486 '''
487 return fstr(self, prec=prec, fmt=Fmt.F, ints=True)
489 __str__ = toStr # PYCHOK default '%.3F', with trailing zeros and decimal point
492class 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)
501class 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
510 def __new__(cls, fi, fin=None, **name_Error):
511 '''New I{Fractional Index} in a C{list} or C{tuple} of points.
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 C{B{name}=NN} (C{str}) and keyword
517 argument C{B{Error}=UnitError}.
519 @return: The B{C{fi}} (named L{FIx}).
521 @note: The returned B{C{fi}} may exceed the B{C{flen}} of
522 the original C{points} in certain open/closed cases.
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
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
544 def fractional(self, points, wrap=None, LatLon=None, Vector=None, **kwds):
545 '''Return the point at this I{Fractional Index}.
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}.
562 @return: See function L{pygeodesy.fractional}.
564 @raise IndexError: This fractional index invalid or B{C{points}}
565 not subscriptable or not closed.
567 @raise TypeError: Invalid B{C{LatLon}}, B{C{Vector}} or B{C{kwds}}
568 argument.
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)
577def _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
585class 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)
594class 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)
603class 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
610def _heigHt(inst, height):
611 '''(INTERNAL) Override the C{inst}ance' height.
612 '''
613 return inst.height if height is None else Height(height)
616class 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)
625class 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 = _arg_name_arg2(arg, **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)
637class 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
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)
649class 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
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)
661class 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
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)
673class 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
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)
685class 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)
693 def __repr__(self):
694 '''Return a representation of this C{Meter}.
696 @see: Method C{Str.toRepr} and property C{Str.std_repr}.
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)
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
714class 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)
723class 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)
732class 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)
741class 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.
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 separate
754 B{C{arg}} and B{C{name}} ones.
756 @returns: A C{Northing} instance.
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 = _arg_name_arg2(arg, **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
770class 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)
779class 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)
788class 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 = _arg_name_arg2(arg, **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)
800class 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)
809class 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)
818class 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)
827class 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)
836class 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)
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_)
853def _isDegrees(obj):
854 # Check for valid degrees types.
855 return isinstance(obj, _Degrees) or _isScalar(obj)
858def _isHeight(obj):
859 # Check for valid heigth types.
860 return isinstance(obj, _Meters) or _isScalar(obj)
863def _isMeter(obj):
864 # Check for valid meter types.
865 return isinstance(obj, _Meters) or _isScalar(obj)
868def _isRadius(obj):
869 # Check for valid earth radius types.
870 return isinstance(obj, _Radii) or _isScalar(obj)
873def _isScalar(obj):
874 # Check for pure scalar types.
875 return isscalar(obj) and not isinstance(obj, _NamedUnit)
878def _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)
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
892def _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
903def _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)
910def _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
920def _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)
933def _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
942_std_repr(Bearing, Bool, Degrees, Float, Int, Meter, Radians, Str) # PYCHOK expected
943del _std_repr
945__all__ += _ALL_DOCS(_NamedUnit)
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.