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