Coverage for pygeodesy/units.py: 96%
301 statements
« prev ^ index » next coverage.py v7.6.1, created at 2025-04-09 11:05 -0400
« prev ^ index » next coverage.py v7.6.1, created at 2025-04-09 11:05 -0400
2# -*- coding: utf-8 -*-
4u'''Various named units, all sub-classes of C{Float}, C{Int} or C{Str} from
5basic C{float}, C{int} respectively C{str} to named units as L{Degrees},
6L{Feet}, L{Meter}, L{Radians}, etc.
7'''
9from pygeodesy.basics import isscalar, 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, _toDMS
13from pygeodesy.errors import _AssertionError, TRFError, UnitError, _xattr, _xcallable
14from pygeodesy.interns import NN, _azimuth_, _band_, _bearing_, _COMMASPACE_, \
15 _degrees_, _degrees2_, _distance_, _E_, _easting_, \
16 _epoch_, _EW_, _feet_, _height_, _lam_, _lat_, _LatLon_, \
17 _lon_, _meter_, _meter2_, _N_, _negative_, _northing_, \
18 _NS_, _NSEW_, _number_, _PERCENT_, _phi_, _precision_, \
19 _radians_, _radians2_, _radius_, _S_, _scalar_, \
20 _units_, _W_, _zone_, _std_ # PYCHOK used!
21from pygeodesy.lazily import _ALL_DOCS, _ALL_LAZY, _ALL_MODS as _MODS
22# from pygeodesy.named import _name__ # _MODS
23from pygeodesy.props import Property_RO
24# from pygeodesy.streprs import Fmt, fstr # from .unitsBase
25from pygeodesy.unitsBase import Float, Int, _NamedUnit, Radius, Str, Fmt, fstr
27from math import degrees, isnan, radians
29__all__ = _ALL_LAZY.units
30__version__ = '24.11.14'
33class Float_(Float):
34 '''Named C{float} with optional C{low} and C{high} limit.
35 '''
36 def __new__(cls, arg=None, name=NN, low=EPS, high=None, **Error_name_arg):
37 '''New, named C{Float_}, see L{Float}.
39 @arg cls: This class (C{Float_} or sub-class).
40 @kwarg arg: The value (any C{type} convertable to C{float}).
41 @kwarg name: Optional instance name (C{str}).
42 @kwarg low: Optional lower B{C{arg}} limit (C{float} or C{None}).
43 @kwarg high: Optional upper B{C{arg}} limit (C{float} or C{None}).
45 @returns: A named C{Float_}.
47 @raise Error: Invalid B{C{arg}} or B{C{arg}} below B{C{low}} or above B{C{high}}.
48 '''
49 self = Float.__new__(cls, arg=arg, name=name, **Error_name_arg)
50 t = _xlimits(self, low, high, g=True)
51 if t:
52 raise _NamedUnit._Error(cls, arg, name, txt=t, **Error_name_arg)
53 return self
56class Int_(Int):
57 '''Named C{int} with optional limits C{low} and C{high}.
58 '''
59 def __new__(cls, arg=None, name=NN, low=0, high=None, **Error_name_arg):
60 '''New, named C{int} instance with limits, see L{Int}.
62 @kwarg cls: This class (C{Int_} or sub-class).
63 @arg arg: The value (any C{type} convertable to C{int}).
64 @kwarg name: Optional instance name (C{str}).
65 @kwarg low: Optional lower B{C{arg}} limit (C{int} or C{None}).
66 @kwarg high: Optional upper B{C{arg}} limit (C{int} or C{None}).
68 @returns: A named L{Int_}.
70 @raise Error: Invalid B{C{arg}} or B{C{arg}} below B{C{low}} or above B{C{high}}.
71 '''
72 self = Int.__new__(cls, arg=arg, name=name, **Error_name_arg)
73 t = _xlimits(self, low, high)
74 if t:
75 raise _NamedUnit._Error(cls, arg, name, txt=t, **Error_name_arg)
76 return self
79class Bool(Int, _NamedUnit):
80 '''Named C{bool}, a sub-class of C{int} like Python's C{bool}.
81 '''
82 # _std_repr = True # set below
83 _bool_True_or_False = None
85 def __new__(cls, arg=None, name=NN, Error=UnitError, **name_arg):
86 '''New, named C{Bool}.
88 @kwarg cls: This class (C{Bool} or sub-class).
89 @kwarg arg: The value (any C{type} convertable to C{bool}).
90 @kwarg name: Optional instance name (C{str}).
91 @kwarg Error: Optional error to raise, overriding the default L{UnitError}.
92 @kwarg name_arg: Optional C{name=arg} keyword argument, inlieu of separate
93 B{C{arg}} and B{C{name}} ones.
95 @returns: A named L{Bool}, C{bool}-like.
97 @raise Error: Invalid B{C{arg}}.
98 '''
99 if name_arg:
100 name, arg = _NamedUnit._arg_name_arg2(arg, **name_arg)
101 try:
102 b = bool(arg)
103 except Exception as x:
104 raise _NamedUnit._Error(cls, arg, name, Error, cause=x)
106 self = Int.__new__(cls, b, name=name, Error=Error)
107 self._bool_True_or_False = b
108 return self
110 # <https://StackOverflow.com/questions/9787890/assign-class-boolean-value-in-python>
111 def __bool__(self): # PYCHOK Python 3+
112 return self._bool_True_or_False
114 __nonzero__ = __bool__ # PYCHOK Python 2-
116 def toRepr(self, std=False, **unused): # PYCHOK **unused
117 '''Return a representation of this C{Bool}.
119 @kwarg std: Use the standard C{repr} or the named representation (C{bool}).
121 @note: Use C{env} variable C{PYGEODESY_BOOL_STD_REPR=std} prior to C{import
122 pygeodesy} to get the standard C{repr} or set property C{std_repr=False}
123 to always get the named C{toRepr} representation.
124 '''
125 r = repr(self._bool_True_or_False)
126 return r if std else self._toRepr(r)
128 def toStr(self, **unused): # PYCHOK **unused
129 '''Return this C{Bool} as standard C{str}.
130 '''
131 return str(self._bool_True_or_False)
134class Band(Str):
135 '''Named C{str} representing a UTM/UPS band letter, unchecked.
136 '''
137 def __new__(cls, arg=None, name=_band_, **Error_name_arg):
138 '''New, named L{Band}, see L{Str}.
139 '''
140 return Str.__new__(cls, arg=arg, name=name, **Error_name_arg)
143class Degrees(Float):
144 '''Named C{float} representing a coordinate in C{degrees}, optionally clipped.
145 '''
146 _ddd_ = 1 # default for .dms._toDMS
147 _sep_ = S_SEP
148 _suf_ = (S_NUL,) * 3
150 def __new__(cls, arg=None, name=_degrees_, suffix=_NSEW_, clip=0, wrap=None, Error=UnitError, **name_arg):
151 '''New C{Degrees} instance, see L{Float}.
153 @arg cls: This class (C{Degrees} or sub-class).
154 @kwarg arg: The value (any C{type} convertable to C{float} or parsable by
155 function L{parseDMS<pygeodesy.dms.parseDMS>}).
156 @kwarg name: Optional instance name (C{str}).
157 @kwarg suffix: Optional, valid compass direction suffixes (C{NSEW}).
158 @kwarg clip: Optional B{C{arg}} range B{C{-clip..+clip}} (C{degrees} or C{0}
159 or C{None} for unclipped).
160 @kwarg wrap: Optionally adjust the B{C{arg}} value (L{wrap90<pygeodesy.wrap90>},
161 L{wrap180<pygeodesy.wrap180>} or L{wrap360<pygeodesy.wrap360>}).
162 @kwarg Error: Optional error to raise, overriding the default L{UnitError}.
163 @kwarg name_arg: Optional C{name=arg} keyword argument, inlieu of separate
164 B{C{arg}} and B{C{name}} ones.
166 @returns: A C{Degrees} instance.
168 @raise Error: Invalid B{C{arg}} or B{C{abs(arg)}} outside the B{C{clip}}
169 range and L{rangerrors<pygeodesy.rangerrors>} is C{True}.
170 '''
171 if name_arg:
172 name, arg = _NamedUnit._arg_name_arg2(arg, name, **name_arg)
173 try:
174 arg = parseDMS(arg, suffix=suffix, clip=clip)
175 if wrap:
176 _xcallable(wrap=wrap)
177 arg = wrap(arg)
178 self = Float.__new__(cls, arg=arg, name=name, Error=Error)
179 except Exception as x:
180 raise _NamedUnit._Error(cls, arg, name, Error, cause=x)
181 return self
183 def toDegrees(self):
184 '''Convert C{Degrees} to C{Degrees}.
185 '''
186 return self
188 def toRadians(self):
189 '''Convert C{Degrees} to C{Radians}.
190 '''
191 return Radians(radians(self), name=self.name)
193 def toRepr(self, std=False, **prec_fmt_ints): # PYCHOK prec=8, ...
194 '''Return a representation of this C{Degrees}.
196 @kwarg std: If C{True}, return the standard C{repr}, otherwise
197 the named representation (C{bool}).
199 @see: Methods L{Degrees.toStr}, L{Float.toRepr} and function
200 L{pygeodesy.toDMS} for futher C{prec_fmt_ints} details.
201 '''
202 return Float.toRepr(self, std=std, **prec_fmt_ints)
204 def toStr(self, prec=None, fmt=F__F_, ints=False, **s_D_M_S): # PYCHOK prec=8, ...
205 '''Return this C{Degrees} as standard C{str}.
207 @see: Function L{pygeodesy.toDMS} for futher details.
208 '''
209 if fmt.startswith(_PERCENT_): # use regular formatting
210 p = 8 if prec is None else prec
211 return fstr(self, prec=p, fmt=fmt, ints=ints, sep=self._sep_)
212 else:
213 s = self._suf_[signOf(self) + 1]
214 return _toDMS(self, fmt, prec, self._sep_, self._ddd_, s, s_D_M_S)
217class Degrees_(Degrees):
218 '''Named C{Degrees} representing a coordinate in C{degrees} with optional limits C{low} and C{high}.
219 '''
220 def __new__(cls, arg=None, name=_degrees_, low=None, high=None, **suffix_Error_name_arg):
221 '''New, named C{Degrees_}, see L{Degrees} and L{Float}.
223 @arg cls: This class (C{Degrees_} or sub-class).
224 @kwarg arg: The value (any C{type} convertable to C{float} or parsable by
225 function L{parseDMS<pygeodesy.dms.parseDMS>}).
226 @kwarg name: Optional instance name (C{str}).
227 @kwarg low: Optional lower B{C{arg}} limit (C{float} or C{None}).
228 @kwarg high: Optional upper B{C{arg}} limit (C{float} or C{None}).
230 @returns: A named C{Degrees}.
232 @raise Error: Invalid B{C{arg}} or B{C{arg}} below B{C{low}} or above B{C{high}}.
233 '''
234 self = Degrees.__new__(cls, arg=arg, name=name, clip=0, **suffix_Error_name_arg)
235 t = _xlimits(self, low, high)
236 if t:
237 raise _NamedUnit._Error(cls, arg, name, txt=t, **suffix_Error_name_arg)
238 return self
241class Degrees2(Float):
242 '''Named C{float} representing a distance in C{degrees squared}.
243 '''
244 def __new__(cls, arg=None, name=_degrees2_, **Error_name_arg):
245 '''See L{Float}.
246 '''
247 return Float.__new__(cls, arg=arg, name=name, **Error_name_arg)
250class Radians(Float):
251 '''Named C{float} representing a coordinate in C{radians}, optionally clipped.
252 '''
253 def __new__(cls, arg=None, name=_radians_, suffix=_NSEW_, clip=0, Error=UnitError, **name_arg):
254 '''New, named C{Radians}, see L{Float}.
256 @arg cls: This class (C{Radians} or sub-class).
257 @kwarg arg: The value (any C{type} convertable to C{float} or parsable by
258 L{pygeodesy.parseRad}).
259 @kwarg name: Optional instance name (C{str}).
260 @kwarg suffix: Optional, valid compass direction suffixes (C{NSEW}).
261 @kwarg clip: Optional B{C{arg}} range B{C{-clip..+clip}} (C{radians} or C{0}
262 or C{None} for unclipped).
263 @kwarg Error: Optional error to raise, overriding the default L{UnitError}.
264 @kwarg name_arg: Optional C{name=arg} keyword argument, inlieu of separate
265 B{C{arg}} and B{C{name}} ones.
267 @returns: A named C{Radians}.
269 @raise Error: Invalid B{C{arg}} or B{C{abs(arg)}} outside the B{C{clip}}
270 range and L{rangerrors<pygeodesy.rangerrors>} is C{True}.
271 '''
272 if name_arg:
273 name, arg = _NamedUnit._arg_name_arg2(arg, name, **name_arg)
274 try:
275 arg = parseRad(arg, suffix=suffix, clip=clip)
276 return Float.__new__(cls, arg, name=name, Error=Error)
277 except Exception as x:
278 raise _NamedUnit._Error(cls, arg, name, Error, cause=x)
280 def toDegrees(self):
281 '''Convert C{Radians} to C{Degrees}.
282 '''
283 return Degrees(degrees(self), name=self.name)
285 def toRadians(self):
286 '''Convert C{Radians} to C{Radians}.
287 '''
288 return self
290 def toRepr(self, std=False, **prec_fmt_ints): # PYCHOK prec=8, ...
291 '''Return a representation of this C{Radians}.
293 @kwarg std: If C{True}, return the standard C{repr}, otherwise
294 the named representation (C{bool}).
296 @see: Methods L{Radians.toStr}, L{Float.toRepr} and function
297 L{pygeodesy.toDMS} for more documentation.
298 '''
299 return Float.toRepr(self, std=std, **prec_fmt_ints)
301 def toStr(self, prec=8, fmt=F__F, ints=False): # PYCHOK prec=8, ...
302 '''Return this C{Radians} as standard C{str}.
304 @see: Function L{pygeodesy.fstr} for keyword argument details.
305 '''
306 return fstr(self, prec=prec, fmt=fmt, ints=ints)
309class Radians_(Radians):
310 '''Named C{float} representing a coordinate in C{radians} with optional limits C{low} and C{high}.
311 '''
312 def __new__(cls, arg=None, name=_radians_, low=_0_0, high=PI2, **suffix_Error_name_arg):
313 '''New, named C{Radians_}, see L{Radians}.
315 @arg cls: This class (C{Radians_} or sub-class).
316 @kwarg arg: The value (any C{type} convertable to C{float} or parsable by
317 function L{parseRad<pygeodesy.dms.parseRad>}).
318 @kwarg name: Optional name (C{str}).
319 @kwarg low: Optional lower B{C{arg}} limit (C{float} or C{None}).
320 @kwarg high: Optional upper B{C{arg}} limit (C{float} or C{None}).
322 @returns: A named C{Radians_}.
324 @raise Error: Invalid B{C{arg}} or B{C{arg}} below B{C{low}} or above B{C{high}}.
325 '''
326 self = Radians.__new__(cls, arg=arg, name=name, **suffix_Error_name_arg)
327 t = _xlimits(self, low, high)
328 if t:
329 raise _NamedUnit._Error(cls, arg, name, txt=t, **suffix_Error_name_arg)
330 return self
333class Radians2(Float_):
334 '''Named C{float} representing a distance in C{radians squared}.
335 '''
336 def __new__(cls, arg=None, name=_radians2_, **Error_name_arg):
337 '''New, named L{Radians2}, see L{Float_}.
338 '''
339 return Float_.__new__(cls, arg=arg, name=name, low=_0_0, **Error_name_arg)
342def _Degrees_new(cls, **arg_name_suffix_clip_Error_name_arg):
343 d = Degrees.__new__(cls, **arg_name_suffix_clip_Error_name_arg)
344 b = _umod_360(d) # 0 <= b < 360
345 return d if b == d else Degrees.__new__(cls, arg=b, name=d.name)
348class Azimuth(Degrees):
349 '''Named C{float} representing an azimuth in compass C{degrees} from (true) North.
350 '''
351 _ddd_ = 1
352 _suf_ = _W_, S_NUL, _E_ # no zero suffix
354 def __new__(cls, arg=None, name=_azimuth_, **clip_Error_name_arg):
355 '''New, named L{Azimuth} with optional suffix 'E' for clockwise or 'W' for
356 anti-clockwise, see L{Degrees}.
357 '''
358 return _Degrees_new(cls, arg=arg, name=name, suffix=_EW_, **clip_Error_name_arg)
361class Bearing(Degrees):
362 '''Named C{float} representing a bearing in compass C{degrees} from (true) North.
363 '''
364 _ddd_ = 1
365 _suf_ = _N_ * 3 # always suffix N
367 def __new__(cls, arg=None, name=_bearing_, **clip_Error_name_arg):
368 '''New, named L{Bearing}, see L{Degrees}.
369 '''
370 return _Degrees_new(cls, arg=arg, name=name, suffix=_N_, **clip_Error_name_arg)
373class Bearing_(Radians):
374 '''Named C{float} representing a bearing in C{radians} from compass C{degrees} from (true) North.
375 '''
376 def __new__(cls, arg=None, **name_clip_Error_name_arg):
377 '''New, named L{Bearing_}, see L{Bearing} and L{Radians}.
378 '''
379 d = Bearing.__new__(cls, arg=arg, **name_clip_Error_name_arg)
380 return Radians.__new__(cls, radians(d), name=d.name)
383class Distance(Float):
384 '''Named C{float} representing a distance, conventionally in C{meter}.
385 '''
386 def __new__(cls, arg=None, name=_distance_, **Error_name_arg):
387 '''New, named L{Distance}, see L{Float}.
388 '''
389 return Float.__new__(cls, arg=arg, name=name, **Error_name_arg)
392class Distance_(Float_):
393 '''Named C{float} with optional C{low} and C{high} limits representing a distance, conventionally in C{meter}.
394 '''
395 def __new__(cls, arg=None, name=_distance_, **low_high_Error_name_arg):
396 '''New L{Distance_} instance, see L{Float}.
397 '''
398 return Float_.__new__(cls, arg=arg, name=name, **low_high_Error_name_arg)
401class _EasNorBase(Float):
402 '''(INTERNAL) L{Easting} and L{Northing} base class.
403 '''
404 def __new__(cls, arg, name, falsed, high, **Error_name_arg):
405 self = Float.__new__(cls, arg=arg, name=name, **Error_name_arg)
406 low = self < 0
407 if (high is not None) and (low or self > high): # like Veness
408 t = _negative_ if low else Fmt.limit(above=high)
409 elif low and falsed:
410 t = _COMMASPACE_(_negative_, 'falsed')
411 else:
412 return self
413 raise _NamedUnit._Error(cls, arg, name, txt=t, **Error_name_arg)
416class Easting(_EasNorBase):
417 '''Named C{float} representing an easting, conventionally in C{meter}.
418 '''
419 def __new__(cls, arg=None, name=_easting_, falsed=False, high=None, **Error_name_arg):
420 '''New, named C{Easting} or C{Easting of Point}, see L{Float}.
422 @arg cls: This class (C{Easting} or sub-class).
423 @kwarg arg: The value (any C{type} convertable to C{float}).
424 @kwarg name: Optional name (C{str}).
425 @kwarg falsed: If C{True}, the B{C{arg}} value is falsed (C{bool}).
426 @kwarg high: Optional upper B{C{arg}} limit (C{scalar} or C{None}).
428 @returns: A named C{Easting}.
430 @raise Error: Invalid B{C{arg}}, above B{C{high}} or negative, falsed B{C{arg}}.
431 '''
432 return _EasNorBase.__new__(cls, arg, name, falsed, high, **Error_name_arg)
435class Epoch(Float_): # in .ellipsoidalBase, .trf
436 '''Named C{epoch} with optional C{low} and C{high} limits representing a fractional
437 calendar year.
438 '''
439 _std_repr = False
441 def __new__(cls, arg=None, name=_epoch_, low=1900, high=9999, Error=TRFError, **name_arg):
442 '''New, named L{Epoch}, see L{Float_}.
443 '''
444 if name_arg:
445 name, arg = _NamedUnit._arg_name_arg2(arg, name, **name_arg)
446 return arg if isinstance(arg, Epoch) else Float_.__new__(cls,
447 arg=arg, name=name, Error=Error, low=low, high=high)
449 def toRepr(self, prec=3, std=False, **unused): # PYCHOK fmt=Fmt.F, ints=True
450 '''Return a representation of this C{Epoch}.
452 @kwarg std: Use the standard C{repr} or the named
453 representation (C{bool}).
455 @see: Method L{Float.toRepr} for more documentation.
456 '''
457 return Float_.toRepr(self, prec=prec, std=std) # fmt=Fmt.F, ints=True
459 def toStr(self, prec=3, **unused): # PYCHOK fmt=Fmt.F, nts=True
460 '''Format this C{Epoch} as C{str}.
462 @see: Function L{pygeodesy.fstr} for more documentation.
463 '''
464 return fstr(self, prec=prec, fmt=Fmt.F, ints=True)
466 __str__ = toStr # PYCHOK default '%.3F', with trailing zeros and decimal point
469class Feet(Float):
470 '''Named C{float} representing a distance or length in C{feet}.
471 '''
472 def __new__(cls, arg=None, name=_feet_, **Error_name_arg):
473 '''New, named L{Feet}, see L{Float}.
474 '''
475 return Float.__new__(cls, arg=arg, name=name, **Error_name_arg)
478class FIx(Float_):
479 '''A named I{Fractional Index}, an C{int} or C{float} index into a C{list}
480 or C{tuple} of C{points}, typically. A C{float} I{Fractional Index}
481 C{fi} represents a location on the edge between C{points[int(fi)]} and
482 C{points[(int(fi) + 1) % len(points)]}.
483 '''
484 _fin = 0
486 def __new__(cls, fi, fin=None, Error=UnitError, **name):
487 '''New, named I{Fractional Index} in a C{list} or C{tuple} of points.
489 @arg fi: The fractional index (C{float} or C{int}).
490 @kwarg fin: Optional C{len}, the number of C{points}, the index
491 C{[n]} wrapped to C{[0]} (C{int} or C{None}).
492 @kwarg Error: Optional error to raise.
493 @kwarg name: Optional C{B{name}=NN} (C{str}).
495 @return: A named B{C{fi}} (L{FIx}).
497 @note: The returned B{C{fi}} may exceed the B{C{len}}, number of
498 original C{points} in certain open/closed cases.
500 @see: Method L{fractional} or function L{pygeodesy.fractional}.
501 '''
502 _ = _MODS.named._name__(name) if name else NN # check error
503 n = Int_(fin=fin, low=0) if fin else None
504 f = Float_.__new__(cls, fi, low=_0_0, high=n, Error=Error, **name)
505 i = int(f)
506 r = f - float(i)
507 if r < EPS: # see .points._fractional
508 f = Float_.__new__(cls, i, low=_0_0, Error=Error, **name)
509 elif r > EPS1:
510 f = Float_.__new__(cls, i + 1, high=n, Error=Error, **name)
511 if n: # non-zero and non-None
512 f._fin = n
513 return f
515 @Property_RO
516 def fin(self):
517 '''Get the given C{len}, the index C{[n]} wrapped to C{[0]} (C{int}).
518 '''
519 return self._fin
521 def fractional(self, points, wrap=None, **LatLon_or_Vector_and_kwds):
522 '''Return the point at this I{Fractional Index}.
524 @arg points: The points (C{LatLon}[], L{Numpy2LatLon}[], L{Tuple2LatLon}[] or
525 C{other}[]).
526 @kwarg wrap: If C{True}, wrap or I{normalize} and unroll the B{C{points}}
527 (C{bool}) or C{None} for backward compatible L{LatLon2Tuple} or
528 B{C{LatLon}} with I{averaged} lat- and longitudes.
529 @kwarg LatLon_or_Vector_and_kwds: Optional C{B{LatLon}=None} I{or} C{B{Vector}=None}
530 to return the I{intermediate}, I{fractional} point and optionally,
531 additional B{C{LatLon}} I{or} B{C{Vector}} keyword arguments, see
532 function L{fractional<pygeodesy.points.fractional>}.
534 @return: See function L{fractional<pygeodesy.points.fractional>}.
536 @raise IndexError: In fractional index invalid or B{C{points}} not
537 subscriptable or not closed.
539 @raise TypeError: Invalid B{C{LatLon}}, B{C{Vector}} or B{C{kwds}} argument.
541 @see: Function L{pygeodesy.fractional}.
542 '''
543 # fi = 0 if self == self.fin else self
544 return _MODS.points.fractional(points, self, wrap=wrap, **LatLon_or_Vector_and_kwds)
547def _fi_j2(f, n): # PYCHOK in .ellipsoidalBaseDI, .vector3d
548 # Get 2-tuple (C{fi}, C{j})
549 i = int(f) # like L{FIx}
550 if not 0 <= i < n:
551 raise _AssertionError(i=i, n=n, f=f, r=f - float(i))
552 return FIx(fi=f, fin=n), (i + 1) % n
555class Height(Float): # here to avoid circular import
556 '''Named C{float} representing a height, conventionally in C{meter}.
557 '''
558 def __new__(cls, arg=None, name=_height_, **Error_name_arg):
559 '''New, named L{Height}, see L{Float}.
560 '''
561 return Float.__new__(cls, arg=arg, name=name, **Error_name_arg)
564class Height_(Float_): # here to avoid circular import
565 '''Named C{float} with optional C{low} and C{high} limits representing a height, conventionally in C{meter}.
566 '''
567 def __new__(cls, arg=None, name=_height_, **low_high_Error_name_arg):
568 '''New, named L{Height}, see L{Float}.
569 '''
570 return Float_.__new__(cls, arg=arg, name=name, **low_high_Error_name_arg)
573class HeightX(Height):
574 '''Like L{Height}, used to distinguish the interpolated height
575 from an original L{Height} at a clip intersection.
576 '''
577 pass
580def _heigHt(inst, height):
581 '''(INTERNAL) Override the C{inst}ance' height.
582 '''
583 return inst.height if height is None else Height(height)
586class Lam(Radians):
587 '''Named C{float} representing a longitude in C{radians}.
588 '''
589 def __new__(cls, arg=None, name=_lam_, clip=PI, **Error_name_arg):
590 '''New, named L{Lam}, see L{Radians}.
591 '''
592 return Radians.__new__(cls, arg=arg, name=name, suffix=_EW_, clip=clip, **Error_name_arg)
595class Lamd(Lam):
596 '''Named C{float} representing a longitude in C{radians} converted from C{degrees}.
597 '''
598 def __new__(cls, arg=None, name=_lon_, clip=180, **Error_name_arg):
599 '''New, named L{Lamd}, see L{Lam} and L{Radians}.
600 '''
601 d = Degrees(arg=arg, name=name, clip=clip, **Error_name_arg)
602 return Lam.__new__(cls, radians(d), clip=radians(clip), name=d.name)
605class Lat(Degrees):
606 '''Named C{float} representing a latitude in C{degrees}.
607 '''
608 _ddd_ = 2
609 _suf_ = _S_, S_NUL, _N_ # no zero suffix
611 def __new__(cls, arg=None, name=_lat_, clip=90, **Error_name_arg):
612 '''New, named L{Lat}, see L{Degrees}.
613 '''
614 return Degrees.__new__(cls, arg=arg, name=name, suffix=_NS_, clip=clip, **Error_name_arg)
617class Lat_(Degrees_):
618 '''Named C{float} representing a latitude in C{degrees} within limits C{low} and C{high}.
619 '''
620 _ddd_ = 2
621 _suf_ = _S_, S_NUL, _N_ # no zero suffix
623 def __new__(cls, arg=None, name=_lat_, low=-90, high=90, **Error_name_arg):
624 '''See L{Degrees_}.
625 '''
626 return Degrees_.__new__(cls, arg=arg, name=name, suffix=_NS_, low=low, high=high, **Error_name_arg)
629class Lon(Degrees):
630 '''Named C{float} representing a longitude in C{degrees}.
631 '''
632 _ddd_ = 3
633 _suf_ = _W_, S_NUL, _E_ # no zero suffix
635 def __new__(cls, arg=None, name=_lon_, clip=180, **Error_name_arg):
636 '''New, named L{Lon}, see L{Degrees}.
637 '''
638 return Degrees.__new__(cls, arg=arg, name=name, suffix=_EW_, clip=clip, **Error_name_arg)
641class Lon_(Degrees_):
642 '''Named C{float} representing a longitude in C{degrees} within limits C{low} and C{high}.
643 '''
644 _ddd_ = 3
645 _suf_ = _W_, S_NUL, _E_ # no zero suffix
647 def __new__(cls, arg=None, name=_lon_, low=-180, high=180, **Error_name_arg):
648 '''New, named L{Lon_}, see L{Lon} and L{Degrees_}.
649 '''
650 return Degrees_.__new__(cls, arg=arg, name=name, suffix=_EW_, low=low, high=high, **Error_name_arg)
653class Meter(Float):
654 '''Named C{float} representing a distance or length in C{meter}.
655 '''
656 def __new__(cls, arg=None, name=_meter_, **Error_name_arg):
657 '''New, named L{Meter}, see L{Float}.
658 '''
659 return Float.__new__(cls, arg=arg, name=name, **Error_name_arg)
661 def __repr__(self):
662 '''Return a representation of this C{Meter}.
664 @see: Method C{Str.toRepr} and property C{Str.std_repr}.
666 @note: Use C{env} variable C{PYGEODESY_METER_STD_REPR=std} prior to C{import
667 pygeodesy} to get the standard C{repr} or set property C{std_repr=False}
668 to always get the named C{toRepr} representation.
669 '''
670 return self.toRepr(std=self._std_repr)
673# _1Å = Meter( _Å= 1e-10) # PyCHOK 1 Ångstrōm
674_1um = Meter( _1um= 1.e-6) # PYCHOK 1 micrometer in .mgrs
675_10um = Meter( _10um= 1.e-5) # PYCHOK 10 micrometer in .osgr
676_1mm = Meter( _1mm=_0_001) # PYCHOK 1 millimeter in .ellipsoidal...
677_100km = Meter( _100km= 1.e+5) # PYCHOK 100 kilometer in .formy, .mgrs, .osgr, .sphericalBase
678_2000km = Meter(_2000km= 2.e+6) # PYCHOK 2,000 kilometer in .mgrs
681class Meter_(Float_):
682 '''Named C{float} representing a distance or length in C{meter}.
683 '''
684 def __new__(cls, arg=None, name=_meter_, low=_0_0, **high_Error_name_arg):
685 '''New, named L{Meter_}, see L{Meter} and L{Float_}.
686 '''
687 return Float_.__new__(cls, arg=arg, name=name, low=low, **high_Error_name_arg)
690class Meter2(Float_):
691 '''Named C{float} representing an area in C{meter squared}.
692 '''
693 def __new__(cls, arg=None, name=_meter2_, **Error_name_arg):
694 '''New, named L{Meter2}, see L{Float_}.
695 '''
696 return Float_.__new__(cls, arg=arg, name=name, low=_0_0, **Error_name_arg)
699class Meter3(Float_):
700 '''Named C{float} representing a volume in C{meter cubed}.
701 '''
702 def __new__(cls, arg=None, name='meter3', **Error_name_arg):
703 '''New, named L{Meter3}, see L{Float_}.
704 '''
705 return Float_.__new__(cls, arg=arg, name=name, low=_0_0, **Error_name_arg)
708class Northing(_EasNorBase):
709 '''Named C{float} representing a northing, conventionally in C{meter}.
710 '''
711 def __new__(cls, arg=None, name=_northing_, falsed=False, high=None, **Error_name_arg):
712 '''New, named C{Northing} or C{Northing of point}, see L{Float}.
714 @arg cls: This class (C{Northing} or sub-class).
715 @kwarg arg: The value (any C{type} convertable to C{float}).
716 @kwarg name: Optional name (C{str}).
717 @kwarg falsed: If C{True}, the B{C{arg}} value is falsed (C{bool}).
718 @kwarg high: Optional upper B{C{arg}} limit (C{scalar} or C{None}).
720 @returns: A named C{Northing}.
722 @raise Error: Invalid B{C{arg}}, above B{C{high}} or negative, falsed B{C{arg}}.
723 '''
724 return _EasNorBase.__new__(cls, arg, name, falsed, high, **Error_name_arg)
727class Number_(Int_):
728 '''Named C{int} representing a non-negative number.
729 '''
730 def __new__(cls, arg=None, name=_number_, **low_high_Error_name_arg):
731 '''New, named L{Number_}, see L{Int_}.
732 '''
733 return Int_.__new__(cls, arg=arg, name=name, **low_high_Error_name_arg)
736class Phi(Radians):
737 '''Named C{float} representing a latitude in C{radians}.
738 '''
739 def __new__(cls, arg=None, name=_phi_, clip=PI_2, **Error_name_arg):
740 '''New, named L{Phi}, see L{Radians}.
741 '''
742 return Radians.__new__(cls, arg=arg, name=name, suffix=_NS_, clip=clip, **Error_name_arg)
745class Phid(Phi):
746 '''Named C{float} representing a latitude in C{radians} converted from C{degrees}.
747 '''
748 def __new__(cls, arg=None, name=_lat_, clip=90, **Error_name_arg):
749 '''New, named L{Phid}, see L{Phi} and L{Radians}.
750 '''
751 d = Degrees(arg=arg, name=name, clip=clip, **Error_name_arg)
752 return Phi.__new__(cls, arg=radians(d), clip=radians(clip), name=d.name)
755class Precision_(Int_):
756 '''Named C{int} with optional C{low} and C{high} limits representing a precision.
757 '''
758 def __new__(cls, arg=None, name=_precision_, **low_high_Error_name_arg):
759 '''New, named L{Precision_}, see L{Int_}.
760 '''
761 return Int_.__new__(cls, arg=arg, name=name, **low_high_Error_name_arg)
764class Radius_(Float_):
765 '''Named C{float} with optional C{low} and C{high} limits representing a radius, conventionally in C{meter}.
766 '''
767 def __new__(cls, arg=None, name=_radius_, **low_high_Error_name_arg):
768 '''New, named L{Radius_}, see L{Radius} and L{Float}.
769 '''
770 return Float_.__new__(cls, arg=arg, name=name, **low_high_Error_name_arg)
773class Scalar(Float):
774 '''Named C{float} representing a factor, fraction, scale, etc.
775 '''
776 def __new__(cls, arg=None, name=_scalar_, **Error_name_arg):
777 '''New, named L{Scalar}, see L{Float}.
778 '''
779 return Float.__new__(cls, arg=arg, name=name, **Error_name_arg)
782class Scalar_(Float_):
783 '''Named C{float} with optional C{low} and C{high} limits representing a factor, fraction, scale, etc.
784 '''
785 def __new__(cls, arg=None, name=_scalar_, low=_0_0, **high_Error_name_arg):
786 '''New, named L{Scalar_}, see L{Scalar} and L{Float_}.
787 '''
788 return Float_.__new__(cls, arg=arg, name=name, low=low, **high_Error_name_arg)
791class Zone(Int):
792 '''Named C{int} representing a UTM/UPS zone number.
793 '''
794 def __new__(cls, arg=None, name=_zone_, **Error_name_arg):
795 '''New, named L{Zone}, see L{Int}
796 '''
797 # usually low=_UTMUPS_ZONE_MIN, high=_UTMUPS_ZONE_MAX
798 return Int_.__new__(cls, arg=arg, name=name, **Error_name_arg)
801_Degrees = (Azimuth, Bearing, Bearing_, Degrees, Degrees_)
802_Meters = (Distance, Distance_, Meter, Meter_)
803_Radians = (Radians, Radians_) # PYCHOK unused
804_Radii = _Meters + (Radius, Radius_)
805_ScalarU = Float, Float_, Scalar, Scalar_
808def _isDegrees(obj, iscalar=True):
809 # Check for valid degrees types.
810 return isinstance(obj, _Degrees) or (iscalar and _isScalar(obj))
813def _isHeight(obj, iscalar=True):
814 # Check for valid height types.
815 return isinstance(obj, _Meters) or (iscalar and _isScalar(obj))
818def _isMeter(obj, iscalar=True):
819 # Check for valid meter types.
820 return isinstance(obj, _Meters) or (iscalar and _isScalar(obj))
823def _isRadius(obj, iscalar=True):
824 # Check for valid earth radius types.
825 return isinstance(obj, _Radii) or (iscalar and _isScalar(obj))
828def _isScalar(obj, iscalar=True):
829 # Check for pure scalar types.
830 return isinstance(obj, _ScalarU) or (iscalar and isscalar(obj) and not isinstance(obj, _NamedUnit))
833def _toUnit(Unit, arg, name=NN, **Error):
834 '''(INTERNAL) Wrap C{arg} in a C{name}d C{Unit}.
835 '''
836 if not (issubclassof(Unit, _NamedUnit) and isinstance(arg, Unit) and
837 _xattr(arg, name=NN) == name):
838 arg = Unit(arg, name=name, **Error)
839 return arg
842def _xlimits(arg, low, high, g=False):
843 '''(INTERNAL) Check C{low <= arg <= high}.
844 '''
845 if (low is not None) and (arg < low or isnan(arg)):
846 if g:
847 low = Fmt.g(low, prec=6, ints=isinstance(arg, Epoch))
848 t = Fmt.limit(below=low)
849 elif (high is not None) and (arg > high or isnan(arg)):
850 if g:
851 high = Fmt.g(high, prec=6, ints=isinstance(arg, Epoch))
852 t = Fmt.limit(above=high)
853 else:
854 t = NN
855 return t
858def _std_repr(*Classes):
859 '''(INTERNAL) Use standard C{repr} or named C{toRepr}.
860 '''
861 from pygeodesy.internals import _getenv
862 for C in Classes:
863 if hasattr(C, _std_repr.__name__): # PYCHOK del _std_repr
864 env = 'PYGEODESY_%s_STD_REPR' % (C.__name__.upper(),)
865 if _getenv(env, _std_).lower() != _std_:
866 C._std_repr = False
868_std_repr(Azimuth, Bearing, Bool, Degrees, Epoch, Float, Int, Meter, Radians, Str) # PYCHOK expected
869del _std_repr
871__all__ += _ALL_DOCS(_NamedUnit)
873# **) MIT License
874#
875# Copyright (C) 2016-2025 -- mrJean1 at Gmail -- All Rights Reserved.
876#
877# Permission is hereby granted, free of charge, to any person obtaining a
878# copy of this software and associated documentation files (the "Software"),
879# to deal in the Software without restriction, including without limitation
880# the rights to use, copy, modify, merge, publish, distribute, sublicense,
881# and/or sell copies of the Software, and to permit persons to whom the
882# Software is furnished to do so, subject to the following conditions:
883#
884# The above copyright notice and this permission notice shall be included
885# in all copies or substantial portions of the Software.
886#
887# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
888# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
889# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
890# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
891# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
892# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
893# OTHER DEALINGS IN THE SOFTWARE.