Coverage for pygeodesy/utily.py: 91%
336 statements
« prev ^ index » next coverage.py v7.2.2, created at 2024-05-15 16:36 -0400
« prev ^ index » next coverage.py v7.2.2, created at 2024-05-15 16:36 -0400
2# -*- coding: utf-8 -*-
4u'''Various utility functions.
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 as _; del _ # PYCHOK semicolon
13from pygeodesy.basics import _copysign, isinstanceof, isint, isstr, neg
14from pygeodesy.constants import EPS, EPS0, INF, NAN, PI, PI2, PI_2, R_M, \
15 _M_KM, _M_NM, _M_SM, _0_0, _1__90, _0_5, _1_0, \
16 _N_1_0, _2__PI, _10_0, _90_0, _N_90_0, _180_0, \
17 _N_180_0, _360_0, _400_0, _copysign_0_0, \
18 _float as _F, _isfinite, isnan, isnear0, \
19 _over, _umod_360, _umod_PI2
20from pygeodesy.errors import _ValueError, _xkwds, _xkwds_get, _ALL_LAZY, _MODS
21from pygeodesy.internals import _passargs # , _MODS?
22from pygeodesy.interns import _edge_, _radians_, _semi_circular_, _SPACE_
23# from pygeodesy.lazily import _ALL_LAZY, _ALL_MODS as _MODS # from .errors
24from pygeodesy.units import Degrees, Degrees_, Feet, Float, Lam, Lam_, \
25 Meter, Meter2, Radians, Radians_
27from math import acos, asin, atan2, cos, degrees, fabs, radians, sin, tan # pow
29__all__ = _ALL_LAZY.utily
30__version__ = '24.05.14'
32# read constant name "_M_Unit" as "meter per Unit"
33_M_CHAIN = _F( 20.1168) # yard2m(1) * 22
34_M_FATHOM = _F( 1.8288) # yard2m(1) * 2 or _M_NM * 1e-3
35_M_FOOT = _F( 0.3048) # Int'l (1 / 3.2808398950131 = 10_000 / (254 * 12))
36_M_FOOT_GE = _F( 0.31608) # German Fuss (1 / 3.1637560111364)
37_M_FOOT_FR = _F( 0.3248406) # French Pied-du-Roi or pied (1 / 3.0784329298739)
38_M_FOOT_USVY = _F( 0.3048006096012192) # US Survey (1200 / 3937)
39_M_FURLONG = _F( 201.168) # 220 * yard2m(1) == 10 * m2chain(1)
40# _M_KM = _F(1000.0) # kilo meter
41# _M_NM = _F(1852.0) # nautical mile
42# _M_SM = _F(1609.344) # statute mile
43_M_TOISE = _F( 1.9490436) # French toise, 6 pieds (6 / 3.0784329298739)
44_M_YARD_UK = _F( 0.9144) # 254 * 12 * 3 / 10_000 == 3 * ft2m(1) Int'l
47def _abs1nan(x):
48 '''(INTERNAL) Bracket C{x}.
49 '''
50 return _N_1_0 < x < _1_0 or isnan(x)
53def acos1(x):
54 '''Return C{math.acos(max(-1, min(1, B{x})))}.
55 '''
56 return acos(x) if _abs1nan(x) else (PI if x < 0 else _0_0)
59def acre2ha(acres):
60 '''Convert acres to hectare.
62 @arg acres: Value in acres (C{scalar}).
64 @return: Value in C{hectare} (C{float}).
66 @raise ValueError: Invalid B{C{acres}}.
67 '''
68 # 0.40468564224 == acre2m2(1) / 10_000
69 return Float(ha=Float(acres) * 0.40468564224)
72def acre2m2(acres):
73 '''Convert acres to I{square} meter.
75 @arg acres: Value in acres (C{scalar}).
77 @return: Value in C{meter^2} (C{float}).
79 @raise ValueError: Invalid B{C{acres}}.
80 '''
81 # 4046.8564224 == chain2m(1) * furlong2m(1)
82 return Meter2(Float(acres) * 4046.8564224)
85def asin1(x):
86 '''Return C{math.asin(max(-1, min(1, B{x})))}.
87 '''
88 return asin(x) if _abs1nan(x) else _copysign(PI_2, x)
91def atan1(y, x=_1_0):
92 '''Return C{atan(B{y} / B{x})} angle in C{radians} M{[-PI/2..+PI/2]}
93 using C{atan2} for consistency and to avoid C{ZeroDivisionError}.
94 '''
95 return atan2(-y, -x) if x < 0 else atan2(y, x or _0_0) # -0. to 0.
98def atan1d(y, x=_1_0):
99 '''Return C{atan(B{y} / B{x})} angle in C{degrees} M{[-90..+90]}
100 using C{atan2d} for consistency and to avoid C{ZeroDivisionError}.
102 @see: Function L{pygeodesy.atan2d}.
103 '''
104 return atan2d(-y, -x) if x < 0 else atan2d(y, x or _0_0) # -0. to 0.
107def atan2b(y, x):
108 '''Return C{atan2(B{y}, B{x})} in degrees M{[0..+360]}.
110 @see: Function L{pygeodesy.atan2d}.
111 '''
112 b = atan2d(y, x)
113 if b < 0:
114 b += _360_0
115 return b
118def atan2d(y, x, reverse=False):
119 '''Return C{atan2(B{y}, B{x})} in degrees M{[-180..+180]},
120 optionally I{reversed} (by 180 degrees for C{azimuth}s).
122 @see: I{Karney}'s C++ function U{Math.atan2d
123 <https://GeographicLib.SourceForge.io/C++/doc/classGeographicLib_1_1Math.html>}.
124 '''
125 if fabs(y) > fabs(x) > 0:
126 if y < 0: # q = 3
127 d = degrees(atan2(x, -y)) - _90_0
128 else: # q = 2
129 d = _90_0 - degrees(atan2(x, y))
130 elif isnan(x) or isnan(y):
131 return NAN
132 elif y:
133 if x > 0: # q = 0
134 d = degrees(atan2(y, x))
135 elif x < 0: # q = 1
136 d = _copysign(_180_0, y) - degrees(atan2(y, -x))
137 else: # x == 0
138 d = _copysign(_90_0, y)
139 else:
140 d = _180_0 if x < 0 else _0_0
141 return _azireversed(d) if reverse else d
144def _azireversed(azimuth): # in .rhumbBase
145 '''(INTERNAL) Return the I{reverse} B{C{azimuth}} in degrees M{[-180..+180]}.
146 '''
147 return azimuth + (_N_180_0 if azimuth > 0 else _180_0)
150def chain2m(chains):
151 '''Convert I{UK} chains to meter.
153 @arg chains: Value in chains (C{scalar}).
155 @return: Value in C{meter} (C{float}).
157 @raise ValueError: Invalid B{C{chains}}.
158 '''
159 return Meter(Float(chains=chains) * _M_CHAIN)
162def circle4(earth, lat):
163 '''Get the equatorial or a parallel I{circle of latitude}.
165 @arg earth: The earth radius, ellipsoid or datum
166 (C{meter}, L{Ellipsoid}, L{Ellipsoid2},
167 L{Datum} or L{a_f2Tuple}).
168 @arg lat: Geodetic latitude (C{degrees90}, C{str}).
170 @return: A L{Circle4Tuple}C{(radius, height, lat, beta)}
171 instance.
173 @raise RangeError: Latitude B{C{lat}} outside valid range and
174 L{pygeodesy.rangerrors} set to C{True}.
176 @raise TypeError: Invalid B{C{earth}}.
178 @raise ValueError: B{C{earth}} or B{C{lat}}.
179 '''
180 E = _MODS.datums._earth_ellipsoid(earth)
181 return E.circle4(lat)
184def cot(rad, **error_kwds):
185 '''Return the C{cotangent} of an angle in C{radians}.
187 @arg rad: Angle (C{radians}).
188 @kwarg error_kwds: Error to raise (C{ValueError}).
190 @return: C{cot(B{rad})}.
192 @raise ValueError: L{pygeodesy.isnear0}C{(sin(B{rad})}.
193 '''
194 s, c = sincos2(rad)
195 if c:
196 if isnear0(s):
197 raise _valueError(cot, rad, **error_kwds)
198 c = c / s # /= chokes PyChecker
199 return c
202def cot_(*rads, **error_kwds):
203 '''Return the C{cotangent} of angle(s) in C{radiansresection}.
205 @arg rads: One or more angles (C{radians}).
206 @kwarg error_kwds: Error to raise (C{ValueError}).
208 @return: Yield the C{cot(B{rad})} for each angle.
210 @raise ValueError: See L{pygeodesy.cot}.
211 '''
212 try:
213 for r in rads:
214 yield cot(r)
215 except ValueError:
216 raise _valueError(cot_, r, **error_kwds)
219def cotd(deg, **error_kwds):
220 '''Return the C{cotangent} of an angle in C{degrees}.
222 @arg deg: Angle (C{degrees}).
223 @kwarg error_kwds: Error to raise (C{ValueError}).
225 @return: C{cot(B{deg})}.
227 @raise ValueError: L{pygeodesy.isnear0}C{(sin(B{deg})}.
228 '''
229 s, c = sincos2d(deg)
230 if c:
231 if isnear0(s):
232 raise _valueError(cotd, deg, **error_kwds)
233 c = c / s # /= chokes PyChecker
234 elif s < 0:
235 c = -c # negate-0
236 return c
239def cotd_(*degs, **error_kwds):
240 '''Return the C{cotangent} of angle(s) in C{degrees}.
242 @arg degs: One or more angles (C{degrees}).
243 @kwarg error_kwds: Error to raise (C{ValueError}).
245 @return: Yield the C{cot(B{deg})} for each angle.
247 @raise ValueError: See L{pygeodesy.cotd}.
248 '''
249 try:
250 for d in degs:
251 yield cotd(d)
252 except ValueError:
253 raise _valueError(cotd_, d, **error_kwds)
256def degrees90(rad):
257 '''Convert radians to degrees and wrap M{[-90..+90)}.
259 @arg rad: Angle (C{radians}).
261 @return: Angle, wrapped (C{degrees90}).
262 '''
263 return wrap90(degrees(rad))
266def degrees180(rad):
267 '''Convert radians to degrees and wrap M{[-180..+180)}.
269 @arg rad: Angle (C{radians}).
271 @return: Angle, wrapped (C{degrees180}).
272 '''
273 return wrap180(degrees(rad))
276def degrees360(rad):
277 '''Convert radians to degrees and wrap M{[0..+360)}.
279 @arg rad: Angle (C{radians}).
281 @return: Angle, wrapped (C{degrees360}).
282 '''
283 return _umod_360(degrees(rad))
286def degrees2grades(deg):
287 '''Convert degrees to I{grades} (aka I{gons} or I{gradians}).
289 @arg deg: Angle (C{degrees}).
291 @return: Angle (C{grades}).
292 '''
293 return Degrees(deg) * _400_0 / _360_0
296def degrees2m(deg, radius=R_M, lat=0):
297 '''Convert an angle to a distance along the equator or
298 along the parallel at an other (geodetic) latitude.
300 @arg deg: The angle (C{degrees}).
301 @kwarg radius: Mean earth radius, ellipsoid or datum
302 (C{meter}, L{Ellipsoid}, L{Ellipsoid2},
303 L{Datum} or L{a_f2Tuple}).
304 @kwarg lat: Parallel latitude (C{degrees90}, C{str}).
306 @return: Distance (C{meter}, same units as B{C{radius}}
307 or equatorial and polar radii) or C{0.0} for
308 near-polar B{C{lat}}.
310 @raise RangeError: Latitude B{C{lat}} outside valid range and
311 L{pygeodesy.rangerrors} set to C{True}.
313 @raise TypeError: Invalid B{C{radius}}.
315 @raise ValueError: Invalid B{C{deg}}, B{C{radius}} or
316 B{C{lat}}.
318 @see: Function L{radians2m} and L{m2degrees}.
319 '''
320 return _Radians2m(Lam_(deg=deg, clip=0), radius, lat)
323def fathom2m(fathoms):
324 '''Convert I{Imperial} fathom to meter.
326 @arg fathoms: Value in fathoms (C{scalar}).
328 @return: Value in C{meter} (C{float}).
330 @raise ValueError: Invalid B{C{fathoms}}.
332 @see: Function L{toise2m}, U{Fathom<https://WikiPedia.org/wiki/Fathom>}
333 and U{Klafter<https://WikiPedia.org/wiki/Klafter>}.
334 '''
335 return Meter(Float(fathoms=fathoms) * _M_FATHOM)
338def ft2m(feet, usurvey=False, pied=False, fuss=False):
339 '''Convert I{International}, I{US Survey}, I{French} or I{German}
340 B{C{feet}} to C{meter}.
342 @arg feet: Value in feet (C{scalar}).
343 @kwarg usurvey: If C{True}, convert I{US Survey} foot else ...
344 @kwarg pied: If C{True}, convert French I{pied-du-Roi} else ...
345 @kwarg fuss: If C{True}, convert German I{Fuss}, otherwise
346 I{International} foot to C{meter}.
348 @return: Value in C{meter} (C{float}).
350 @raise ValueError: Invalid B{C{feet}}.
351 '''
352 return Meter(Feet(feet) * (_M_FOOT_USVY if usurvey else
353 (_M_FOOT_FR if pied else
354 (_M_FOOT_GE if fuss else _M_FOOT))))
357def furlong2m(furlongs):
358 '''Convert a furlong to meter.
360 @arg furlongs: Value in furlongs (C{scalar}).
362 @return: Value in C{meter} (C{float}).
364 @raise ValueError: Invalid B{C{furlongs}}.
365 '''
366 return Meter(Float(furlongs=furlongs) * _M_FURLONG)
369def grades(rad):
370 '''Convert radians to I{grades} (aka I{gons} or I{gradians}).
372 @arg rad: Angle (C{radians}).
374 @return: Angle (C{grades}).
375 '''
376 return Float(grades=Float(rad=rad) * _400_0 / PI2)
379def grades400(rad):
380 '''Convert radians to I{grades} (aka I{gons} or I{gradians}) and wrap M{[0..+400)}.
382 @arg rad: Angle (C{radians}).
384 @return: Angle, wrapped (C{grades}).
385 '''
386 return Float(grades400=(grades(rad) % _400_0) or _0_0) # _umod_400
389def grades2degrees(gon):
390 '''Convert I{grades} (aka I{gons} or I{gradians}) to C{degrees}.
392 @arg gon: Angle (C{grades}).
394 @return: Angle (C{degrees}).
395 '''
396 return Degrees(Float(gon=gon) * _360_0 / _400_0)
399def grades2radians(gon):
400 '''Convert I{grades} (aka I{gons} or I{gradians}) to C{radians}.
402 @arg gon: Angle (C{grades}).
404 @return: Angle (C{radians}).
405 '''
406 return Radians(Float(gon=gon) * PI2 / _400_0)
409def km2m(km):
410 '''Convert kilo meter to meter (m).
412 @arg km: Value in kilo meter (C{scalar}).
414 @return: Value in meter (C{float}).
416 @raise ValueError: Invalid B{C{km}}.
417 '''
418 return Meter(Float(km=km) * _M_KM)
421def _loneg(lon):
422 '''(INTERNAL) "Complement" of C{lon}.
423 '''
424 return _180_0 - lon
427def m2chain(meter):
428 '''Convert meter to I{UK} chains.
430 @arg meter: Value in meter (C{scalar}).
432 @return: Value in C{chains} (C{float}).
434 @raise ValueError: Invalid B{C{meter}}.
435 '''
436 return Float(chain=Meter(meter) / _M_CHAIN) # * 0.049709695378986715
439def m2degrees(distance, radius=R_M, lat=0):
440 '''Convert a distance to an angle along the equator or
441 along the parallel at an other (geodetic) latitude.
443 @arg distance: Distance (C{meter}, same units as B{C{radius}}).
444 @kwarg radius: Mean earth radius, ellipsoid or datum (C{meter},
445 an L{Ellipsoid}, L{Ellipsoid2}, L{Datum} or
446 L{a_f2Tuple}).
447 @kwarg lat: Parallel latitude (C{degrees90}, C{str}).
449 @return: Angle (C{degrees}) or C{INF} for near-polar B{C{lat}}.
451 @raise RangeError: Latitude B{C{lat}} outside valid range and
452 L{pygeodesy.rangerrors} set to C{True}.
454 @raise TypeError: Invalid B{C{radius}}.
456 @raise ValueError: Invalid B{C{distance}}, B{C{radius}}
457 or B{C{lat}}.
459 @see: Function L{m2radians} and L{degrees2m}.
460 '''
461 return degrees(m2radians(distance, radius=radius, lat=lat))
464def m2fathom(meter):
465 '''Convert meter to I{Imperial} fathoms.
467 @arg meter: Value in meter (C{scalar}).
469 @return: Value in C{fathoms} (C{float}).
471 @raise ValueError: Invalid B{C{meter}}.
473 @see: Function L{m2toise}, U{Fathom<https://WikiPedia.org/wiki/Fathom>}
474 and U{Klafter<https://WikiPedia.org/wiki/Klafter>}.
475 '''
476 return Float(fathom=Meter(meter) / _M_FATHOM) # * 0.546806649
479def m2ft(meter, usurvey=False, pied=False, fuss=False):
480 '''Convert meter to I{International}, I{US Survey}, I{French} or
481 or I{German} feet (C{ft}).
483 @arg meter: Value in meter (C{scalar}).
484 @kwarg usurvey: If C{True}, convert to I{US Survey} foot else ...
485 @kwarg pied: If C{True}, convert to French I{pied-du-Roi} else ...
486 @kwarg fuss: If C{True}, convert to German I{Fuss}, otherwise to
487 I{International} foot.
489 @return: Value in C{feet} (C{float}).
491 @raise ValueError: Invalid B{C{meter}}.
492 '''
493 # * 3.2808333333333333, US Survey 3937 / 1200
494 # * 3.2808398950131235, Int'l 10_000 / (254 * 12)
495 return Float(feet=Meter(meter) / (_M_FOOT_USVY if usurvey else
496 (_M_FOOT_FR if pied else
497 (_M_FOOT_GE if fuss else _M_FOOT))))
500def m2furlong(meter):
501 '''Convert meter to furlongs.
503 @arg meter: Value in meter (C{scalar}).
505 @return: Value in C{furlongs} (C{float}).
507 @raise ValueError: Invalid B{C{meter}}.
508 '''
509 return Float(furlong=Meter(meter) / _M_FURLONG) # * 0.00497096954
512def m2km(meter):
513 '''Convert meter to kilo meter (Km).
515 @arg meter: Value in meter (C{scalar}).
517 @return: Value in Km (C{float}).
519 @raise ValueError: Invalid B{C{meter}}.
520 '''
521 return Float(km=Meter(meter) / _M_KM)
524def m2NM(meter):
525 '''Convert meter to nautical miles (NM).
527 @arg meter: Value in meter (C{scalar}).
529 @return: Value in C{NM} (C{float}).
531 @raise ValueError: Invalid B{C{meter}}.
532 '''
533 return Float(NM=Meter(meter) / _M_NM) # * 5.39956804e-4
536def m2radians(distance, radius=R_M, lat=0):
537 '''Convert a distance to an angle along the equator or along the
538 parallel at an other (geodetic) latitude.
540 @arg distance: Distance (C{meter}, same units as B{C{radius}}).
541 @kwarg radius: Mean earth radius, ellipsoid or datum (C{meter},
542 an L{Ellipsoid}, L{Ellipsoid2}, L{Datum} or
543 L{a_f2Tuple}).
544 @kwarg lat: Parallel latitude (C{degrees90}, C{str}).
546 @return: Angle (C{radians}) or C{INF} for near-polar B{C{lat}}.
548 @raise RangeError: Latitude B{C{lat}} outside valid range and
549 L{pygeodesy.rangerrors} set to C{True}.
551 @raise TypeError: Invalid B{C{radius}}.
553 @raise ValueError: Invalid B{C{distance}}, B{C{radius}}
554 or B{C{lat}}.
556 @see: Function L{m2degrees} and L{radians2m}.
557 '''
558 m = circle4(radius, lat).radius
559 return INF if m < EPS0 else Radians(Float(distance=distance) / m)
562def m2SM(meter):
563 '''Convert meter to statute miles (SM).
565 @arg meter: Value in meter (C{scalar}).
567 @return: Value in C{SM} (C{float}).
569 @raise ValueError: Invalid B{C{meter}}.
570 '''
571 return Float(SM=Meter(meter) / _M_SM) # * 6.21369949e-4 == 1 / 1609.344
574def m2toise(meter):
575 '''Convert meter to French U{toises<https://WikiPedia.org/wiki/Toise>}.
577 @arg meter: Value in meter (C{scalar}).
579 @return: Value in C{toises} (C{float}).
581 @raise ValueError: Invalid B{C{meter}}.
583 @see: Function L{m2fathom}.
584 '''
585 return Float(toise=Meter(meter) / _M_TOISE) # * 0.513083632632119
588def m2yard(meter):
589 '''Convert meter to I{UK} yards.
591 @arg meter: Value in meter (C{scalar}).
593 @return: Value in C{yards} (C{float}).
595 @raise ValueError: Invalid B{C{meter}}.
596 '''
597 return Float(yard=Meter(meter) / _M_YARD_UK) # * 1.0936132983377078
600def NM2m(nm):
601 '''Convert nautical miles to meter (m).
603 @arg nm: Value in nautical miles (C{scalar}).
605 @return: Value in meter (C{float}).
607 @raise ValueError: Invalid B{C{nm}}.
608 '''
609 return Meter(Float(nm=nm) * _M_NM)
612def radians2m(rad, radius=R_M, lat=0):
613 '''Convert an angle to a distance along the equator or
614 along the parallel at an other (geodetic) latitude.
616 @arg rad: The angle (C{radians}).
617 @kwarg radius: Mean earth radius, ellipsoid or datum
618 (C{meter}, L{Ellipsoid}, L{Ellipsoid2},
619 L{Datum} or L{a_f2Tuple}).
620 @kwarg lat: Parallel latitude (C{degrees90}, C{str}).
622 @return: Distance (C{meter}, same units as B{C{radius}}
623 or equatorial and polar radii) or C{0.0} for
624 near-polar B{C{lat}}.
626 @raise RangeError: Latitude B{C{lat}} outside valid range and
627 L{pygeodesy.rangerrors} set to C{True}.
629 @raise TypeError: Invalid B{C{radius}}.
631 @raise ValueError: Invalid B{C{rad}}, B{C{radius}} or
632 B{C{lat}}.
634 @see: Function L{degrees2m} and L{m2radians}.
635 '''
636 return _Radians2m(Lam(rad=rad, clip=0), radius, lat)
639def _Radians2m(rad, radius, lat):
640 '''(INTERNAL) Helper for C{degrees2m} and C{radians2m}.
641 '''
642 m = circle4(radius, lat).radius
643 return _0_0 if m < EPS0 else (rad * m)
646def radiansPI(deg):
647 '''Convert and wrap degrees to radians M{[-PI..+PI]}.
649 @arg deg: Angle (C{degrees}).
651 @return: Radians, wrapped (C{radiansPI})
652 '''
653 return wrapPI(radians(deg))
656def radiansPI2(deg):
657 '''Convert and wrap degrees to radians M{[0..+2PI)}.
659 @arg deg: Angle (C{degrees}).
661 @return: Radians, wrapped (C{radiansPI2})
662 '''
663 return _umod_PI2(radians(deg))
666def radiansPI_2(deg):
667 '''Convert and wrap degrees to radians M{[-3PI/2..+PI/2]}.
669 @arg deg: Angle (C{degrees}).
671 @return: Radians, wrapped (C{radiansPI_2})
672 '''
673 return wrapPI_2(radians(deg))
676def _sin0cos2(q, r, sign):
677 '''(INTERNAL) 2-tuple (C{sin(r), cos(r)}) in quadrant C{0 <= B{q} <= 3}
678 and C{sin} zero I{signed} with B{C{sign}}.
679 '''
680 if r < PI_2:
681 s, c = sin(r), cos(r)
682 t = s, c, -s, -c, s
683 else: # r == PI_2
684 t = _1_0, _0_0, _N_1_0, _0_0, _1_0
685# else: # r == 0, testUtility failures
686# t = _0_0, _1_0, _0_0, _N_1_0, _0_0
687# q &= 3
688 s = t[q] or _copysign_0_0(sign)
689 c = t[q + 1] or _0_0
690 return s, c
693def SinCos2(x):
694 '''Get C{sin} and C{cos} of I{typed} angle.
696 @arg x: Angle (L{Degrees}, L{Radians} or scalar C{radians}).
698 @return: 2-Tuple (C{sin(B{x})}, C{cos(B{x})}).
699 '''
700 return sincos2d(x) if isinstanceof(x, Degrees, Degrees_) else (
701 sincos2(x) if isinstanceof(x, Radians, Radians_) else
702 sincos2(float(x))) # assume C{radians}
705def sincos2(rad):
706 '''Return the C{sine} and C{cosine} of an angle in C{radians}.
708 @arg rad: Angle (C{radians}).
710 @return: 2-Tuple (C{sin(B{rad})}, C{cos(B{rad})}).
712 @see: U{GeographicLib<https://GeographicLib.SourceForge.io/C++/doc/
713 classGeographicLib_1_1Math.html#sincosd>} function U{sincosd
714 <https://SourceForge.net/p/geographiclib/code/ci/release/tree/
715 python/geographiclib/geomath.py#l155>} and C++ U{sincosd
716 <https://SourceForge.net/p/geographiclib/code/ci/release/tree/
717 include/GeographicLib/Math.hpp#l558>}.
718 '''
719 if _isfinite(rad):
720 q = int(rad * _2__PI) # int(math.floor)
721 if q < 0:
722 q -= 1
723 t = _sin0cos2(q & 3, rad - q * PI_2, rad)
724 else:
725 t = NAN, NAN
726 return t
729def sincos2_(*rads):
730 '''Return the C{sine} and C{cosine} of angle(s) in C{radians}.
732 @arg rads: One or more angles (C{radians}).
734 @return: Yield the C{sin(B{rad})} and C{cos(B{rad})} for each angle.
736 @see: function L{sincos2}.
737 '''
738 for r in rads:
739 s, c = sincos2(r)
740 yield s
741 yield c
744def sincos2d(deg, **adeg):
745 '''Return the C{sine} and C{cosine} of an angle in C{degrees}.
747 @arg deg: Angle (C{degrees}).
748 @kwarg adeg: Optional correction (C{degrees}).
750 @return: 2-Tuple (C{sin(B{deg_})}, C{cos(B{deg_})}, C{B{deg_} =
751 B{deg} + B{adeg}}).
753 @see: U{GeographicLib<https://GeographicLib.SourceForge.io/C++/doc/
754 classGeographicLib_1_1Math.html#sincosd>} function U{sincosd
755 <https://SourceForge.net/p/geographiclib/code/ci/release/tree/
756 python/geographiclib/geomath.py#l155>} and C++ U{sincosd
757 <https://SourceForge.net/p/geographiclib/code/ci/release/tree/
758 include/GeographicLib/Math.hpp#l558>}.
759 '''
760 if _isfinite(deg):
761 q = int(deg * _1__90) # int(math.floor)
762 if q < 0:
763 q -= 1
764 d = deg - q * _90_0
765 if adeg:
766 t = _xkwds_get(adeg, adeg=_0_0)
767 d = _MODS.karney._around(d + t)
768 t = _sin0cos2(q & 3, radians(d), deg)
769 else:
770 t = NAN, NAN
771 return t
774def sincos2d_(*degs):
775 '''Return the C{sine} and C{cosine} of angle(s) in C{degrees}.
777 @arg degs: One or more angles (C{degrees}).
779 @return: Yield the C{sin(B{deg})} and C{cos(B{deg})} for each angle.
781 @see: Function L{sincos2d}.
782 '''
783 for d in degs:
784 s, c = sincos2d(d)
785 yield s
786 yield c
789def sincostan3(rad):
790 '''Return the C{sine}, C{cosine} and C{tangent} of an angle in C{radians}.
792 @arg rad: Angle (C{radians}).
794 @return: 3-Tuple (C{sin(B{rad})}, C{cos(B{rad})}, C{tan(B{rad})}).
796 @see: Function L{sincos2}.
797 '''
798 s, c = sincos2(float(rad))
799 t = NAN if s is NAN else (_over(s, c) if s else neg(s, neg0=c < 0))
800 return s, c, t
803def SM2m(sm):
804 '''Convert statute miles to meter (m).
806 @arg sm: Value in statute miles (C{scalar}).
808 @return: Value in meter (C{float}).
810 @raise ValueError: Invalid B{C{sm}}.
811 '''
812 return Meter(Float(sm=sm) * _M_SM)
815def tan_2(rad, **semi): # edge=1
816 '''Compute the tangent of half angle.
818 @arg rad: Angle (C{radians}).
819 @kwarg semi: Angle or edge name and index
820 for semi-circular error.
822 @return: M{tan(rad / 2)} (C{float}).
824 @raise ValueError: If B{C{rad}} is semi-circular
825 and B{C{semi}} is given.
826 '''
827 # .formy.excessKarney_, .sphericalTrigonometry.areaOf
828 if semi and isnear0(fabs(rad) - PI):
829 for n, v in semi.items():
830 break
831 n = _SPACE_(n, _radians_) if not isint(v) else \
832 _SPACE_(_MODS.streprs.Fmt.SQUARE(**semi), _edge_)
833 raise _ValueError(n, rad, txt=_semi_circular_)
835 return tan(rad * _0_5) if _isfinite(rad) else NAN
838def tand(deg, **error_kwds):
839 '''Return the C{tangent} of an angle in C{degrees}.
841 @arg deg: Angle (C{degrees}).
842 @kwarg error_kwds: Error to raise (C{ValueError}).
844 @return: C{tan(B{deg})}.
846 @raise ValueError: If L{pygeodesy.isnear0}C{(cos(B{deg})}.
847 '''
848 s, c = sincos2d(deg)
849 if s:
850 if isnear0(c):
851 raise _valueError(tand, deg, **error_kwds)
852 s = s / c # /= chokes PyChecker
853 elif c < 0:
854 s = -s # negate-0
855 return s
858def tand_(*degs, **error_kwds):
859 '''Return the C{tangent} of angle(s) in C{degrees}.
861 @arg degs: One or more angles (C{degrees}).
862 @kwarg error_kwds: Error to raise (C{ValueError}).
864 @return: Yield the C{tan(B{deg})} for each angle.
866 @raise ValueError: See L{pygeodesy.tand}.
867 '''
868 for d in degs:
869 yield tand(d, **error_kwds)
872def tanPI_2_2(rad):
873 '''Compute the tangent of half angle, 90 degrees rotated.
875 @arg rad: Angle (C{radians}).
877 @return: M{tan((rad + PI/2) / 2)} (C{float}).
878 '''
879 return tan((rad + PI_2) * _0_5) if _isfinite(rad) else (
880 NAN if isnan(rad) else (_N_90_0 if rad < 0 else _90_0))
883def toise2m(toises):
884 '''Convert French U{toises<https://WikiPedia.org/wiki/Toise>} to meter.
886 @arg toises: Value in toises (C{scalar}).
888 @return: Value in C{meter} (C{float}).
890 @raise ValueError: Invalid B{C{toises}}.
892 @see: Function L{fathom2m}.
893 '''
894 return Meter(Float(toises=toises) * _M_TOISE)
897def truncate(x, ndigits=None):
898 '''Truncate to the given number of digits.
900 @arg x: Value to truncate (C{scalar}).
901 @kwarg ndigits: Number of digits (C{int}),
902 aka I{precision}.
904 @return: Truncated B{C{x}} (C{float}).
906 @see: Python function C{round}.
907 '''
908 if isint(ndigits):
909 p = _10_0**ndigits
910 x = int(x * p) / p
911 return x
914def unroll180(lon1, lon2, wrap=True):
915 '''Unroll longitudinal delta and wrap longitude in degrees.
917 @arg lon1: Start longitude (C{degrees}).
918 @arg lon2: End longitude (C{degrees}).
919 @kwarg wrap: If C{True}, wrap and unroll to the M{(-180..+180]}
920 range (C{bool}).
922 @return: 2-Tuple C{(B{lon2}-B{lon1}, B{lon2})} unrolled (C{degrees},
923 C{degrees}).
925 @see: Capability C{LONG_UNROLL} in U{GeographicLib
926 <https://GeographicLib.SourceForge.io/html/python/interface.html#outmask>}.
927 '''
928 d = lon2 - lon1
929 if wrap:
930 u = wrap180(d)
931 if u != d:
932 return u, (lon1 + u)
933 return d, lon2
936def _unrollon(p1, p2, wrap=False): # unroll180 == .karney._unroll2
937 '''(INTERNAL) Wrap/normalize, unroll and replace longitude.
938 '''
939 lat, lon = p2.lat, p2.lon
940 if wrap and _Wrap.normal:
941 lat, lon = _Wrap.latlon(lat, lon)
942 _, lon = unroll180(p1.lon, lon, wrap=True)
943 if lat != p2.lat or fabs(lon - p2.lon) > EPS:
944 p2 = p2.dup(lat=lat, lon=wrap180(lon))
945 # p2 = p2.copy(); p2.latlon = lat, wrap180(lon)
946 return p2
949def _unrollon3(p1, p2, p3, wrap=False):
950 '''(INTERNAL) Wrap/normalize, unroll 2 points.
951 '''
952 w = wrap
953 if w:
954 w = _Wrap.normal
955 p2 = _unrollon(p1, p2, wrap=w)
956 p3 = _unrollon(p1, p3, wrap=w)
957 p2 = _unrollon(p2, p3)
958 return p2, p3, w # was wrapped?
961def unrollPI(rad1, rad2, wrap=True):
962 '''Unroll longitudinal delta and wrap longitude in radians.
964 @arg rad1: Start longitude (C{radians}).
965 @arg rad2: End longitude (C{radians}).
966 @kwarg wrap: If C{True}, wrap and unroll to the M{(-PI..+PI]}
967 range (C{bool}).
969 @return: 2-Tuple C{(B{rad2}-B{rad1}, B{rad2})} unrolled
970 (C{radians}, C{radians}).
972 @see: Capability C{LONG_UNROLL} in U{GeographicLib
973 <https://GeographicLib.SourceForge.io/html/python/interface.html#outmask>}.
974 '''
975 r = rad2 - rad1
976 if wrap:
977 u = wrapPI(r)
978 if u != r:
979 return u, (rad1 + u)
980 return r, rad2
983def _valueError(where, x, **kwds):
984 '''(INTERNAL) Return a C{_ValueError}.
985 '''
986 t = _MODS.streprs.Fmt.PAREN(where.__name__, x)
987 return _ValueError(t, **kwds)
990class _Wrap(object):
992 _normal = False # default
994 @property
995 def normal(self):
996 '''Get the current L{normal} setting (C{True},
997 C{False} or C{None}).
998 '''
999 return self._normal
1001 @normal.setter # PYCHOK setter!
1002 def normal(self, setting):
1003 '''Set L{normal} to C{True}, C{False} or C{None}.
1004 '''
1005 t = {True: (_MODS.formy.normal, _MODS.formy.normal_),
1006 False: (self.wraplatlon, self.wraphilam),
1007 None: (_passargs, _passargs)}.get(setting, ())
1008 if t:
1009 self.latlon, self.philam = t
1010 self._normal = setting
1012 def latlonDMS2(self, lat, lon, **DMS2_kwds):
1013 if isstr(lat) or isstr(lon):
1014 kwds = _xkwds(DMS2_kwds, clipLon=0, clipLat=0)
1015 lat, lon = _MODS.dms.parseDMS2(lat, lon, **kwds)
1016 return self.latlon(lat, lon)
1018# def normalatlon(self, *latlon):
1019# return _MODS.formy.normal(*latlon)
1021# def normalamphi(self, *philam):
1022# return _MODS.formy.normal_(*philam)
1024 def wraplatlon(self, lat, lon):
1025 return wrap90(lat), wrap180(lon)
1027 latlon = wraplatlon # default
1029 def latlon3(self, lon1, lat2, lon2, wrap):
1030 if wrap:
1031 lat2, lon2 = self.latlon(lat2, lon2)
1032 lon21, lon2 = unroll180(lon1, lon2)
1033 else:
1034 lon21 = lon2 - lon1
1035 return lon21, lat2, lon2
1037 def _latlonop(self, wrap):
1038 if wrap and self._normal is not None:
1039 return self.latlon
1040 else:
1041 return _passargs
1043 def wraphilam(self, phi, lam):
1044 return wrapPI_2(phi), wrapPI(lam)
1046 philam = wraphilam # default
1048 def philam3(self, lam1, phi2, lam2, wrap):
1049 if wrap:
1050 phi2, lam2 = self.philam(phi2, lam2)
1051 lam21, lam2 = unrollPI(lam1, lam2)
1052 else:
1053 lam21 = lam2 - lam1
1054 return lam21, phi2, lam2
1056 def _philamop(self, wrap):
1057 if wrap and self._normal is not None:
1058 return self.philam
1059 else:
1060 return _passargs
1062 def point(self, ll, wrap=True): # in .points._fractional, -.PointsIter.iterate, ...
1063 '''Return C{ll} or a copy, I{normalized} or I{wrap}'d.
1064 '''
1065 if wrap and self._normal is not None:
1066 lat, lon = ll.latlon
1067 if fabs(lon) > 180 or fabs(lat) > 90:
1068 _n = self.latlon
1069 ll = ll.copy(name=_n.__name__)
1070 ll.latlon = _n(lat, lon)
1071 return ll
1073_Wrap = _Wrap() # PYCHOK singleton
1076# def _wrap(angle, wrap, modulo):
1077# '''(INTERNAL) Angle wrapper M{((wrap-modulo)..+wrap]}.
1078#
1079# @arg angle: Angle (C{degrees}, C{radians} or C{grades}).
1080# @arg wrap: Range (C{degrees}, C{radians} or C{grades}).
1081# @arg modulo: Upper limit (360 C{degrees}, PI2 C{radians} or 400 C{grades}).
1082#
1083# @return: The B{C{angle}}, wrapped (C{degrees}, C{radians} or C{grades}).
1084# '''
1085# a = float(angle)
1086# if not (wrap - modulo) <= a < wrap:
1087# # math.fmod(-1.5, 3.14) == -1.5, but -1.5 % 3.14 == 1.64
1088# # math.fmod(-1.5, 360) == -1.5, but -1.5 % 360 == 358.5
1089# a %= modulo
1090# if a > wrap:
1091# a -= modulo
1092# return a
1095def wrap90(deg):
1096 '''Wrap degrees to M{[-90..+90]}.
1098 @arg deg: Angle (C{degrees}).
1100 @return: Degrees, wrapped (C{degrees90}).
1101 '''
1102 w = wrap180(deg)
1103 return (w - _180_0) if w > 90 else ((w + _180_0) if w < -90 else w)
1106def wrap180(deg):
1107 '''Wrap degrees to M{[-180..+180]}.
1109 @arg deg: Angle (C{degrees}).
1111 @return: Degrees, wrapped (C{degrees180}).
1112 '''
1113 d = float(deg)
1114 w = _umod_360(d)
1115 if w > 180:
1116 w -= _360_0
1117 elif d < 0 and w == 180:
1118 w = -w
1119 return w
1122def wrap360(deg): # see .streprs._umod_360
1123 '''Wrap degrees to M{[0..+360)}.
1125 @arg deg: Angle (C{degrees}).
1127 @return: Degrees, wrapped (C{degrees360}).
1128 '''
1129 return _umod_360(float(deg))
1132def wrapPI(rad):
1133 '''Wrap radians to M{[-PI..+PI]}.
1135 @arg rad: Angle (C{radians}).
1137 @return: Radians, wrapped (C{radiansPI}).
1138 '''
1139 r = float(rad)
1140 w = _umod_PI2(r)
1141 if w > PI:
1142 w -= PI2
1143 elif r < 0 and w == PI:
1144 w = -PI
1145 return w
1148def wrapPI2(rad):
1149 '''Wrap radians to M{[0..+2PI)}.
1151 @arg rad: Angle (C{radians}).
1153 @return: Radians, wrapped (C{radiansPI2}).
1154 '''
1155 return _umod_PI2(float(rad))
1158def wrapPI_2(rad):
1159 '''Wrap radians to M{[-PI/2..+PI/2]}.
1161 @arg rad: Angle (C{radians}).
1163 @return: Radians, wrapped (C{radiansPI_2}).
1164 '''
1165 w = wrapPI(rad)
1166 return (w - PI) if w > PI_2 else ((w + PI) if w < (-PI_2) else w)
1169# def wraplatlon(lat, lon):
1170# '''Both C{wrap90(B{lat})} and C{wrap180(B{lon})}.
1171# '''
1172# return wrap90(lat), wrap180(lon)
1175def wrap_normal(*normal):
1176 '''Define the operation for the keyword argument C{B{wrap}=True},
1177 across L{pygeodesy}: I{wrap}, I{normalize} or I{no-op}. For
1178 backward compatibility, the default is I{wrap}.
1180 @arg normal: If C{True}, I{normalize} lat- and longitude using
1181 L{normal} or L{normal_}, if C{False}, I{wrap} the
1182 lat- and longitude individually by L{wrap90} or
1183 L{wrapPI_2} respectively L{wrap180}, L{wrapPI} or
1184 if C{None}, leave lat- and longitude I{unchanged}.
1185 Do not supply any value to get the current setting.
1187 @return: The previous L{wrap_normal} setting (C{bool} or C{None}).
1188 '''
1189 t = _Wrap.normal
1190 if normal:
1191 _Wrap.normal = normal[0]
1192 return t
1195# def wraphilam(phi, lam,):
1196# '''Both C{wrapPI_2(B{phi})} and C{wrapPI(B{lam})}.
1197# '''
1198# return wrapPI_2(phi), wrapPI(lam)
1201def yard2m(yards):
1202 '''Convert I{UK} yards to meter.
1204 @arg yards: Value in yards (C{scalar}).
1206 @return: Value in C{meter} (C{float}).
1208 @raise ValueError: Invalid B{C{yards}}.
1209 '''
1210 return Float(yards=yards) * _M_YARD_UK
1212# **) MIT License
1213#
1214# Copyright (C) 2016-2024 -- mrJean1 at Gmail -- All Rights Reserved.
1215#
1216# Permission is hereby granted, free of charge, to any person obtaining a
1217# copy of this software and associated documentation files (the "Software"),
1218# to deal in the Software without restriction, including without limitation
1219# the rights to use, copy, modify, merge, publish, distribute, sublicense,
1220# and/or sell copies of the Software, and to permit persons to whom the
1221# Software is furnished to do so, subject to the following conditions:
1222#
1223# The above copyright notice and this permission notice shall be included
1224# in all copies or substantial portions of the Software.
1225#
1226# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
1227# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
1228# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
1229# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
1230# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
1231# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
1232# OTHER DEALINGS IN THE SOFTWARE.