Coverage for pygeodesy / units.py: 96%
305 statements
« prev ^ index » next coverage.py v7.14.0, created at 2026-07-07 13:30 -0400
« prev ^ index » next coverage.py v7.14.0, created at 2026-07-07 13:30 -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'''
9# from pygeodesy.angles import isAng # _MODS
10from pygeodesy.basics import isscalar, issubclassof, signOf, typename
11from pygeodesy.constants import EPS, EPS1, PI, PI2, PI_2, _0_0, _0_001, \
12 _isNAN, _umod_360
13from pygeodesy.dms import F__F, F__F_, S_NUL, S_SEP, parseDMS, parseRad, _toDMS
14from pygeodesy.errors import _AssertionError, TRFError, UnitError, _xattr, _xcallable
15# from pygeodesy.internals import typename # from .basics
16from pygeodesy.interns import NN, _azimuth_, _band_, _bearing_, _COMMASPACE_, \
17 _degrees_, _degrees2_, _distance_, _E_, _easting_, \
18 _epoch_, _EW_, _feet_, _height_, _lam_, _lat_, _lon_, \
19 _meter_, _meter2_, _N_, _negative_, _northing_, _NS_, \
20 _NSEW_, _number_, _PERCENT_, _phi_, _precision_, \
21 _radians_, _radians2_, _radius_, _S_, _scalar_, \
22 _W_, _zone_, _std_ # PYCHOK used!
23from pygeodesy.lazily import _ALL_DOCS, _ALL_LAZY, _ALL_MODS as _MODS
24# from pygeodesy.named import _name__ # _MODS
25from pygeodesy.props import Property_RO
26# from pygeodesy.streprs import Fmt, fstr # from .unitsBase
27from pygeodesy.unitsBase import Float, Int, _NamedUnit, Radius, Str, Fmt, fstr
29from math import degrees, radians
31__all__ = _ALL_LAZY.units
32__version__ = '26.07.04'
35class Float_(Float):
36 '''Named C{float} with optional C{low} and C{high} limit.
37 '''
38 def __new__(cls, arg=None, name=NN, low=EPS, high=None, **Error_name_arg):
39 '''New, named C{Float_}, see L{Float}.
41 @arg cls: This class (C{Float_} or sub-class).
42 @kwarg arg: The value (any C{type} convertable to C{float}).
43 @kwarg name: Optional instance name (C{str}).
44 @kwarg low: Optional lower B{C{arg}} limit (C{float} or C{None}).
45 @kwarg high: Optional upper B{C{arg}} limit (C{float} or C{None}).
47 @returns: A named C{Float_}.
49 @raise Error: Invalid B{C{arg}} or B{C{arg}} below B{C{low}} or above B{C{high}}.
50 '''
51 self = Float.__new__(cls, arg=arg, name=name, **Error_name_arg)
52 t = _xlimits(self, low, high, g=True)
53 if t:
54 raise _NamedUnit._Error(cls, arg, name, txt=t, **Error_name_arg)
55 return self
58class Int_(Int):
59 '''Named C{int} with optional limits C{low} and C{high}.
60 '''
61 def __new__(cls, arg=None, name=NN, low=0, high=None, **Error_name_arg):
62 '''New, named C{int} instance with limits, see L{Int}.
64 @kwarg cls: This class (C{Int_} or sub-class).
65 @arg arg: The value (any C{type} convertable to C{int}).
66 @kwarg name: Optional instance name (C{str}).
67 @kwarg low: Optional lower B{C{arg}} limit (C{int} or C{None}).
68 @kwarg high: Optional upper B{C{arg}} limit (C{int} or C{None}).
70 @returns: A named L{Int_}.
72 @raise Error: Invalid B{C{arg}} or B{C{arg}} below B{C{low}} or above B{C{high}}.
73 '''
74 self = Int.__new__(cls, arg=arg, name=name, **Error_name_arg)
75 t = _xlimits(self, low, high)
76 if t:
77 raise _NamedUnit._Error(cls, arg, name, txt=t, **Error_name_arg)
78 return self
81class Bool(Int, _NamedUnit):
82 '''Named C{bool}, a sub-class of C{int} like Python's C{bool}.
83 '''
84 # _std_repr = True # set below
85 _bool_True_or_False = None
87 def __new__(cls, arg=None, name=NN, Error=UnitError, **name_arg):
88 '''New, named C{Bool}.
90 @kwarg cls: This class (C{Bool} or sub-class).
91 @kwarg arg: The value (any C{type} convertable to C{bool}).
92 @kwarg name: Optional instance name (C{str}).
93 @kwarg Error: Optional error to raise, overriding the default L{UnitError}.
94 @kwarg name_arg: Optional C{name=arg} keyword argument, inlieu of separate
95 B{C{arg}} and B{C{name}} ones.
97 @returns: A named L{Bool}, C{bool}-like.
99 @raise Error: Invalid B{C{arg}}.
100 '''
101 if name_arg:
102 name, arg = _NamedUnit._arg_name_arg2(arg, **name_arg)
103 try:
104 b = bool(arg)
105 except Exception as x:
106 raise _NamedUnit._Error(cls, arg, name, Error, cause=x)
108 self = Int.__new__(cls, b, name=name, Error=Error)
109 self._bool_True_or_False = b
110 return self
112 # <https://StackOverflow.com/questions/9787890/assign-class-boolean-value-in-python>
113 def __bool__(self): # PYCHOK Python 3+
114 return self._bool_True_or_False
116 __nonzero__ = __bool__ # PYCHOK Python 2-
118 def toRepr(self, std=False, **unused): # PYCHOK **unused
119 '''Return a representation of this C{Bool}.
121 @kwarg std: Use the standard C{repr} or the named representation (C{bool}).
123 @note: Use C{env} variable C{PYGEODESY_BOOL_STD_REPR=std} prior to C{import
124 pygeodesy} to get the standard C{repr} or set property C{std_repr=False}
125 to always get the named C{toRepr} representation.
126 '''
127 r = repr(self._bool_True_or_False)
128 return r if std else self._toRepr(r)
130 def toStr(self, **unused): # PYCHOK **unused
131 '''Return this C{Bool} as standard C{str}.
132 '''
133 return str(self._bool_True_or_False)
136class Band(Str):
137 '''Named C{str} representing a UTM/UPS band letter, unchecked.
138 '''
139 def __new__(cls, arg=None, name=_band_, **Error_name_arg):
140 '''New, named L{Band}, see L{Str}.
141 '''
142 return Str.__new__(cls, arg=arg, name=name, **Error_name_arg)
145class Degrees(Float):
146 '''Named C{float} representing a coordinate in C{degrees}, optionally clipped.
147 '''
148 _ddd_ = 1 # default for .dms._toDMS
149 _sep_ = S_SEP
150 _suf_ = (S_NUL,) * 3
152 def __new__(cls, arg=None, name=_degrees_, suffix=_NSEW_, clip=0, wrap=None, Error=UnitError, **name_arg):
153 '''New C{Degrees} instance, see L{Float}.
155 @arg cls: This class (C{Degrees} or sub-class).
156 @kwarg arg: The value (any C{type} convertable to C{float} or parsable by
157 function L{parseDMS<pygeodesy.dms.parseDMS>}).
158 @kwarg name: Optional instance name (C{str}).
159 @kwarg suffix: Optional, valid compass direction suffixes (C{NSEW}).
160 @kwarg clip: Optional B{C{arg}} range B{C{-clip..+clip}} (C{degrees} or C{0}
161 or C{None} for unclipped).
162 @kwarg wrap: Optionally adjust the B{C{arg}} value (L{wrap90<pygeodesy.wrap90>},
163 L{wrap180<pygeodesy.wrap180>} or L{wrap360<pygeodesy.wrap360>}).
164 @kwarg Error: Optional error to raise, overriding the default L{UnitError}.
165 @kwarg name_arg: Optional C{name=arg} keyword argument, inlieu of separate
166 B{C{arg}} and B{C{name}} ones.
168 @returns: A C{Degrees} instance.
170 @raise Error: Invalid B{C{arg}} or B{C{abs(arg)}} outside the B{C{clip}}
171 range and L{rangerrors<pygeodesy.rangerrors>} is C{True}.
172 '''
173 if name_arg:
174 name, arg = _NamedUnit._arg_name_arg2(arg, name, **name_arg)
175 try:
176 arg = parseDMS(arg, suffix=suffix, clip=clip)
177 if wrap:
178 _xcallable(wrap=wrap)
179 arg = wrap(arg)
180 self = Float.__new__(cls, arg=arg, name=name, Error=Error)
181 except Exception as x:
182 raise _NamedUnit._Error(cls, arg, name, Error, cause=x)
183 return self
185 def toDegrees(self):
186 '''Convert C{Degrees} to C{Degrees}.
187 '''
188 return self
190 def toRadians(self):
191 '''Convert C{Degrees} to C{Radians}.
192 '''
193 return Radians(radians(self), name=self.name)
195 def toRepr(self, std=False, **prec_fmt_ints): # PYCHOK prec=8, ...
196 '''Return a representation of this C{Degrees}.
198 @kwarg std: If C{True}, return the standard C{repr}, otherwise
199 the named representation (C{bool}).
201 @see: Methods L{Degrees.toStr}, L{Float.toRepr} and function
202 L{pygeodesy.toDMS} for futher C{prec_fmt_ints} details.
203 '''
204 return Float.toRepr(self, std=std, **prec_fmt_ints)
206 def toStr(self, prec=None, fmt=F__F_, ints=False, **s_D_M_S): # PYCHOK prec=8, ...
207 '''Return this C{Degrees} as standard C{str}.
209 @see: Function L{pygeodesy.toDMS} for futher details.
210 '''
211 if fmt.startswith(_PERCENT_): # use regular formatting
212 p = 8 if prec is None else prec
213 return fstr(self, prec=p, fmt=fmt, ints=ints, sep=self._sep_)
214 else:
215 s = self._suf_[signOf(self) + 1]
216 return _toDMS(self, fmt, prec, self._sep_, self._ddd_, s, s_D_M_S)
219class Degrees_(Degrees):
220 '''Named C{Degrees} representing a coordinate in C{degrees} with optional limits C{low} and C{high}.
221 '''
222 def __new__(cls, arg=None, name=_degrees_, low=None, high=None, **suffix_Error_name_arg):
223 '''New, named C{Degrees_}, see L{Degrees} and L{Float}.
225 @arg cls: This class (C{Degrees_} or sub-class).
226 @kwarg arg: The value (any C{type} convertable to C{float} or parsable by
227 function L{parseDMS<pygeodesy.dms.parseDMS>}).
228 @kwarg name: Optional instance name (C{str}).
229 @kwarg low: Optional lower B{C{arg}} limit (C{float} or C{None}).
230 @kwarg high: Optional upper B{C{arg}} limit (C{float} or C{None}).
232 @returns: A named C{Degrees}.
234 @raise Error: Invalid B{C{arg}} or B{C{arg}} below B{C{low}} or above B{C{high}}.
235 '''
236 self = Degrees.__new__(cls, arg=arg, name=name, clip=0, **suffix_Error_name_arg)
237 t = _xlimits(self, low, high)
238 if t:
239 raise _NamedUnit._Error(cls, arg, name, txt=t, **suffix_Error_name_arg)
240 return self
243class Degrees2(Float):
244 '''Named C{float} representing a distance in C{degrees squared}.
245 '''
246 def __new__(cls, arg=None, name=_degrees2_, **Error_name_arg):
247 '''See L{Float}.
248 '''
249 return Float.__new__(cls, arg=arg, name=name, **Error_name_arg)
252class Radians(Float):
253 '''Named C{float} representing a coordinate in C{radians}, optionally clipped.
254 '''
255 def __new__(cls, arg=None, name=_radians_, suffix=_NSEW_, clip=0, Error=UnitError, **name_arg):
256 '''New, named C{Radians}, see L{Float}.
258 @arg cls: This class (C{Radians} or sub-class).
259 @kwarg arg: The value (any C{type} convertable to C{float} or parsable by
260 L{pygeodesy.parseRad}).
261 @kwarg name: Optional instance name (C{str}).
262 @kwarg suffix: Optional, valid compass direction suffixes (C{NSEW}).
263 @kwarg clip: Optional B{C{arg}} range B{C{-clip..+clip}} (C{radians} or C{0}
264 or C{None} for unclipped).
265 @kwarg Error: Optional error to raise, overriding the default L{UnitError}.
266 @kwarg name_arg: Optional C{name=arg} keyword argument, inlieu of separate
267 B{C{arg}} and B{C{name}} ones.
269 @returns: A named C{Radians}.
271 @raise Error: Invalid B{C{arg}} or B{C{abs(arg)}} outside the B{C{clip}}
272 range and L{rangerrors<pygeodesy.rangerrors>} is C{True}.
273 '''
274 if name_arg:
275 name, arg = _NamedUnit._arg_name_arg2(arg, name, **name_arg)
276 try:
277 arg = parseRad(arg, suffix=suffix, clip=clip)
278 return Float.__new__(cls, arg, name=name, Error=Error)
279 except Exception as x:
280 raise _NamedUnit._Error(cls, arg, name, Error, cause=x)
282 def toDegrees(self):
283 '''Convert C{Radians} to C{Degrees}.
284 '''
285 return Degrees(degrees(self), name=self.name)
287 def toRadians(self):
288 '''Convert C{Radians} to C{Radians}.
289 '''
290 return self
292 def toRepr(self, std=False, **prec_fmt_ints): # PYCHOK prec=8, ...
293 '''Return a representation of this C{Radians}.
295 @kwarg std: If C{True}, return the standard C{repr}, otherwise
296 the named representation (C{bool}).
298 @see: Methods L{Radians.toStr}, L{Float.toRepr} and function
299 L{pygeodesy.toDMS} for more documentation.
300 '''
301 return Float.toRepr(self, std=std, **prec_fmt_ints)
303 def toStr(self, prec=8, fmt=F__F, ints=False): # PYCHOK prec=8, ...
304 '''Return this C{Radians} as standard C{str}.
306 @see: Function L{pygeodesy.fstr} for keyword argument details.
307 '''
308 return fstr(self, prec=prec, fmt=fmt, ints=ints)
311class Radians_(Radians):
312 '''Named C{float} representing a coordinate in C{radians} with optional limits C{low} and C{high}.
313 '''
314 def __new__(cls, arg=None, name=_radians_, low=_0_0, high=PI2, **suffix_Error_name_arg):
315 '''New, named C{Radians_}, see L{Radians}.
317 @arg cls: This class (C{Radians_} or sub-class).
318 @kwarg arg: The value (any C{type} convertable to C{float} or parsable by
319 function L{parseRad<pygeodesy.dms.parseRad>}).
320 @kwarg name: Optional name (C{str}).
321 @kwarg low: Optional lower B{C{arg}} limit (C{float} or C{None}).
322 @kwarg high: Optional upper B{C{arg}} limit (C{float} or C{None}).
324 @returns: A named C{Radians_}.
326 @raise Error: Invalid B{C{arg}} or B{C{arg}} below B{C{low}} or above B{C{high}}.
327 '''
328 self = Radians.__new__(cls, arg=arg, name=name, **suffix_Error_name_arg)
329 t = _xlimits(self, low, high)
330 if t:
331 raise _NamedUnit._Error(cls, arg, name, txt=t, **suffix_Error_name_arg)
332 return self
335class Radians2(Float_):
336 '''Named C{float} representing a distance in C{radians squared}.
337 '''
338 def __new__(cls, arg=None, name=_radians2_, **Error_name_arg):
339 '''New, named L{Radians2}, see L{Float_}.
340 '''
341 return Float_.__new__(cls, arg=arg, name=name, low=_0_0, **Error_name_arg)
344def _Degrees_new(cls, **arg_name_suffix_clip_Error_name_arg):
345 d = Degrees.__new__(cls, **arg_name_suffix_clip_Error_name_arg)
346 b = _umod_360(d) # 0 <= b < 360
347 return d if b == d else Degrees.__new__(cls, arg=b, name=d.name)
350class Azimuth(Degrees):
351 '''Named C{float} representing an azimuth in compass C{degrees} from (true) North.
352 '''
353 _ddd_ = 1
354 _suf_ = _W_, S_NUL, _E_ # no zero suffix
356 def __new__(cls, arg=None, name=_azimuth_, **clip_Error_name_arg):
357 '''New, named L{Azimuth} with optional suffix 'E' for clockwise or 'W' for
358 anti-clockwise, see L{Degrees}.
359 '''
360 return _Degrees_new(cls, arg=arg, name=name, suffix=_EW_, **clip_Error_name_arg)
363class Bearing(Degrees):
364 '''Named C{float} representing a bearing in compass C{degrees} from (true) North.
365 '''
366 _ddd_ = 1
367 _suf_ = _N_ * 3 # always suffix N
369 def __new__(cls, arg=None, name=_bearing_, **clip_Error_name_arg):
370 '''New, named L{Bearing}, see L{Degrees}.
371 '''
372 return _Degrees_new(cls, arg=arg, name=name, suffix=_N_, **clip_Error_name_arg)
375class Bearing_(Radians):
376 '''Named C{float} representing a bearing in C{radians} from compass C{degrees} from (true) North.
377 '''
378 def __new__(cls, arg=None, **name_clip_Error_name_arg):
379 '''New, named L{Bearing_}, see L{Bearing} and L{Radians}.
380 '''
381 d = Bearing.__new__(cls, arg=arg, **name_clip_Error_name_arg)
382 return Radians.__new__(cls, radians(d), name=d.name)
385class Distance(Float):
386 '''Named C{float} representing a distance, conventionally in C{meter}.
387 '''
388 def __new__(cls, arg=None, name=_distance_, **Error_name_arg):
389 '''New, named L{Distance}, see L{Float}.
390 '''
391 return Float.__new__(cls, arg=arg, name=name, **Error_name_arg)
394class Distance_(Float_):
395 '''Named C{float} with optional C{low} and C{high} limits representing a distance, conventionally in C{meter}.
396 '''
397 def __new__(cls, arg=None, name=_distance_, **low_high_Error_name_arg):
398 '''New L{Distance_} instance, see L{Float}.
399 '''
400 return Float_.__new__(cls, arg=arg, name=name, **low_high_Error_name_arg)
403class _EasNorBase(Float):
404 '''(INTERNAL) L{Easting} and L{Northing} base class.
405 '''
406 def __new__(cls, arg, name, falsed, high, **Error_name_arg):
407 self = Float.__new__(cls, arg=arg, name=name, **Error_name_arg)
408 low = self < 0
409 if (high is not None) and (low or self > high): # like Veness
410 t = _negative_ if low else Fmt.limit(above=high)
411 elif low and falsed:
412 t = _COMMASPACE_(_negative_, 'falsed')
413 else:
414 return self
415 raise _NamedUnit._Error(cls, arg, name, txt=t, **Error_name_arg)
418class Easting(_EasNorBase):
419 '''Named C{float} representing an easting, conventionally in C{meter}.
420 '''
421 def __new__(cls, arg=None, name=_easting_, falsed=False, high=None, **Error_name_arg):
422 '''New, named C{Easting} or C{Easting of Point}, see L{Float}.
424 @arg cls: This class (C{Easting} or sub-class).
425 @kwarg arg: The value (any C{type} convertable to C{float}).
426 @kwarg name: Optional name (C{str}).
427 @kwarg falsed: If C{True}, the B{C{arg}} value is falsed (C{bool}).
428 @kwarg high: Optional upper B{C{arg}} limit (C{scalar} or C{None}).
430 @returns: A named C{Easting}.
432 @raise Error: Invalid B{C{arg}}, above B{C{high}} or negative, falsed B{C{arg}}.
433 '''
434 return _EasNorBase.__new__(cls, arg, name, falsed, high, **Error_name_arg)
437class Epoch(Float_): # in .ellipsoidalBase, .trf
438 '''Named C{epoch} with optional C{low} and C{high} limits representing a fractional
439 calendar year.
440 '''
441 _std_repr = False
443 def __new__(cls, arg=None, name=_epoch_, low=1900, high=9999, Error=TRFError, **name_arg):
444 '''New, named L{Epoch}, see L{Float_}.
445 '''
446 if name_arg:
447 name, arg = _NamedUnit._arg_name_arg2(arg, name, **name_arg)
448 return arg if isinstance(arg, Epoch) else Float_.__new__(cls,
449 arg=arg, name=name, Error=Error, low=low, high=high)
451 def toRepr(self, prec=3, std=False, **unused): # PYCHOK fmt=Fmt.F, ints=True
452 '''Return a representation of this C{Epoch}.
454 @kwarg std: Use the standard C{repr} or the named
455 representation (C{bool}).
457 @see: Method L{Float.toRepr} for more documentation.
458 '''
459 return Float_.toRepr(self, prec=prec, std=std) # fmt=Fmt.F, ints=True
461 def toStr(self, prec=3, **unused): # PYCHOK fmt=Fmt.F, nts=True
462 '''Format this C{Epoch} as C{str}.
464 @see: Function L{pygeodesy.fstr} for more documentation.
465 '''
466 return fstr(self, prec=prec, fmt=Fmt.F, ints=True)
468 __str__ = toStr # PYCHOK default '%.3F', with trailing zeros and decimal point
471class Feet(Float):
472 '''Named C{float} representing a distance or length in C{feet}.
473 '''
474 def __new__(cls, arg=None, name=_feet_, **Error_name_arg):
475 '''New, named L{Feet}, see L{Float}.
476 '''
477 return Float.__new__(cls, arg=arg, name=name, **Error_name_arg)
480class FIx(Float_):
481 '''A named I{Fractional Index}, an C{int} or C{float} index into a C{list}
482 or C{tuple} of C{points}, typically. A C{float} I{Fractional Index}
483 C{fi} represents a location on the edge between C{points[int(fi)]} and
484 C{points[(int(fi) + 1) % len(points)]}.
485 '''
486 _fin = 0
488 def __new__(cls, fi, fin=None, Error=UnitError, **name):
489 '''New, named I{Fractional Index} in a C{list} or C{tuple} of points.
491 @arg fi: The fractional index (C{float} or C{int}).
492 @kwarg fin: Optional C{len}, the number of C{points}, the index
493 C{[n]} wrapped to C{[0]} (C{int} or C{None}).
494 @kwarg Error: Optional error to raise.
495 @kwarg name: Optional C{B{name}=NN} (C{str}).
497 @return: A named B{C{fi}} (L{FIx}).
499 @note: The returned B{C{fi}} may exceed the B{C{len}}, number of
500 original C{points} in certain open/closed cases.
502 @see: Method L{fractional} or function L{pygeodesy.fractional}.
503 '''
504 _ = _MODS.named._name__(name) if name else NN # check error
505 n = Int_(fin=fin, low=0) if fin else None
506 f = Float_.__new__(cls, fi, low=_0_0, high=n, Error=Error, **name)
507 i = int(f)
508 r = f - float(i)
509 if r < EPS: # see .points._fractional
510 f = Float_.__new__(cls, i, low=_0_0, Error=Error, **name)
511 elif r > EPS1:
512 f = Float_.__new__(cls, i + 1, high=n, Error=Error, **name)
513 if n: # non-zero and non-None
514 f._fin = n
515 return f
517 @Property_RO
518 def fin(self):
519 '''Get the given C{len}, the index C{[n]} wrapped to C{[0]} (C{int}).
520 '''
521 return self._fin
523 def fractional(self, points, wrap=None, **LatLon_or_Vector_and_kwds):
524 '''Return the point at this I{Fractional Index}.
526 @arg points: The points (C{LatLon}[], L{Numpy2LatLon}[], L{Tuple2LatLon}[] or
527 C{other}[]).
528 @kwarg wrap: If C{True}, wrap or I{normalize} and unroll the B{C{points}}
529 (C{bool}) or C{None} for backward compatible L{LatLon2Tuple} or
530 B{C{LatLon}} with I{averaged} lat- and longitudes.
531 @kwarg LatLon_or_Vector_and_kwds: Optional C{B{LatLon}=None} I{or} C{B{Vector}=None}
532 to return the I{intermediate}, I{fractional} point and optionally,
533 additional B{C{LatLon}} I{or} B{C{Vector}} keyword arguments, see
534 function L{fractional<pygeodesy.points.fractional>}.
536 @return: See function L{fractional<pygeodesy.points.fractional>}.
538 @raise IndexError: In fractional index invalid or B{C{points}} not
539 subscriptable or not closed.
541 @raise TypeError: Invalid B{C{LatLon}}, B{C{Vector}} or B{C{kwds}} argument.
543 @see: Function L{pygeodesy.fractional}.
544 '''
545 # fi = 0 if self == self.fin else self
546 return _MODS.points.fractional(points, self, wrap=wrap, **LatLon_or_Vector_and_kwds)
549def _fi_j2(f, n): # PYCHOK in .ellipsoidalBaseDI, .vector3d
550 # Get 2-tuple (C{fi}, C{j})
551 i = int(f) # like L{FIx}
552 if not 0 <= i < n:
553 raise _AssertionError(i=i, n=n, f=f, r=f - float(i))
554 return FIx(fi=f, fin=n), (i + 1) % n
557class Height(Float): # here to avoid circular import
558 '''Named C{float} representing a height, conventionally in C{meter}.
559 '''
560 def __new__(cls, arg=None, name=_height_, **Error_name_arg):
561 '''New, named L{Height}, see L{Float}.
562 '''
563 return Float.__new__(cls, arg=arg, name=name, **Error_name_arg)
566class Height_(Float_): # here to avoid circular import
567 '''Named C{float} with optional C{low} and C{high} limits representing a height, conventionally in C{meter}.
568 '''
569 def __new__(cls, arg=None, name=_height_, **low_high_Error_name_arg):
570 '''New, named L{Height}, see L{Float}.
571 '''
572 return Float_.__new__(cls, arg=arg, name=name, **low_high_Error_name_arg)
575class HeightX(Height):
576 '''Like L{Height}, used to distinguish the interpolated height
577 from an original L{Height} at a clip intersection.
578 '''
579 pass
582def _heigHt(inst, height):
583 '''(INTERNAL) Override the C{inst}ance' height.
584 '''
585 return inst.height if height is None else Height(height)
588class Lam(Radians):
589 '''Named C{float} representing a longitude in C{radians}.
590 '''
591 def __new__(cls, arg=None, name=_lam_, clip=PI, **Error_name_arg):
592 '''New, named L{Lam}, see L{Radians}.
593 '''
594 return Radians.__new__(cls, arg=arg, name=name, suffix=_EW_, clip=clip, **Error_name_arg)
597class Lamd(Lam):
598 '''Named C{float} representing a longitude in C{radians} converted from C{degrees}.
599 '''
600 def __new__(cls, arg=None, name=_lon_, clip=180, **Error_name_arg):
601 '''New, named L{Lamd}, see L{Lam} and L{Radians}.
602 '''
603 d = Degrees(arg=arg, name=name, clip=clip, **Error_name_arg)
604 return Lam.__new__(cls, radians(d), clip=radians(clip), name=d.name)
607class Lat(Degrees):
608 '''Named C{float} representing a latitude in C{degrees}.
609 '''
610 _ddd_ = 2
611 _suf_ = _S_, S_NUL, _N_ # no zero suffix
613 def __new__(cls, arg=None, name=_lat_, clip=90, **Error_name_arg):
614 '''New, named L{Lat}, see L{Degrees}.
615 '''
616 return Degrees.__new__(cls, arg=arg, name=name, suffix=_NS_, clip=clip, **Error_name_arg)
619class Lat_(Degrees_):
620 '''Named C{float} representing a latitude in C{degrees} within limits C{low} and C{high}.
621 '''
622 _ddd_ = 2
623 _suf_ = _S_, S_NUL, _N_ # no zero suffix
625 def __new__(cls, arg=None, name=_lat_, low=-90, high=90, **Error_name_arg):
626 '''See L{Degrees_}.
627 '''
628 return Degrees_.__new__(cls, arg=arg, name=name, suffix=_NS_, low=low, high=high, **Error_name_arg)
631def _Lat0(lat): # in .ellipsods and .triaxials.bases
632 '''(INTERNAL) Get latitude.
633 '''
634 return Lat(lat.degrees0 if _MODS.angles.isAng(lat) else lat)
637class Lon(Degrees):
638 '''Named C{float} representing a longitude in C{degrees}.
639 '''
640 _ddd_ = 3
641 _suf_ = _W_, S_NUL, _E_ # no zero suffix
643 def __new__(cls, arg=None, name=_lon_, clip=180, **Error_name_arg):
644 '''New, named L{Lon}, see L{Degrees}.
645 '''
646 return Degrees.__new__(cls, arg=arg, name=name, suffix=_EW_, clip=clip, **Error_name_arg)
649class Lon_(Degrees_):
650 '''Named C{float} representing a longitude in C{degrees} within limits C{low} and C{high}.
651 '''
652 _ddd_ = 3
653 _suf_ = _W_, S_NUL, _E_ # no zero suffix
655 def __new__(cls, arg=None, name=_lon_, low=-180, high=180, **Error_name_arg):
656 '''New, named L{Lon_}, see L{Lon} and L{Degrees_}.
657 '''
658 return Degrees_.__new__(cls, arg=arg, name=name, suffix=_EW_, low=low, high=high, **Error_name_arg)
661class Meter(Float):
662 '''Named C{float} representing a distance or length in C{meter}.
663 '''
664 def __new__(cls, arg=None, name=_meter_, **Error_name_arg):
665 '''New, named L{Meter}, see L{Float}.
666 '''
667 return Float.__new__(cls, arg=arg, name=name, **Error_name_arg)
669 def __repr__(self):
670 '''Return a representation of this C{Meter}.
672 @see: Method C{Str.toRepr} and property C{Str.std_repr}.
674 @note: Use C{env} variable C{PYGEODESY_METER_STD_REPR=std} prior to C{import
675 pygeodesy} to get the standard C{repr} or set property C{std_repr=False}
676 to always get the named C{toRepr} representation.
677 '''
678 return self.toRepr(std=self._std_repr)
681# _1Å = Meter( _Å= 1e-10) # PyCHOK 1 Ångstrōm
682_1um = Meter( _1um= 1.e-6) # PYCHOK 1 micrometer in .mgrs
683_10um = Meter( _10um= 1.e-5) # PYCHOK 10 micrometer in .osgr
684_1mm = Meter( _1mm=_0_001) # PYCHOK 1 millimeter in .ellipsoidal...
685_100km = Meter( _100km= 1.e+5) # PYCHOK 100 kilometer in .formy, .mgrs, .osgr, .sphericalBase
686_2000km = Meter(_2000km= 2.e+6) # PYCHOK 2,000 kilometer in .mgrs
689class Meter_(Float_):
690 '''Named C{float} representing a distance or length in C{meter}.
691 '''
692 def __new__(cls, arg=None, name=_meter_, low=_0_0, **high_Error_name_arg):
693 '''New, named L{Meter_}, see L{Meter} and L{Float_}.
694 '''
695 return Float_.__new__(cls, arg=arg, name=name, low=low, **high_Error_name_arg)
698class Meter2(Float_):
699 '''Named C{float} representing an area in C{meter squared}.
700 '''
701 def __new__(cls, arg=None, name=_meter2_, **Error_name_arg):
702 '''New, named L{Meter2}, see L{Float_}.
703 '''
704 return Float_.__new__(cls, arg=arg, name=name, low=_0_0, **Error_name_arg)
707class Meter3(Float_):
708 '''Named C{float} representing a volume in C{meter cubed}.
709 '''
710 def __new__(cls, arg=None, name='meter3', **Error_name_arg):
711 '''New, named L{Meter3}, see L{Float_}.
712 '''
713 return Float_.__new__(cls, arg=arg, name=name, low=_0_0, **Error_name_arg)
716class Northing(_EasNorBase):
717 '''Named C{float} representing a northing, conventionally in C{meter}.
718 '''
719 def __new__(cls, arg=None, name=_northing_, falsed=False, high=None, **Error_name_arg):
720 '''New, named C{Northing} or C{Northing of point}, see L{Float}.
722 @arg cls: This class (C{Northing} or sub-class).
723 @kwarg arg: The value (any C{type} convertable to C{float}).
724 @kwarg name: Optional name (C{str}).
725 @kwarg falsed: If C{True}, the B{C{arg}} value is falsed (C{bool}).
726 @kwarg high: Optional upper B{C{arg}} limit (C{scalar} or C{None}).
728 @returns: A named C{Northing}.
730 @raise Error: Invalid B{C{arg}}, above B{C{high}} or negative, falsed B{C{arg}}.
731 '''
732 return _EasNorBase.__new__(cls, arg, name, falsed, high, **Error_name_arg)
735class Number_(Int_):
736 '''Named C{int} representing a non-negative number.
737 '''
738 def __new__(cls, arg=None, name=_number_, **low_high_Error_name_arg):
739 '''New, named L{Number_}, see L{Int_}.
740 '''
741 return Int_.__new__(cls, arg=arg, name=name, **low_high_Error_name_arg)
744class Phi(Radians):
745 '''Named C{float} representing a latitude in C{radians}.
746 '''
747 def __new__(cls, arg=None, name=_phi_, clip=PI_2, **Error_name_arg):
748 '''New, named L{Phi}, see L{Radians}.
749 '''
750 return Radians.__new__(cls, arg=arg, name=name, suffix=_NS_, clip=clip, **Error_name_arg)
753class Phid(Phi):
754 '''Named C{float} representing a latitude in C{radians} converted from C{degrees}.
755 '''
756 def __new__(cls, arg=None, name=_lat_, clip=90, **Error_name_arg):
757 '''New, named L{Phid}, see L{Phi} and L{Radians}.
758 '''
759 d = Degrees(arg=arg, name=name, clip=clip, **Error_name_arg)
760 return Phi.__new__(cls, arg=radians(d), clip=radians(clip), name=d.name)
763class Precision_(Int_):
764 '''Named C{int} with optional C{low} and C{high} limits representing a precision.
765 '''
766 def __new__(cls, arg=None, name=_precision_, **low_high_Error_name_arg):
767 '''New, named L{Precision_}, see L{Int_}.
768 '''
769 return Int_.__new__(cls, arg=arg, name=name, **low_high_Error_name_arg)
772class Radius_(Float_):
773 '''Named C{float} with optional C{low} and C{high} limits representing a radius, conventionally in C{meter}.
774 '''
775 def __new__(cls, arg=None, name=_radius_, **low_high_Error_name_arg):
776 '''New, named L{Radius_}, see L{Radius} and L{Float}.
777 '''
778 return Float_.__new__(cls, arg=arg, name=name, **low_high_Error_name_arg)
781class Scalar(Float):
782 '''Named C{float} representing a factor, fraction, scale, etc.
783 '''
784 def __new__(cls, arg=None, name=_scalar_, **Error_name_arg):
785 '''New, named L{Scalar}, see L{Float}.
786 '''
787 return Float.__new__(cls, arg=arg, name=name, **Error_name_arg)
790class Scalar_(Float_):
791 '''Named C{float} with optional C{low} and C{high} limits representing a factor, fraction, scale, etc.
792 '''
793 def __new__(cls, arg=None, name=_scalar_, low=_0_0, **high_Error_name_arg):
794 '''New, named L{Scalar_}, see L{Scalar} and L{Float_}.
795 '''
796 return Float_.__new__(cls, arg=arg, name=name, low=low, **high_Error_name_arg)
799class Zone(Int):
800 '''Named C{int} representing a UTM/UPS zone number.
801 '''
802 def __new__(cls, arg=None, name=_zone_, **Error_name_arg):
803 '''New, named L{Zone}, see L{Int}
804 '''
805 # usually low=_UTMUPS_ZONE_MIN, high=_UTMUPS_ZONE_MAX
806 return Int_.__new__(cls, arg=arg, name=name, **Error_name_arg)
809_Degrees = (Azimuth, Bearing, Bearing_, Degrees, Degrees_)
810_Meters = (Distance, Distance_, Meter, Meter_)
811_Radians = (Radians, Radians_) # PYCHOK unused
812_Radii = _Meters + (Radius, Radius_)
813_ScalarU = Float, Float_, Scalar, Scalar_
816def _isDegrees(obj, iscalar=True):
817 # Check for valid degrees types.
818 return isinstance(obj, _Degrees) or (iscalar and _isScalar(obj))
821def _isHeight(obj, iscalar=True):
822 # Check for valid height types.
823 return isinstance(obj, _Meters) or (iscalar and _isScalar(obj))
826def _isMeter(obj, iscalar=True):
827 # Check for valid meter types.
828 return isinstance(obj, _Meters) or (iscalar and _isScalar(obj))
831def _isRadians(obj, iscalar=True):
832 # Check for valid radian types.
833 return isinstance(obj, _Radians) or (iscalar and _isScalar(obj))
836def _isRadius(obj, iscalar=True):
837 # Check for valid earth radius types.
838 return isinstance(obj, _Radii) or (iscalar and _isScalar(obj))
841def _isScalar(obj, iscalar=True):
842 # Check for pure scalar types.
843 return isinstance(obj, _ScalarU) or (iscalar and isscalar(obj) and not isinstance(obj, _NamedUnit))
846def _toUnit(Unit, arg, name=NN, **Error):
847 '''(INTERNAL) Wrap C{arg} in a C{name}d C{Unit}.
848 '''
849 if not (issubclassof(Unit, _NamedUnit) and isinstance(arg, Unit) and
850 _xattr(arg, name=NN) == name):
851 arg = Unit(arg, name=name, **Error)
852 return arg
855def _xlimits(arg, low, high, g=False):
856 '''(INTERNAL) Check C{low <= arg <= high}.
857 '''
858 if (low is not None) and (arg < low or _isNAN(arg)):
859 if g:
860 low = Fmt.g(low, prec=6, ints=isinstance(arg, Epoch))
861 t = Fmt.limit(below=low)
862 elif (high is not None) and (arg > high or _isNAN(arg)):
863 if g:
864 high = Fmt.g(high, prec=6, ints=isinstance(arg, Epoch))
865 t = Fmt.limit(above=high)
866 else:
867 t = NN
868 return t
871def _std_repr(*Classes):
872 '''(INTERNAL) Use standard C{repr} or named C{toRepr}.
873 '''
874 from pygeodesy.internals import _getenv
875 for C in Classes:
876 if hasattr(C, typename(_std_repr)): # noqa: F821 del
877 env = 'PYGEODESY_%s_STD_REPR' % (typename(C).upper(),)
878 if _getenv(env, _std_).lower() != _std_:
879 C._std_repr = False
881_std_repr(Azimuth, Bearing, Bool, Degrees, Epoch, Float, Int, Meter, Radians, Str) # PYCHOK expected
882del _std_repr
884__all__ += _ALL_DOCS(_NamedUnit)
886# **) MIT License
887#
888# Copyright (C) 2016-2026 -- mrJean1 at Gmail -- All Rights Reserved.
889#
890# Permission is hereby granted, free of charge, to any person obtaining a
891# copy of this software and associated documentation files (the "Software"),
892# to deal in the Software without restriction, including without limitation
893# the rights to use, copy, modify, merge, publish, distribute, sublicense,
894# and/or sell copies of the Software, and to permit persons to whom the
895# Software is furnished to do so, subject to the following conditions:
896#
897# The above copyright notice and this permission notice shall be included
898# in all copies or substantial portions of the Software.
899#
900# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
901# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
902# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
903# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
904# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
905# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
906# OTHER DEALINGS IN THE SOFTWARE.