1
2# -*- coding: utf-8 -*-
3
4u'''Various utility functions.
5
6After I{(C) Chris Veness 2011-2015} published under the same MIT Licence**, see
7U{Latitude/Longitude<https://www.Movable-Type.co.UK/scripts/latlong.html>} and
8U{Vector-based geodesy<https://www.Movable-Type.co.UK/scripts/latlong-vectors.html>}.
9'''
10# make sure int/int division yields float quotient, see .basics
11from __future__ import division
12
13from pygeodesy.basics import copysign0, isint, isnear0
14from pygeodesy.interns import EPS, EPS0, INF, PI, PI2, PI_2, R_M, \
15                             _edge_, _radians_, _semi_circular_, _SPACE_, \
16                             _0_0, _0_5, _1_0, _90_0, _180_0, _360_0, _400_0
17from pygeodesy.lazily import _ALL_LAZY
18from pygeodesy.units import Feet, Float, Lam, Lam_, Meter
19
20from math import acos, asin, atan2, cos, degrees, radians, sin, tan  # pow
21
22__all__ = _ALL_LAZY.utily
23__version__ = '21.08.30'
24
25# <https://Numbers.Computation.Free.FR/Constants/Miscellaneous/digits.html>
26_1__90 = _1_0 / _90_0  # 0.01111111111111111111111111111111111111111111111111
27_2__PI = _1_0 /  PI_2  # 0.63661977236758134307553505349005744813783858296182
28# sqrt(2) + 1 <https://WikiPedia.org/wiki/Square_root_of_2>
29# _1sqrt2 = 2.41421356237309504880  # _16887_24209_69807_85696_71875_37694_80731_76679_73799
30
31
32def acos1(x):
33    '''Return C{math.acos(max(-1, min(1, B{x})))}.
34    '''
35    return acos(x) if abs(x) < _1_0 else (PI if x < 0 else _0_0)
36
37
38def acre2ha(acres):
39    '''Convert acres to hectare.
40
41       @arg acres: Value in acres (C{scalar}).
42
43       @return: Value in C{hectare} (C{float}).
44
45       @raise ValueError: Invalid B{C{acres}}.
46    '''
47    # 0.40468564224 == acre2m2(1) / 10_000
48    return Float(acres) * 0.40468564224
49
50
51def acre2m2(acres):
52    '''Convert acres to I{square} meter.
53
54       @arg acres: Value in acres (C{scalar}).
55
56       @return: Value in C{meter^2} (C{float}).
57
58       @raise ValueError: Invalid B{C{acres}}.
59    '''
60    # 4046.8564224 == chain2m(1) * furlong2m(1)
61    return Float(acres) * 4046.8564224
62
63
64def asin1(x):
65    '''Return C{math.asin(max(-1, min(1, B{x})))}.
66    '''
67    return asin(x) if abs(x) < _1_0 else (-PI_2 if x < 0 else PI_2)  # -PI_2, not PI3_2!
68
69
70def atand(y_x):
71    '''Return C{atan(B{y_x})} angle in C{degrees}.
72
73       @see: Function L{atan2d}.
74    '''
75    return atan2d(y_x, _1_0)
76
77
78def atan2b(y, x):
79    '''Return C{atan2(B{y}, B{x})} in degrees M{[0..+360]}.
80
81       @see: Function L{atan2d}.
82    '''
83    d = atan2d(y, x)
84    if d < 0:
85        d += _360_0
86    return d
87
88
89def atan2d(y, x, reverse=False):
90    '''Return C{atan2(B{y}, B{x})} in degrees M{[-180..+180]},
91       optionally reversed (by 180 degrees for C{azi2}).
92
93       @see: I{Karney}'s C++ function U{Math.atan2d
94             <https://GeographicLib.SourceForge.io/html/classGeographicLib_1_1Math.html>}.
95    '''
96    if abs(y) > abs(x) > 0:
97        if y < 0:  # q = 3
98            d = degrees(atan2(x, -y)) - _90_0
99        else:  # q = 2
100            d = _90_0 - degrees(atan2(x, y))
101    elif x < 0:  # q = 1
102        d = copysign0(_180_0, y) - degrees(atan2(y, -x))
103    elif x > 0:  # q = 0
104        d = degrees(atan2(y, x)) if y else _0_0
105    else:  # x == 0
106        d = -_90_0 if y < 0 else (_90_0 if y > 0 else _0_0)
107    if reverse:
108        d += _180_0 if d < 0 else -_180_0
109    return d
110
111
112def chain2m(chains):
113    '''Convert I{UK} chains to meter.
114
115       @arg chains: Value in chains (C{scalar}).
116
117       @return: Value in C{meter} (C{float}).
118
119       @raise ValueError: Invalid B{C{chains}}.
120    '''
121    # 20.1168 = 22 * yard2m(1)
122    return Float(chains) * 20.1168
123
124
125def circle4(earth, lat):
126    '''Get the equatorial or a parallel I{circle of latitude}.
127
128       @arg earth: The earth radius, ellipsoid or datum
129                   (C{meter}, L{Ellipsoid}, L{Ellipsoid2},
130                   L{Datum} or L{a_f2Tuple}).
131       @arg lat: Geodetic latitude (C{degrees90}, C{str}).
132
133       @return: A L{Circle4Tuple}C{(radius, height, lat, beta)}
134                instance.
135
136       @raise RangeError: Latitude B{C{lat}} outside valid range
137                          and L{rangerrors} set to C{True}.
138
139       @raise TypeError: Invalid B{C{earth}}.
140
141       @raise ValueError: B{C{earth}} or B{C{lat}}.
142    '''
143    from pygeodesy.datums import _spherical_datum
144    E = _spherical_datum(earth).ellipsoid
145    return E.circle4(lat)
146
147
148def cotd(deg, **error_kwds):
149    '''Return the C{cotangent} of an angle in C{degrees}.
150
151       @arg deg: Angle (C{degrees}).
152       @kwarg error_kwds: Error to raise (C{ValueError}).
153
154       @return: C{cot(B{deg})}.
155
156       @raise ValueError: L{pygeodesy.isnear0}C{(sin(B{deg})}.
157    '''
158    s, c = sincos2d(deg)
159    if isnear0(s):
160        from pygeodesy.errors import _ValueError, _xkwds
161        raise _ValueError(**_xkwds(error_kwds, cotd=deg))
162    return c / s
163
164
165def cotd_(*degs, **error_kwds):
166    '''Return the C{cotangent} of angle(s) in C{degrees}.
167
168       @arg degs: One or more angles (C{degrees}).
169       @kwarg error_kwds: Error to raise (C{ValueError}).
170
171       @return: Yield the C{cot(B{deg})} for each angle.
172
173       @raise ValueError: See L{pygeodesy.cotd}.
174    '''
175    for d in degs:
176        yield cotd(d, **error_kwds)
177
178
179def degrees90(rad):
180    '''Convert radians to degrees and wrap M{[-270..+90]}.
181
182       @arg rad: Angle (C{radians}).
183
184       @return: Angle, wrapped (C{degrees90}).
185    '''
186    return _wrap(degrees(rad), _90_0, _360_0)
187
188
189def degrees180(rad):
190    '''Convert radians to degrees and wrap M{[-180..+180]}.
191
192       @arg rad: Angle (C{radians}).
193
194       @return: Angle, wrapped (C{degrees180}).
195    '''
196    return _wrap(degrees(rad), _180_0, _360_0)
197
198
199def degrees360(rad):
200    '''Convert radians to degrees and wrap M{[0..+360)}.
201
202       @arg rad: Angle (C{radians}).
203
204       @return: Angle, wrapped (C{degrees360}).
205    '''
206    return _wrap(degrees(rad), _360_0, _360_0)
207
208
209def degrees2grades(deg):
210    '''Convert degrees to I{grades} (aka I{gradians} or I{gons}).
211
212       @arg deg: Angle (C{degrees}).
213
214       @return: Angle (C{grades}).
215    '''
216    return deg * _400_0 / _360_0
217
218
219def degrees2m(deg, radius=R_M, lat=0):
220    '''Convert an angle to a distance along the equator or
221       along the parallel at an other (geodetic) latitude.
222
223       @arg deg: The angle (C{degrees}).
224       @kwarg radius: Mean earth radius, ellipsoid or datum
225                      (C{meter}, L{Ellipsoid}, L{Ellipsoid2},
226                      L{Datum} or L{a_f2Tuple}).
227       @kwarg lat: Parallel latitude (C{degrees90}, C{str}).
228
229       @return: Distance (C{meter}, same units as B{C{radius}}
230                or ellipsoidal and polar radii) or C{0} for
231                near-polar B{C{lat}}.
232
233       @raise RangeError: Latitude B{C{lat}} outside valid range
234                          and L{rangerrors} set to C{True}.
235
236       @raise TypeError: Invalid B{C{radius}}.
237
238       @raise ValueError: Invalid B{C{deg}}, B{C{radius}} or
239                          B{C{lat}}.
240
241       @see: Function L{radians2m} and L{m2degrees}.
242    '''
243    return radians2m(Lam_(deg=deg, clip=0), radius=radius, lat=lat)
244
245
246def fathom2m(fathoms):
247    '''Convert I{UK} fathom to meter.
248
249       @arg fathoms: Value in fathoms (C{scalar}).
250
251       @return: Value in C{meter} (C{float}).
252
253       @raise ValueError: Invalid B{C{fathoms}}.
254    '''
255    # 1.8288 == 2 * yard2m(1)
256    return Float(fathoms) * 1.8288
257
258
259def ft2m(feet, usurvey=False):
260    '''Convert I{International} or I{US Survey} feet to meter.
261
262       @arg feet: Value in feet (C{scalar}).
263       @kwarg usurvey: Convert I{US Survey} feet (C{bool}),
264                       I{International} feet otherwise.
265
266       @return: Value in C{meter} (C{float}).
267
268       @raise ValueError: Invalid B{C{feet}}.
269    '''
270    # US Survey 1200 / 3937 == 0.3048006096012192
271    # Int'l 0.3048 == 254 * 12 / 10_000
272    return Feet(feet) * (0.3048006096 if usurvey else 0.3048)
273
274
275def furlong2m(furlongs):
276    '''Convert a I{UK} furlong to meter.
277
278       @arg furlongs: Value in furlongs (C{scalar}).
279
280       @return: Value in C{meter} (C{float}).
281
282       @raise ValueError: Invalid B{C{furlongs}}.
283    '''
284    # 201.168 = 220 * yard2m(1)
285    return Float(furlongs) * 201.168
286
287
288def grades(rad):
289    '''Convert radians to I{grades} (aka I{gradians} or I{gons}).
290
291       @arg rad: Angle (C{radians}).
292
293       @return: Angle (C{grades}).
294    '''
295    return rad * _400_0 / PI2
296
297
298def grades400(rad):
299    '''Convert radians to I{grades} (aka I{gradians} or I{gons}) and wrap M{[0..+400)}.
300
301       @arg rad: Angle (C{radians}).
302
303       @return: Angle, wrapped (C{grades}).
304    '''
305    return _wrap(grades(rad), _400_0, _400_0)
306
307
308def grades2degrees(gon):
309    '''Convert I{grades} (aka I{gradians} or I{gons}) to C{degrees}.
310
311       @arg gon: Angle (C{grades}).
312
313       @return: Angle (C{degrees}).
314    '''
315    return gon * _360_0 / _400_0
316
317
318def grades2radians(gon):
319    '''Convert I{grades} (aka I{gradians} or I{gons}) to C{radians}.
320
321       @arg gon: Angle (C{grades}).
322
323       @return: Angle (C{radians}).
324    '''
325    return gon * PI2 / _400_0
326
327
328def m2degrees(distance, radius=R_M, lat=0):
329    '''Convert a distance to an angle along the equator or
330       along the parallel at an other (geodetic) latitude.
331
332       @arg distance: Distance (C{meter}, same units as B{C{radius}}).
333       @kwarg radius: Mean earth radius, ellipsoid or datum (C{meter},
334                      an L{Ellipsoid}, L{Ellipsoid2}, L{Datum} or
335                      L{a_f2Tuple}).
336       @kwarg lat: Parallel latitude (C{degrees90}, C{str}).
337
338       @return: Angle (C{degrees}) or C{INF} for near-polar B{C{lat}}.
339
340       @raise RangeError: Latitude B{C{lat}} outside valid range
341                          and L{rangerrors} set to C{True}.
342
343       @raise TypeError: Invalid B{C{radius}}.
344
345       @raise ValueError: Invalid B{C{distance}}, B{C{radius}}
346                          or B{C{lat}}.
347
348       @see: Function L{m2radians} and L{degrees2m}.
349    '''
350    return degrees(m2radians(distance, radius=radius, lat=lat))
351
352
353def m2ft(meter, usurvey=False):
354    '''Convert meter to I{International} or I{US Survey} feet (C{ft}).
355
356       @arg meter: Value in meter (C{scalar}).
357       @kwarg usurvey: Convert to I{US Survey} feet (C{bool}),
358                       I{International} feet otherwise.
359
360       @return: Value in C{feet} (C{float}).
361
362       @raise ValueError: Invalid B{C{meter}}.
363    '''
364    # US Survey == 3937 / 1200  == 3.2808333333333333
365    # Int'l 10_000 / (254 * 12) == 3.2808398950131235
366    return Meter(meter) * (3.280833333 if usurvey else 3.280839895)
367
368
369def m2km(meter):
370    '''Convert meter to kilo meter (km).
371
372       @arg meter: Value in meter (C{scalar}).
373
374       @return: Value in km (C{float}).
375
376       @raise ValueError: Invalid B{C{meter}}.
377    '''
378    return Meter(meter) * 1.0e-3
379
380
381def m2NM(meter):
382    '''Convert meter to nautical miles (NM).
383
384       @arg meter: Value in meter (C{scalar}).
385
386       @return: Value in NM (C{float}).
387
388       @raise ValueError: Invalid B{C{meter}}.
389    '''
390    return Meter(meter) * 5.39956804e-4  # == * _1_0 / 1852
391
392
393def m2radians(distance, radius=R_M, lat=0):
394    '''Convert a distance to an angle along the equator or
395       along the parallel at an other (geodetic) latitude.
396
397       @arg distance: Distance (C{meter}, same units as B{C{radius}}).
398       @kwarg radius: Mean earth radius, ellipsoid or datum (C{meter},
399                      an L{Ellipsoid}, L{Ellipsoid2}, L{Datum} or
400                      L{a_f2Tuple}).
401       @kwarg lat: Parallel latitude (C{degrees90}, C{str}).
402
403       @return: Angle (C{radians}) or C{INF} for near-polar B{C{lat}}.
404
405       @raise RangeError: Latitude B{C{lat}} outside valid range
406                          and L{rangerrors} set to C{True}.
407
408       @raise TypeError: Invalid B{C{radius}}.
409
410       @raise ValueError: Invalid B{C{distance}}, B{C{radius}}
411                          or B{C{lat}}.
412
413       @see: Function L{m2degrees} and L{radians2m}.
414    '''
415    m = circle4(radius, lat).radius
416    return INF if m < EPS0 else (Float(distance) / m)
417
418
419def m2SM(meter):
420    '''Convert meter to statute miles (SM).
421
422       @arg meter: Value in meter (C{scalar}).
423
424       @return: Value in SM (C{float}).
425
426       @raise ValueError: Invalid B{C{meter}}.
427    '''
428    return Meter(meter) * 6.21369949e-4  # == _1_0 / 1609.344
429
430
431def m2yard(meter):
432    '''Convert meter to I{UK} yards.
433
434       @arg meter: Value in meter (C{scalar}).
435
436       @return: Value in yards (C{float}).
437
438       @raise ValueError: Invalid B{C{meter}}.
439    '''
440    # 1.0936132983377078 == 10_000 / (254 * 12 * 3)
441    return Meter(meter) * 1.09361329833771
442
443
444def radiansPI(deg):
445    '''Convert and wrap degrees to radians M{[-PI..+PI]}.
446
447       @arg deg: Angle (C{degrees}).
448
449       @return: Radians, wrapped (C{radiansPI})
450    '''
451    return _wrap(radians(deg), PI, PI2)
452
453
454def radiansPI2(deg):
455    '''Convert and wrap degrees to radians M{[0..+2PI)}.
456
457       @arg deg: Angle (C{degrees}).
458
459       @return: Radians, wrapped (C{radiansPI2})
460    '''
461    return _wrap(radians(deg), PI2, PI2)
462
463
464def radiansPI_2(deg):
465    '''Convert and wrap degrees to radians M{[-3PI/2..+PI/2]}.
466
467       @arg deg: Angle (C{degrees}).
468
469       @return: Radians, wrapped (C{radiansPI_2})
470    '''
471    return _wrap(radians(deg), PI_2, PI2)
472
473
474def radians2m(rad, radius=R_M, lat=0):
475    '''Convert an angle to a distance along the equator or
476       along the parallel at an other (geodetic) latitude.
477
478       @arg rad: The angle (C{radians}).
479       @kwarg radius: Mean earth radius, ellipsoid or datum
480                      (C{meter}, L{Ellipsoid}, L{Ellipsoid2},
481                      L{Datum} or L{a_f2Tuple}).
482       @kwarg lat: Parallel latitude (C{degrees90}, C{str}).
483
484       @return: Distance (C{meter}, same units as B{C{radius}}
485                or ellipsoidal and polar radii) or C{0} for
486                near-polar B{C{lat}}.
487
488       @raise RangeError: Latitude B{C{lat}} outside valid range
489                          and L{rangerrors} set to C{True}.
490
491       @raise TypeError: Invalid B{C{radius}}.
492
493       @raise ValueError: Invalid B{C{rad}}, B{C{radius}} or
494                          B{C{lat}}.
495
496       @see: Function L{degrees2m} and L{m2radians}.
497    '''
498    m = circle4(radius, lat).radius
499    return _0_0 if m < EPS0 else (Lam(rad=rad, clip=0) * m)
500
501
502def _sincos2(q, r):
503    '''(INTERNAL) 2-tuple (C{sin(r), cos(r)}) in quadrant M{0 <= q <= 3}.
504    '''
505    if r < EPS:  # XXX EPS0
506        s, c = _0_0, _1_0
507    elif r < PI_2:
508        s, c = sin(r), cos(r)
509    else:  # r == PI_2
510        s, c = _1_0, _0_0
511    t = s, c, -s, -c, s
512#   q &= 3
513    return t[q], t[q + 1]
514
515
516def sincos2(rad):
517    '''Return the C{sine} and C{cosine} of an angle in C{radians}.
518
519       @arg rad: Angle (C{radians}).
520
521       @return: 2-Tuple (C{sin(B{rad})}, C{cos(B{rad})}).
522
523       @see: U{GeographicLib<https://GeographicLib.SourceForge.io/html/
524             classGeographicLib_1_1Math.html#sincosd>} function U{sincosd
525             <https://SourceForge.net/p/geographiclib/code/ci/release/tree/
526             python/geographiclib/geomath.py#l155>} and C++ U{sincosd
527             <https://SourceForge.net/p/geographiclib/code/ci/release/tree/
528             include/GeographicLib/Math.hpp#l558>}.
529    '''
530    q = int(rad * _2__PI)  # int(math.floor)
531    if rad < 0:
532        q -= 1
533    return _sincos2(q & 3, rad - q * PI_2)
534
535
536def sincos2_(*rads):
537    '''Return the C{sine} and C{cosine} of angle(s) in {Cradians}.
538
539       @arg rads: One or more angles (C{radians}).
540
541       @return: Yield the C{sin(B{rad})} and C{cos(B{rad})} for each angle.
542
543       @see: function L{sincos2}.
544    '''
545    for r in rads:
546        s, c = sincos2(r)
547        yield s
548        yield c
549
550
551def sincos2d(deg):
552    '''Return the C{sine} and C{cosine} of an angle in C{degrees}.
553
554       @arg deg: Angle (C{degrees}).
555
556       @return: 2-Tuple (C{sin(B{deg})}, C{cos(B{deg})}).
557
558       @see: U{GeographicLib<https://GeographicLib.SourceForge.io/html/
559             classGeographicLib_1_1Math.html#sincosd>} function U{sincosd
560             <https://SourceForge.net/p/geographiclib/code/ci/release/tree/
561             python/geographiclib/geomath.py#l155>} and C++ U{sincosd
562             <https://SourceForge.net/p/geographiclib/code/ci/release/tree/
563             include/GeographicLib/Math.hpp#l558>}.
564    '''
565    q = int(deg * _1__90)  # int(math.floor)
566    if deg < 0:
567        q -= 1
568    return _sincos2(q & 3, radians(deg - q * _90_0))
569
570
571def sincos2d_(*degs):
572    '''Return the C{sine} and C{cosine} of angle(s) in C{degrees}.
573
574       @arg degs: One or more angles (C{degrees}).
575
576       @return: Yield the C{sin(B{deg})} and C{cos(B{deg})} for each angle.
577
578       @see: Function L{sincos2d}.
579    '''
580    for d in degs:
581        s, c = sincos2d(d)
582        yield s
583        yield c
584
585
586def tan_2(rad, **semi):  # edge=1
587    '''Compute the tangent of half angle.
588
589       @arg rad: Angle (C{radians}).
590       @kwarg semi: Angle or edge name and index
591                    for semi-circular error.
592
593       @return: M{tan(rad / 2)} (C{float}).
594
595       @raise ValueError: If B{C{rad}} is semi-circular
596                          and B{C{semi}} is given.
597    '''
598    # .formy.excessKarney_, .sphericalTrigonometry.areaOf
599    if semi and isnear0(abs(rad) - PI):
600        for n, v in semi.items():
601            break
602        if isint(v):
603            from pygeodesy.streprs import Fmt
604            n = _SPACE_(Fmt.SQUARE(**semi), _edge_)
605        else:
606            n = _SPACE_(n, _radians_)
607        from pygeodesy.errors import _ValueError
608        raise _ValueError(n, rad, txt=_semi_circular_)
609
610    return tan(rad * _0_5)
611
612
613def tand(deg, **error_kwds):
614    '''Return the C{tangent} of an angle in C{degrees}.
615
616       @arg deg: Angle (C{degrees}).
617       @kwarg error_kwds: Error to raise (C{ValueError}).
618
619       @return: C{tan(B{deg})}.
620
621       @raise ValueError: If L{pygeodesy.isnear0}C{(cos(B{deg})}.
622    '''
623    s, c = sincos2d(deg)
624    if isnear0(c):
625        from pygeodesy.errors import _ValueError, _xkwds
626        raise _ValueError(**_xkwds(error_kwds, tand=deg))
627    return s / c
628
629
630def tand_(*degs, **error_kwds):
631    '''Return the C{tangent} of angle(s) in C{degrees}.
632
633       @arg degs: One or more angles (C{degrees}).
634       @kwarg error_kwds: Error to raise (C{ValueError}).
635
636       @return: Yield the C{tan(B{deg})} for each angle.
637
638       @raise ValueError: See L{pygeodesy.tand}.
639    '''
640    for d in degs:
641        yield tand(d, **error_kwds)
642
643
644def tanPI_2_2(rad):
645    '''Compute the tangent of half angle, 90 degrees rotated.
646
647       @arg rad: Angle (C{radians}).
648
649       @return: M{tan((rad + PI/2) / 2)} (C{float}).
650    '''
651    return tan((rad + PI_2) * _0_5)
652
653
654def unroll180(lon1, lon2, wrap=True):
655    '''Unroll longitudinal delta and wrap longitude in degrees.
656
657       @arg lon1: Start longitude (C{degrees}).
658       @arg lon2: End longitude (C{degrees}).
659       @kwarg wrap: Wrap and unroll to the M{(-180..+180]} range (C{bool}).
660
661       @return: 2-Tuple C{(B{lon2}-B{lon1}, B{lon2})} unrolled (C{degrees},
662                C{degrees}).
663
664       @see: Capability C{LONG_UNROLL} in U{GeographicLib
665             <https://GeographicLib.SourceForge.io/html/python/interface.html#outmask>}.
666    '''
667    d = lon2 - lon1
668    if wrap and abs(d) > _180_0:
669        u = _wrap(d, _180_0, _360_0)
670        if u != d:
671            return u, lon1 + u
672    return d, lon2
673
674
675def unrollPI(rad1, rad2, wrap=True):
676    '''Unroll longitudinal delta and wrap longitude in radians.
677
678       @arg rad1: Start longitude (C{radians}).
679       @arg rad2: End longitude (C{radians}).
680       @kwarg wrap: Wrap and unroll to the M{(-PI..+PI]} range (C{bool}).
681
682       @return: 2-Tuple C{(B{rad2}-B{rad1}, B{rad2})} unrolled (C{radians},
683                C{radians}).
684
685       @see: Capability C{LONG_UNROLL} in U{GeographicLib
686             <https://GeographicLib.SourceForge.io/html/python/interface.html#outmask>}.
687    '''
688    r = rad2 - rad1
689    if wrap and abs(r) > PI:
690        u = _wrap(r, PI, PI2)
691        if u != r:
692            return u, rad1 + u
693    return r, rad2
694
695
696def _wrap(angle, wrap, modulo):
697    '''(INTERNAL) Angle wrapper M{((wrap-modulo)..+wrap]}.
698
699       @arg angle: Angle (C{degrees}, C{radians} or C{grades}).
700       @arg wrap: Range (C{degrees}, C{radians} or C{grades}).
701       @arg modulo: Upper limit (360 C{degrees}, PI2 C{radians} or 400 C{grades}).
702
703       @return: The B{C{angle}}, wrapped (C{degrees}, C{radians} or C{grades}).
704    '''
705    angle = float(angle)
706    if not wrap > angle >= (wrap - modulo):
707        # math.fmod(-1.5, 3.14) == -1.5, but -1.5 % 3.14 == 1.64
708        # math.fmod(-1.5, 360) == -1.5, but -1.5 % 360 == 358.5
709        angle %= modulo
710        if angle > wrap:
711            angle -= modulo
712    return angle
713
714
715def wrap90(deg):
716    '''Wrap degrees to M{[-270..+90]}.
717
718       @arg deg: Angle (C{degrees}).
719
720       @return: Degrees, wrapped (C{degrees90}).
721    '''
722    return _wrap(deg, _90_0, _360_0)
723
724
725def wrap180(deg):
726    '''Wrap degrees to M{[-180..+180]}.
727
728       @arg deg: Angle (C{degrees}).
729
730       @return: Degrees, wrapped (C{degrees180}).
731    '''
732    return _wrap(deg, _180_0, _360_0)
733
734
735def wrap360(deg):
736    '''Wrap degrees to M{[0..+360)}.
737
738       @arg deg: Angle (C{degrees}).
739
740       @return: Degrees, wrapped (C{degrees360}).
741    '''
742    return _wrap(deg, _360_0, _360_0)
743
744
745def wrapPI(rad):
746    '''Wrap radians to M{[-PI..+PI]}.
747
748       @arg rad: Angle (C{radians}).
749
750       @return: Radians, wrapped (C{radiansPI}).
751    '''
752    return _wrap(rad, PI, PI2)
753
754
755def wrapPI2(rad):
756    '''Wrap radians to M{[0..+2PI)}.
757
758       @arg rad: Angle (C{radians}).
759
760       @return: Radians, wrapped (C{radiansPI2}).
761    '''
762    return _wrap(rad, PI2, PI2)
763
764
765def wrapPI_2(rad):
766    '''Wrap radians to M{[-3PI/2..+PI/2]}.
767
768       @arg rad: Angle (C{radians}).
769
770       @return: Radians, wrapped (C{radiansPI_2}).
771    '''
772    return _wrap(rad, PI_2, PI2)
773
774
775def yard2m(yards):
776    '''Convert I{UK} yards to meter.
777
778       @arg yards: Value in yards (C{scalar}).
779
780       @return: Value in C{meter} (C{float}).
781
782       @raise ValueError: Invalid B{C{yards}}.
783    '''
784    # 0.9144 == 254 * 12 * 3 / 10_000 == 3 * ft2m(1) Int'l
785    return Float(yards) * 0.9144
786
787# **) MIT License
788#
789# Copyright (C) 2016-2021 -- mrJean1 at Gmail -- All Rights Reserved.
790#
791# Permission is hereby granted, free of charge, to any person obtaining a
792# copy of this software and associated documentation files (the "Software"),
793# to deal in the Software without restriction, including without limitation
794# the rights to use, copy, modify, merge, publish, distribute, sublicense,
795# and/or sell copies of the Software, and to permit persons to whom the
796# Software is furnished to do so, subject to the following conditions:
797#
798# The above copyright notice and this permission notice shall be included
799# in all copies or substantial portions of the Software.
800#
801# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
802# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
803# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
804# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
805# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
806# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
807# OTHER DEALINGS IN THE SOFTWARE.
808