Coverage for pygeodesy/units.py: 95%
330 statements
« prev ^ index » next coverage.py v7.2.2, created at 2024-05-06 16:50 -0400
« prev ^ index » next coverage.py v7.2.2, created at 2024-05-06 16:50 -0400
2# -*- coding: utf-8 -*-
4u'''Various units, all sub-classes of C{Float}, C{Int} and
5C{Str} from basic C{float}, C{int} respectively C{str} to
6named units as L{Degrees}, L{Feet}, L{Meter}, L{Radians}, etc.
7'''
9from pygeodesy.basics import isinstanceof, isscalar, isstr, issubclassof, \
10 signOf
11from pygeodesy.constants import EPS, EPS1, PI, PI2, PI_2, \
12 _umod_360, _0_0, _0_001, \
13 _0_5, INT0 # PYCHOK for .mgrs, .namedTuples
14from pygeodesy.dms import F__F, F__F_, parseDMS, parseRad, \
15 S_NUL, S_SEP, _toDMS, toDMS
16from pygeodesy.errors import _AssertionError, _IsnotError, TRFError, \
17 UnitError, _xkwds, _xkwds_item2
18from pygeodesy.interns import NN, _band_, _bearing_, _degrees_, _degrees2_, \
19 _distance_, _E_, _easting_, _epoch_, _EW_, \
20 _feet_, _height_, _lam_, _lat_, \
21 _LatLon_, _lon_, _meter_, _meter2_, _N_, \
22 _northing_, _NS_, _NSEW_, _number_, _PERCENT_, \
23 _phi_, _precision_, _radians_, _radians2_, \
24 _radius_, _S_, _scalar_, _units_, \
25 _W_, _zone_, _std_ # PYCHOK used!
26from pygeodesy.lazily import _ALL_DOCS, _ALL_LAZY, _ALL_MODS as _MODS, _getenv
27from pygeodesy.props import Property_RO
28# from pygeodesy.streprs import Fmt, fstr # from .unitsBase
29from pygeodesy.unitsBase import _Error, Float, Fmt, fstr, Int, _NamedUnit, \
30 Radius, Str # PYCHOK shared .namedTuples
31from math import degrees, radians
33__all__ = _ALL_LAZY.units
34__version__ = '24.02.20'
36_negative_falsed_ = 'negative, falsed'
39class Float_(Float):
40 '''Named C{float} with optional C{low} and C{high} limit.
41 '''
42 def __new__(cls, arg=None, name=NN, Error=UnitError, low=EPS, high=None, **name_arg):
43 '''New C{Float_} instance.
45 @arg cls: This class (C{Float_} or sub-class).
46 @kwarg arg: The value (any C{type} convertable to C{float}).
47 @kwarg name: Optional instance name (C{str}).
48 @kwarg Error: Optional error to raise, overriding the default L{UnitError}.
49 @kwarg low: Optional lower B{C{arg}} limit (C{float} or C{None}).
50 @kwarg high: Optional upper B{C{arg}} limit (C{float} or C{None}).
51 @kwarg name_arg: Optional C{name=arg} keyword argument, inlieu of B{C{name}}
52 and B{C{arg}}.
54 @returns: A C{Float_} instance.
56 @raise Error: Invalid B{C{arg}} or B{C{arg}} below B{C{low}} or above B{C{high}}.
57 '''
58 if name_arg:
59 name, arg = _xkwds_item2(name_arg)
60 self = Float.__new__(cls, arg=arg, name=name, Error=Error)
61 if (low is not None) and self < low:
62 txt = Fmt.limit(below=Fmt.g(low, prec=6, ints=isinstance(self, Epoch)))
63 elif (high is not None) and self > high:
64 txt = Fmt.limit(above=Fmt.g(high, prec=6, ints=isinstance(self, Epoch)))
65 else:
66 return self
67 raise _Error(cls, arg, name, Error, txt=txt)
70class Int_(Int):
71 '''Named C{int} with optional limits C{low} and C{high}.
72 '''
73 def __new__(cls, arg=None, name=NN, Error=UnitError, low=0, high=None, **name_arg):
74 '''New named C{int} instance with limits.
76 @kwarg cls: This class (C{Int_} or sub-class).
77 @arg arg: The value (any C{type} convertable to C{int}).
78 @kwarg name: Optional instance name (C{str}).
79 @kwarg Error: Optional error to raise, overriding the default C{UnitError}.
80 @kwarg low: Optional lower B{C{arg}} limit (C{int} or C{None}).
81 @kwarg high: Optional upper B{C{arg}} limit (C{int} or C{None}).
82 @kwarg name_arg: Optional C{name=arg} keyword argument, inlieu of B{C{name}}
83 and B{C{arg}}.
85 @returns: An L{Int_} instance.
87 @raise Error: Invalid B{C{arg}} or B{C{arg}} below B{C{low}} or above B{C{high}}.
88 '''
89 if name_arg:
90 name, arg = _xkwds_item2(name_arg)
91 self = Int.__new__(cls, arg=arg, name=name, Error=Error)
92 if (low is not None) and self < low:
93 txt = Fmt.limit(below=low)
94 elif (high is not None) and self > high:
95 txt = Fmt.limit(above=high)
96 else:
97 return self
98 raise _Error(cls, arg, name, Error, txt=txt)
101class Bool(Int, _NamedUnit):
102 '''Named C{bool}, a sub-class of C{int} like Python's C{bool}.
103 '''
104 # _std_repr = True # set below
105 _bool_True_or_False = None
107 def __new__(cls, arg=None, name=NN, Error=UnitError, **name_arg):
108 '''New C{Bool} instance.
110 @kwarg cls: This class (C{Bool} or sub-class).
111 @kwarg arg: The value (any C{type} convertable to C{bool}).
112 @kwarg name: Optional instance name (C{str}).
113 @kwarg Error: Optional error to raise, overriding the default
114 L{UnitError}.
115 @kwarg name_arg: Optional C{name=arg} keyword argument, inlieu
116 of B{C{name}} and B{C{arg}}.
118 @returns: A L{Bool}, a C{bool}-like instance.
120 @raise Error: Invalid B{C{arg}}.
121 '''
122 if name_arg:
123 name, arg = _xkwds_item2(name_arg)
124 try:
125 b = bool(arg)
126 except Exception as x: # XXX not ... as x:
127 raise _Error(cls, arg, name, Error, x=x)
129 self = Int.__new__(cls, b, name=name, Error=Error)
130 self._bool_True_or_False = b
131 return self
133 # <https://StackOverflow.com/questions/9787890/assign-class-boolean-value-in-python>
134 def __bool__(self): # PYCHOK Python 3+
135 return self._bool_True_or_False
137 __nonzero__ = __bool__ # PYCHOK Python 2-
139 def toRepr(self, std=False, **unused): # PYCHOK **unused
140 '''Return a representation of this C{Bool}.
142 @kwarg std: Use the standard C{repr} or the named
143 representation (C{bool}).
145 @note: Use C{env} variable C{PYGEODESY_BOOL_STD_REPR=std}
146 prior to C{import pygeodesy} to get the standard
147 C{repr} or set property C{std_repr=False} to always
148 get the named C{toRepr} representation.
149 '''
150 r = repr(self._bool_True_or_False)
151 return r if std else self._toRepr(r)
153 def toStr(self, **unused): # PYCHOK **unused
154 '''Return this C{Bool} as standard C{str}.
155 '''
156 return str(self._bool_True_or_False)
159class Band(Str):
160 '''Named C{str} representing a UTM/UPS band letter, unchecked.
161 '''
162 def __new__(cls, arg=None, name=_band_, **Error_name_arg):
163 '''New L{Band} instance, see L{Str}.
164 '''
165 return Str.__new__(cls, arg=arg, name=name, **Error_name_arg)
168class Degrees(Float):
169 '''Named C{float} representing a coordinate in C{degrees}, optionally clipped.
170 '''
171 _ddd_ = 1 # default for .dms._toDMS
172 _sep_ = S_SEP
173 _suf_ = (S_NUL,) * 3
175 def __new__(cls, arg=None, name=_degrees_, Error=UnitError, suffix=_NSEW_, clip=0, wrap=None, **name_arg):
176 '''New C{Degrees} instance, see L{Float}.
178 @arg cls: This class (C{Degrees} or sub-class).
179 @kwarg arg: The value (any scalar C{type} convertable to C{float} or
180 parsable by L{pygeodesy.parseDMS}).
181 @kwarg name: Optional instance name (C{str}).
182 @kwarg Error: Optional error to raise, overriding the default
183 L{UnitError}.
184 @kwarg suffix: Optional, valid compass direction suffixes (C{NSEW}).
185 @kwarg clip: Optional B{C{arg}} range B{C{-clip..+clip}}
186 (C{degrees} or C{0} or C{None} for unclipped).
187 @kwarg wrap: Optionally adjust the B{C{arg}} value (L{pygeodesy.wrap90},
188 L{pygeodesy.wrap180} or L{pygeodesy.wrap360}).
189 @kwarg name_arg: Optional C{name=arg} keyword argument, inlieu of
190 B{C{name}} and B{C{arg}}.
192 @returns: A C{Degrees} instance.
194 @raise Error: Invalid B{C{arg}} or B{C{abs(arg)}} outside the B{C{clip}}
195 range and L{pygeodesy.rangerrors} set to C{True}.
196 '''
197 if name_arg:
198 name, arg = _xkwds_item2(name_arg)
199 try:
200 d = Float.__new__(cls, parseDMS(arg, suffix=suffix, clip=clip),
201 Error=Error, name=name)
202 if wrap:
203 w = wrap(d)
204 if w != d:
205 d = Float.__new__(cls, arg=w, name=name, Error=Error)
206 except Exception as x:
207 raise _Error(cls, arg, name, Error, x=x)
208 return d
210 def toDegrees(self):
211 '''Convert C{Degrees} to C{Degrees}.
212 '''
213 return self
215 def toRadians(self):
216 '''Convert C{Degrees} to C{Radians}.
217 '''
218 return Radians(radians(self), name=self.name)
220 def toRepr(self, std=False, **prec_fmt_ints): # PYCHOK prec=8, ...
221 '''Return a representation of this C{Degrees}.
223 @kwarg std: If C{True} return the standard C{repr},
224 otherwise the named representation (C{bool}).
226 @see: Methods L{Degrees.toStr}, L{Float.toRepr} and function
227 L{pygeodesy.toDMS} for more documentation.
228 '''
229 return Float.toRepr(self, std=std, **prec_fmt_ints)
231 def toStr(self, prec=None, fmt=F__F_, ints=False, **s_D_M_S): # PYCHOK prec=8, ...
232 '''Return this C{Degrees} as standard C{str}.
234 @see: Function L{pygeodesy.toDMS} for keyword argument details.
235 '''
236 if fmt.startswith(_PERCENT_): # use regular formatting
237 p = 8 if prec is None else prec
238 return fstr(self, prec=p, fmt=fmt, ints=ints, sep=self._sep_)
239 else:
240 s = self._suf_[signOf(self) + 1]
241 return _toDMS(self, fmt, prec, self._sep_, self._ddd_, s, s_D_M_S)
244class Degrees_(Degrees):
245 '''Named C{Degrees} representing a coordinate in C{degrees} with optional limits C{low} and C{high}.
246 '''
247 def __new__(cls, arg=None, name=_degrees_, Error=UnitError, suffix=_NSEW_, low=None, high=None, **name_arg):
248 '''New C{Degrees_} instance, see L{Degrees} and L{Float}..
250 @arg cls: This class (C{Degrees_} or sub-class).
251 @kwarg arg: The value (any C{type} convertable to C{float} or parsable by
252 L{pygeodesy.parseDMS}).
253 @kwarg name: Optional instance name (C{str}).
254 @kwarg Error: Optional error to raise, overriding the default L{UnitError}.
255 @kwarg suffix: Optional, valid compass direction suffixes (C{NSEW}).
256 @kwarg low: Optional lower B{C{arg}} limit (C{float} or C{None}).
257 @kwarg high: Optional upper B{C{arg}} limit (C{float} or C{None}).
258 @kwarg name_arg: Optional C{name=arg} keyword argument, inlieu of B{C{name}}
259 and B{C{arg}}.
261 @returns: A C{Degrees} instance.
263 @raise Error: Invalid B{C{arg}} or B{C{arg}} below B{C{low}} or above B{C{high}}.
264 '''
265 if name_arg:
266 name, arg = _xkwds_item2(name_arg)
267 self = Degrees.__new__(cls, arg=arg, name=name, Error=Error, suffix=suffix, clip=0)
268 if (low is not None) and self < low:
269 txt = Fmt.limit(below=low)
270 elif (high is not None) and self > high:
271 txt = Fmt.limit(above=high)
272 else:
273 return self
274 raise _Error(cls, arg, name, Error, txt=txt)
277class Degrees2(Float):
278 '''Named C{float} representing a distance in C{degrees squared}.
279 '''
280 def __new__(cls, arg=None, name=_degrees2_, **Error_name_arg):
281 '''See L{Float}.
282 '''
283 return Float.__new__(cls, arg=arg, name=name, **Error_name_arg)
286class Radians(Float):
287 '''Named C{float} representing a coordinate in C{radians}, optionally clipped.
288 '''
289 def __new__(cls, arg=None, name=_radians_, Error=UnitError, suffix=_NSEW_, clip=0, **name_arg):
290 '''New C{Radians} instance, see L{Float}.
292 @arg cls: This class (C{Radians} or sub-class).
293 @kwarg arg: The value (any C{type} convertable to C{float} or parsable
294 by L{pygeodesy.parseRad}).
295 @kwarg name: Optional instance name (C{str}).
296 @kwarg Error: Optional error to raise, overriding the default L{UnitError}.
297 @kwarg suffix: Optional, valid compass direction suffixes (C{NSEW}).
298 @kwarg clip: Optional B{C{arg}} range B{C{-clip..+clip}} (C{radians} or C{0}
299 or C{None} for unclipped).
300 @kwarg name_arg: Optional C{name=arg} keyword argument, inlieu of B{C{name}}
301 and B{C{arg}}.
303 @returns: A C{Radians} instance.
305 @raise Error: Invalid B{C{arg}} or B{C{abs(arg)}} outside the B{C{clip}}
306 range and L{pygeodesy.rangerrors} set to C{True}.
307 '''
308 if name_arg:
309 name, arg = _xkwds_item2(name_arg)
310 try:
311 return Float.__new__(cls, parseRad(arg, suffix=suffix, clip=clip),
312 Error=Error, name=name)
313 except Exception as x:
314 raise _Error(cls, arg, name, Error, x=x)
316 def toDegrees(self):
317 '''Convert C{Radians} to C{Degrees}.
318 '''
319 return Degrees(degrees(self), name=self.name)
321 def toRadians(self):
322 '''Convert C{Radians} to C{Radians}.
323 '''
324 return self
326 def toRepr(self, std=False, **prec_fmt_ints): # PYCHOK prec=8, ...
327 '''Return a representation of this C{Radians}.
329 @kwarg std: If C{True} return the standard C{repr},
330 otherwise the named representation (C{bool}).
332 @see: Methods L{Radians.toStr}, L{Float.toRepr} and function
333 L{pygeodesy.toDMS} for more documentation.
334 '''
335 return Float.toRepr(self, std=std, **prec_fmt_ints)
337 def toStr(self, prec=8, fmt=F__F, ints=False): # PYCHOK prec=8, ...
338 '''Return this C{Radians} as standard C{str}.
340 @see: Function L{pygeodesy.fstr} for keyword argument details.
341 '''
342 return fstr(self, prec=prec, fmt=fmt, ints=ints)
345class Radians_(Radians):
346 '''Named C{float} representing a coordinate in C{radians} with optional limits C{low} and C{high}.
347 '''
348 def __new__(cls, arg=None, name=_radians_, Error=UnitError, suffix=_NSEW_, low=_0_0, high=PI2, **name_arg):
349 '''New C{Radians_} instance.
351 @arg cls: This class (C{Radians_} or sub-class).
352 @kwarg arg: The value (any C{type} convertable to C{float} or parsable by
353 L{pygeodesy.parseRad}).
354 @kwarg name: Optional instance name (C{str}).
355 @kwarg Error: Optional error to raise, overriding the default L{UnitError}.
356 @kwarg suffix: Optional, valid compass direction suffixes (C{NSEW}).
357 @kwarg low: Optional lower B{C{arg}} limit (C{float} or C{None}).
358 @kwarg high: Optional upper B{C{arg}} limit (C{float} or C{None}).
359 @kwarg name_arg: Optional C{name=arg} keyword argument, inlieu of B{C{name}}
360 and B{C{arg}}.
362 @returns: A C{Radians_} instance.
364 @raise Error: Invalid B{C{arg}} or B{C{arg}} below B{C{low}} or above B{C{high}}.
365 '''
366 if name_arg:
367 name, arg = _xkwds_item2(name_arg)
368 self = Radians.__new__(cls, arg=arg, name=name, Error=Error, suffix=suffix, clip=0)
369 if (low is not None) and self < low:
370 txt = Fmt.limit(below=low)
371 elif (high is not None) and self > high:
372 txt = Fmt.limit(above=high)
373 else:
374 return self
375 raise _Error(cls, arg, name, Error, txt=txt)
378class Radians2(Float_):
379 '''Named C{float} representing a distance in C{radians squared}.
380 '''
381 def __new__(cls, arg=None, name=_radians2_, **Error_name_arg):
382 '''New L{Radians2} instance, see L{Float_}.
383 '''
384 return Float_.__new__(cls, arg=arg, name=name, low=_0_0, **Error_name_arg)
387class Bearing(Degrees):
388 '''Named C{float} representing a bearing in compass C{degrees} from (true) North.
389 '''
390 _ddd_ = 1
391 _suf_ = _N_ * 3 # always suffix N
393 def __new__(cls, arg=None, name=_bearing_, Error=UnitError, clip=0, **name_arg):
394 '''New L{Bearing} instance, see L{Degrees}.
395 '''
396 if name_arg:
397 name, arg = _xkwds_item2(name_arg)
398 d = Degrees.__new__(cls, arg=arg, name=name, Error=Error, suffix=_N_, clip=clip)
399 b = _umod_360(d) # 0 <= b < 360
400 return d if b == d else Degrees.__new__(cls, arg=b, name=name, Error=Error)
403class Bearing_(Radians):
404 '''Named C{float} representing a bearing in C{radians} from compass C{degrees} from (true) North.
405 '''
406 def __new__(cls, arg=None, name=_bearing_, clip=0, **Error_name_arg):
407 '''New L{Bearing_} instance, see L{Bearing} and L{Radians}.
408 '''
409 d = Bearing.__new__(cls, arg=arg, name=name, clip=clip, **Error_name_arg)
410 return Radians.__new__(cls, radians(d), name=name)
413class Distance(Float):
414 '''Named C{float} representing a distance, conventionally in C{meter}.
415 '''
416 def __new__(cls, arg=None, name=_distance_, **Error_name_arg):
417 '''New L{Distance} instance, see L{Float}.
418 '''
419 return Float.__new__(cls, arg=arg, name=name, **Error_name_arg)
422class Distance_(Float_):
423 '''Named C{float} with optional C{low} and C{high} limits representing a distance, conventionally in C{meter}.
424 '''
425 def __new__(cls, arg=None, name=_distance_, **low_high_Error_name_arg):
426 '''New L{Distance_} instance, see L{Float}.
427 '''
428 return Float_.__new__(cls, arg=arg, name=name, **low_high_Error_name_arg)
431class Easting(Float):
432 '''Named C{float} representing an easting, conventionally in C{meter}.
433 '''
434 def __new__(cls, arg=None, name=_easting_, Error=UnitError, falsed=False, high=None, **name_arg):
435 '''New named C{Easting} or C{Easting of Point} instance.
437 @arg cls: This class (C{Easting} or sub-class).
438 @kwarg arg: The value (any C{type} convertable to C{float}).
439 @kwarg name: Optional instance name (C{str}).
440 @kwarg Error: Optional error to raise, overriding the default L{UnitError}.
441 @kwarg falsed: The B{C{arg}} value includes false origin (C{bool}).
442 @kwarg high: Optional upper B{C{arg}} easting limit (C{scalar} or C{None}).
443 @kwarg name_arg: Optional C{name=arg} keyword argument, inlieu of B{C{name}}
444 and B{C{arg}}.
446 @returns: An C{Easting} instance.
448 @raise Error: Invalid B{C{arg}}, above B{C{high}} or negative, falsed B{C{arg}}.
449 '''
450 if name_arg:
451 name, arg = _xkwds_item2(name_arg)
452 self = Float.__new__(cls, arg=arg, name=name, Error=Error)
453 if high and (self < 0 or self > high): # like Veness
454 raise _Error(cls, arg, name, Error)
455 elif falsed and self < 0:
456 raise _Error(cls, arg, name, Error, txt=_negative_falsed_)
457 return self
460class Epoch(Float_): # in .ellipsoidalBase, .trf
461 '''Named C{epoch} with optional C{low} and C{high} limits representing a fractional
462 calendar year.
463 '''
464 _std_repr = False
466 def __new__(cls, arg=None, name=_epoch_, Error=TRFError, low=1900, high=9999, **name_arg):
467 '''New L{Epoch} instance, see L{Float_}.
468 '''
469 if name_arg:
470 name, arg = _xkwds_item2(name_arg)
471 return arg if isinstance(arg, Epoch) else Float_.__new__(cls,
472 arg=arg, name=name, Error=Error, low=low, high=high)
474 def toRepr(self, prec=3, std=False, **unused): # PYCHOK fmt=Fmt.F, ints=True
475 '''Return a representation of this C{Epoch}.
477 @kwarg std: Use the standard C{repr} or the named
478 representation (C{bool}).
480 @see: Method L{Float.toRepr} for more documentation.
481 '''
482 return Float_.toRepr(self, prec=prec, std=std) # fmt=Fmt.F, ints=True
484 def toStr(self, prec=3, **unused): # PYCHOK fmt=Fmt.F, ints=True
485 '''Format this C{Epoch} as C{str}.
487 @see: Function L{pygeodesy.fstr} for more documentation.
488 '''
489 return fstr(self, prec=prec, fmt=Fmt.F, ints=True)
491 __str__ = toStr # PYCHOK default '%.3F', with trailing zeros and decimal point
494class Feet(Float):
495 '''Named C{float} representing a distance or length in C{feet}.
496 '''
497 def __new__(cls, arg=None, name=_feet_, **Error_name_arg):
498 '''New L{Feet} instance, see L{Float}.
499 '''
500 return Float.__new__(cls, arg=arg, name=name, **Error_name_arg)
503class FIx(Float_):
504 '''A named I{Fractional Index}, an C{int} or C{float} index into
505 a C{list} or C{tuple} of C{points}, typically. A C{float}
506 I{Fractional Index} C{fi} represents a location on the edge
507 between C{points[int(fi)]} and C{points[(int(fi) + 1) %
508 len(points)]}.
509 '''
510 _fin = 0
512 def __new__(cls, fi, fin=None, **name_Error):
513 '''New I{Fractional Index} in a C{list} or C{tuple} of points.
515 @arg fi: The fractional index (C{float} or C{int}).
516 @kwarg fin: Optional C{len}, the number of C{points}, the index
517 C{[n]} wrapped to C{[0]} (C{int} or C{None}).
518 @kwarg name_Error: Optional keyword argument C{B{name}=NN}
519 and C{B{Error}=UnitError}.
521 @return: The B{C{fi}} (named L{FIx}).
523 @note: The returned B{C{fi}} may exceed the B{C{flen}} of
524 the original C{points} in certain open/closed cases.
526 @see: Method L{fractional} or function L{pygeodesy.fractional}.
527 '''
528 n = Int_(fin=fin, low=0) if fin else None
529 f = Float_.__new__(cls, fi, low=_0_0, high=n, **name_Error)
530 i = int(f)
531 r = f - float(i)
532 if r < EPS: # see .points._fractional
533 f = Float_.__new__(cls, i, low=_0_0)
534 elif r > EPS1:
535 f = Float_.__new__(cls, i + 1, high=n, **name_Error)
536 if n: # non-zero and non-None
537 f._fin = n
538 return f
540 @Property_RO
541 def fin(self):
542 '''Get the given C{len}, the index C{[n]} wrapped to C{[0]} (C{int}).
543 '''
544 return self._fin
546 def fractional(self, points, wrap=None, LatLon=None, Vector=None, **kwds):
547 '''Return the point at this I{Fractional Index}.
549 @arg points: The points (C{LatLon}[], L{Numpy2LatLon}[],
550 L{Tuple2LatLon}[] or C{other}[]).
551 @kwarg wrap: If C{True}, wrap or I{normalize} and unroll the
552 B{C{points}} (C{bool}) C{None} for backward
553 compatible L{LatLon2Tuple} or B{C{LatLon}} with
554 I{averaged} lat- and longitudes.
555 @kwarg LatLon: Optional class to return the I{intermediate},
556 I{fractional} point (C{LatLon}) or C{None}.
557 @kwarg Vector: Optional class to return the I{intermediate},
558 I{fractional} point (C{Cartesian}, C{Vector3d})
559 or C{None}.
560 @kwarg kwds: Optional, additional B{C{LatLon}} I{or} B{C{Vector}}
561 keyword arguments, ignored if both C{B{LatLon}} and
562 C{B{Vector}} are C{None}.
564 @return: See function L{pygeodesy.fractional}.
566 @raise IndexError: This fractional index invalid or B{C{points}}
567 not subscriptable or not closed.
569 @raise TypeError: Invalid B{C{LatLon}}, B{C{Vector}} or B{C{kwds}}
570 argument.
572 @see: Function L{pygeodesy.fractional}.
573 '''
574 # fi = 0 if self == self.fin else self
575 return _MODS.points.fractional(points, self, wrap=wrap,
576 LatLon=LatLon, Vector=Vector, **kwds)
579def _fi_j2(f, n): # PYCHOK in .ellipsoidalBaseDI, .vector3d
580 # Get 2-tuple (C{fi}, C{j})
581 i = int(f) # like L{FIx}
582 if not 0 <= i < n:
583 raise _AssertionError(i=i, n=n, f=f, r=f - float(i))
584 return FIx(fi=f, fin=n), (i + 1) % n
587class Height(Float): # here to avoid circular import
588 '''Named C{float} representing a height, conventionally in C{meter}.
589 '''
590 def __new__(cls, arg=None, name=_height_, **Error_name_arg):
591 '''New L{Height} instance, see L{Float}.
592 '''
593 return Float.__new__(cls, arg=arg, name=name, **Error_name_arg)
596class Height_(Float_): # here to avoid circular import
597 '''Named C{float} with optional C{low} and C{high} limits representing a height, conventionally in C{meter}.
598 '''
599 def __new__(cls, arg=None, name=_height_, **low_high_Error_name_arg):
600 '''New L{Height} instance, see L{Float}.
601 '''
602 return Float_.__new__(cls, arg=arg, name=name, **low_high_Error_name_arg)
605class HeightX(Height):
606 '''Like L{Height}, used to distinguish the interpolated height
607 from an original L{Height} at a clip intersection.
608 '''
609 pass
612def _heigHt(inst, height):
613 '''(INTERNAL) Override the C{inst}ance' height.
614 '''
615 return inst.height if height is None else Height(height)
618class Lam(Radians):
619 '''Named C{float} representing a longitude in C{radians}.
620 '''
621 def __new__(cls, arg=None, name=_lam_, clip=PI, **Error_name_arg):
622 '''New L{Lam} instance, see L{Radians}.
623 '''
624 return Radians.__new__(cls, arg=arg, name=name, suffix=_EW_, clip=clip, **Error_name_arg)
627class Lam_(Lam):
628 '''Named C{float} representing a longitude in C{radians} converted from C{degrees}.
629 '''
630 def __new__(cls, arg=None, name=_lon_, Error=UnitError, clip=180, **name_arg):
631 '''New L{Lam_} instance, see L{Lam} and L{Radians}.
632 '''
633 if name_arg:
634 name, arg = _xkwds_item2(name_arg)
635 d = Lam.__new__(cls, arg=arg, name=name, Error=Error, clip=clip)
636 return Radians.__new__(cls, radians(d), name=name, Error=Error)
639class Lat(Degrees):
640 '''Named C{float} representing a latitude in C{degrees}.
641 '''
642 _ddd_ = 2
643 _suf_ = _S_, S_NUL, _N_ # no zero suffix
645 def __new__(cls, arg=None, name=_lat_, clip=90, **Error_name_arg):
646 '''New L{Lat} instnace, see L{Degrees}.
647 '''
648 return Degrees.__new__(cls, arg=arg, name=name, suffix=_NS_, clip=clip, **Error_name_arg)
651class Lat_(Degrees_):
652 '''Named C{float} representing a latitude in C{degrees} within limits C{low} and C{high}.
653 '''
654 _ddd_ = 2
655 _suf_ = _S_, S_NUL, _N_ # no zero suffix
657 def __new__(cls, arg=None, name=_lat_, low=-90, high=90, **Error_name_arg):
658 '''See L{Degrees_}.
659 '''
660 return Degrees_.__new__(cls, arg=arg, name=name, suffix=_NS_, low=low, high=high, **Error_name_arg)
663class Lon(Degrees):
664 '''Named C{float} representing a longitude in C{degrees}.
665 '''
666 _ddd_ = 3
667 _suf_ = _W_, S_NUL, _E_ # no zero suffix
669 def __new__(cls, arg=None, name=_lon_, clip=180, **Error_name_arg):
670 '''New L{Lon} instance, see L{Degrees}.
671 '''
672 return Degrees.__new__(cls, arg=arg, name=name, suffix=_EW_, clip=clip, **Error_name_arg)
675class Lon_(Degrees_):
676 '''Named C{float} representing a longitude in C{degrees} within limits C{low} and C{high}.
677 '''
678 _ddd_ = 3
679 _suf_ = _W_, S_NUL, _E_ # no zero suffix
681 def __new__(cls, arg=None, name=_lon_, low=-180, high=180, **Error_name_arg):
682 '''New L{Lon_} instance, see L{Lon} and L{Degrees_}.
683 '''
684 return Degrees_.__new__(cls, arg=arg, name=name, suffix=_EW_, low=low, high=high, **Error_name_arg)
687class Meter(Float):
688 '''Named C{float} representing a distance or length in C{meter}.
689 '''
690 def __new__(cls, arg=None, name=_meter_, **Error_name_arg):
691 '''New L{Meter} instance, see L{Float}.
692 '''
693 return Float.__new__(cls, arg=arg, name=name, **Error_name_arg)
695 def __repr__(self):
696 '''Return a representation of this C{Meter}.
698 @see: Method C{Str.toRepr} and property C{Str.std_repr}.
700 @note: Use C{env} variable C{PYGEODESY_METER_STD_REPR=std}
701 prior to C{import pygeodesy} to get the standard
702 C{repr} or set property C{std_repr=False} to always
703 get the named C{toRepr} representation.
704 '''
705 return self.toRepr(std=self._std_repr)
708# _1Å = Meter( _Å= 1e-10) # PyCHOK 1 Ångstrōm
709_1um = Meter( _1um= 1.e-6) # PYCHOK 1 micrometer in .mgrs
710_10um = Meter( _10um= 1.e-5) # PYCHOK 10 micrometer in .osgr
711_1mm = Meter( _1mm=_0_001) # PYCHOK 1 millimeter in .ellipsoidal...
712_100km = Meter( _100km= 1.e+5) # PYCHOK 100 kilometer in .formy, .mgrs, .osgr, .sphericalBase
713_2000km = Meter(_2000km= 2.e+6) # PYCHOK 2,000 kilometer in .mgrs
716class Meter_(Float_):
717 '''Named C{float} representing a distance or length in C{meter}.
718 '''
719 def __new__(cls, arg=None, name=_meter_, low=_0_0, **high_Error_name_arg):
720 '''New L{Meter_} instance, see L{Meter} and L{Float_}.
721 '''
722 return Float_.__new__(cls, arg=arg, name=name, low=low, **high_Error_name_arg)
725class Meter2(Float_):
726 '''Named C{float} representing an area in C{meter squared}.
727 '''
728 def __new__(cls, arg=None, name=_meter2_, Error=UnitError, **name_arg):
729 '''New L{Meter2} instance, see L{Float_}.
730 '''
731 return Float_.__new__(cls, arg=arg, name=name, Error=Error, low=_0_0, **name_arg)
734class Meter3(Float_):
735 '''Named C{float} representing a volume in C{meter cubed}.
736 '''
737 def __new__(cls, arg=None, name='meter3', **Error_name_arg):
738 '''New L{Meter3} instance, see L{Float_}.
739 '''
740 return Float_.__new__(cls, arg=arg, name=name, low=_0_0, **Error_name_arg)
743class Northing(Float):
744 '''Named C{float} representing a northing, conventionally in C{meter}.
745 '''
746 def __new__(cls, arg=None, name=_northing_, Error=UnitError, falsed=False, high=None, **name_arg):
747 '''New C{Northing} or C{Northing of point} instance.
749 @arg cls: This class (C{Northing} or sub-class).
750 @kwarg arg: The value (any C{type} convertable to C{float}).
751 @kwarg name: Optional instance name (C{str}).
752 @kwarg Error: Optional error to raise, overriding the default L{UnitError}.
753 @kwarg falsed: The B{C{arg}} value includes false origin (C{bool}).
754 @kwarg high: Optional upper B{C{arg}} northing limit (C{scalar} or C{None}).
755 @kwarg name_arg: Optional C{name=arg} keyword argument, inlieu of B{C{name}}
756 and B{C{arg}}.
758 @returns: A C{Northing} instance.
760 @raise Error: Invalid B{C{arg}}, above B{C{high}} or negative, falsed B{C{arg}}.
761 '''
762 if name_arg:
763 name, arg = _xkwds_item2(name_arg)
764 self = Float.__new__(cls, arg=arg, name=name, Error=Error)
765 if high and (self < 0 or self > high):
766 raise _Error(cls, arg, name, Error)
767 elif falsed and self < 0:
768 raise _Error(cls, arg, name, Error, txt=_negative_falsed_)
769 return self
772class Number_(Int_):
773 '''Named C{int} representing a non-negative number.
774 '''
775 def __new__(cls, arg=None, name=_number_, **low_high_Error_name_arg):
776 '''New L{Number_} instance, see L{Int_}.
777 '''
778 return Int_.__new__(cls, arg=arg, name=name, **low_high_Error_name_arg)
781class Phi(Radians):
782 '''Named C{float} representing a latitude in C{radians}.
783 '''
784 def __new__(cls, arg=None, name=_phi_, clip=PI_2, **Error_name_arg):
785 '''New L{Phi} instance, see L{Radians}.
786 '''
787 return Radians.__new__(cls, arg=arg, name=name, suffix=_NS_, clip=clip, **Error_name_arg)
790class Phi_(Phi):
791 '''Named C{float} representing a latitude in C{radians} converted from C{degrees}.
792 '''
793 def __new__(cls, arg=None, name=_lat_, Error=UnitError, clip=90, **name_arg):
794 '''New L{Phi_} instance, see L{Phi} and L{Radians}.
795 '''
796 if name_arg:
797 name, arg = _xkwds_item2(name_arg)
798 d = Phi.__new__(cls, arg=arg, name=name, Error=Error, clip=clip)
799 return Radians.__new__(cls, arg=radians(d), name=name, Error=Error)
802class Precision_(Int_):
803 '''Named C{int} with optional C{low} and C{high} limits representing a precision.
804 '''
805 def __new__(cls, arg=None, name=_precision_, **low_high_Error_name_arg):
806 '''New L{Precision_} instance, see L{Int_}.
807 '''
808 return Int_.__new__(cls, arg=arg, name=name, **low_high_Error_name_arg)
811class Radius_(Float_):
812 '''Named C{float} with optional C{low} and C{high} limits representing a radius, conventionally in C{meter}.
813 '''
814 def __new__(cls, arg=None, name=_radius_, **low_high_Error_name_arg):
815 '''New L{Radius_} instance, see L{Radius} and L{Float}.
816 '''
817 return Float_.__new__(cls, arg=arg, name=name, **low_high_Error_name_arg)
820class Scalar(Float):
821 '''Named C{float} representing a factor, fraction, scale, etc.
822 '''
823 def __new__(cls, arg=None, name=_scalar_, **Error_name_arg):
824 '''New L{Scalar} instance, see L{Float}.
825 '''
826 return Float.__new__(cls, arg=arg, name=name, **Error_name_arg)
829class Scalar_(Float_):
830 '''Named C{float} with optional C{low} and C{high} limits representing a factor, fraction, scale, etc.
831 '''
832 def __new__(cls, arg=None, name=_scalar_, low=_0_0, **high_Error_name_arg):
833 '''New L{Scalar_} instance, see L{Scalar} and L{Float_}.
834 '''
835 return Float_.__new__(cls, arg=arg, name=name, low=low, **high_Error_name_arg)
838class Zone(Int):
839 '''Named C{int} representing a UTM/UPS zone number.
840 '''
841 def __new__(cls, arg=None, name=_zone_, **Error_name_arg):
842 '''New L{Zone} instance, see L{Int}
843 '''
844 # usually low=_UTMUPS_ZONE_MIN, high=_UTMUPS_ZONE_MAX
845 return Int_.__new__(cls, arg=arg, name=name, **Error_name_arg)
848_Scalars = Float, Float_, Scalar, Scalar_
849_Degrees = (Bearing, Bearing_, Degrees, Degrees_) + _Scalars
850_Meters = (Distance, Distance_, Meter, Meter_) + _Scalars
851_Radians = (Radians, Radians_) + _Scalars # PYCHOK unused
852_Radii = _Meters + (Radius, Radius_)
855def _isDegrees(obj):
856 # Check for valid degrees types.
857 return isinstance(obj, _Degrees) or _isScalar(obj)
860def _isHeight(obj):
861 # Check for valid heigth types.
862 return isinstance(obj, _Meters) or _isScalar(obj)
865def _isMeter(obj):
866 # Check for valid meter types.
867 return isinstance(obj, _Meters) or _isScalar(obj)
870def _isRadius(obj):
871 # Check for valid earth radius types.
872 return isinstance(obj, _Radii) or _isScalar(obj)
875def _isScalar(obj):
876 # Check for pure scalar types.
877 return isscalar(obj) and not isinstance(obj, _NamedUnit)
880def _toDegrees(s, *xs, **toDMS_kwds):
881 '''(INTERNAL) Convert C{xs} from C{Radians} to C{Degrees} or C{toDMS}.
882 '''
883 if toDMS_kwds:
884 toDMS_kwds = _xkwds(toDMS_kwds, ddd=1, pos=NN)
886 for x in xs:
887 if not isinstanceof(x, Degrees, Degrees_):
888 s = None
889 x = x.toDegrees()
890 yield toDMS(x, **toDMS_kwds) if toDMS_kwds else x
891 yield None if toDMS_kwds else s
894def _toRadians(s, *xs):
895 '''(INTERNAL) Convert C{xs} from C{Degrees} to C{Radians}.
896 '''
897 for x in xs:
898 if not isinstanceof(x, Radians, Radians_):
899 s = None
900 x = x.toRadians()
901 yield x
902 yield s
905def _xStrError(*Refs, **name_value_Error):
906 '''(INTERNAL) Create a C{TypeError} for C{Garef}, C{Geohash}, C{Wgrs}.
907 '''
908 r = tuple(r.__name__ for r in Refs) + (Str.__name__, _LatLon_, 'LatLon*Tuple')
909 return _IsnotError(*r, **name_value_Error)
912def _xUnit(units, Base): # in .frechet, .hausdorff
913 '''(INTERNAL) Get C{Unit} from C{Unit} or C{name}, ortherwise C{Base}.
914 '''
915 if not issubclassof(Base, _NamedUnit):
916 raise _IsnotError(_NamedUnit.__name__, Base=Base)
917 U = globals().get(units.capitalize(), Base) if isstr(units) else (
918 units if issubclassof(units, Base) else Base)
919 return U if issubclassof(U, Base) else Base
922def _xUnits(units, Base=_NamedUnit): # in .frechet, .hausdorff
923 '''(INTERNAL) Set property C{units} as C{Unit} or C{Str}.
924 '''
925 if not issubclassof(Base, _NamedUnit):
926 raise _IsnotError(_NamedUnit.__name__, Base=Base)
927 elif issubclassof(units, Base):
928 return units
929 elif isstr(units):
930 return Str(units, name=_units_) # XXX Str to _Pass and for backward compatibility
931 else:
932 raise _IsnotError(Base.__name__, Str.__name__, str.__name__, units=units)
935def _std_repr(*Classes):
936 '''(INTERNAL) Use standard C{repr} or named C{toRepr}.
937 '''
938 for C in Classes:
939 if hasattr(C, _std_repr.__name__): # PYCHOK del _std_repr
940 env = 'PYGEODESY_%s_STD_REPR' % (C.__name__.upper(),)
941 if _getenv(env, _std_).lower() != _std_:
942 C._std_repr = False
944_std_repr(Bearing, Bool, Degrees, Float, Int, Meter, Radians, Str) # PYCHOK expected
945del _std_repr
947__all__ += _ALL_DOCS(_NamedUnit)
949# **) MIT License
950#
951# Copyright (C) 2016-2024 -- mrJean1 at Gmail -- All Rights Reserved.
952#
953# Permission is hereby granted, free of charge, to any person obtaining a
954# copy of this software and associated documentation files (the "Software"),
955# to deal in the Software without restriction, including without limitation
956# the rights to use, copy, modify, merge, publish, distribute, sublicense,
957# and/or sell copies of the Software, and to permit persons to whom the
958# Software is furnished to do so, subject to the following conditions:
959#
960# The above copyright notice and this permission notice shall be included
961# in all copies or substantial portions of the Software.
962#
963# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
964# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
965# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
966# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
967# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
968# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
969# OTHER DEALINGS IN THE SOFTWARE.