Coverage for pygeodesy/utily.py: 91%
360 statements
« prev ^ index » next coverage.py v7.6.1, created at 2025-04-25 13:15 -0400
« prev ^ index » next coverage.py v7.6.1, created at 2025-04-25 13:15 -0400
2# -*- coding: utf-8 -*-
4u'''Various utility functions.
6After I{Karney}'s C++ U{Math<https://GeographicLib.SourceForge.io/C++/doc/classGeographicLib_1_1Math.html>}
7class and I{Veness}' U{Latitude/Longitude<https://www.Movable-Type.co.UK/scripts/latlong.html>}
8and U{Vector-based geodesy<https://www.Movable-Type.co.UK/scripts/latlong-vectors.html>}
9and published under the same MIT Licence**.
10'''
11# make sure int/int division yields float quotient, see .basics
12from __future__ import division as _; del _ # PYCHOK semicolon
14from pygeodesy.basics import _copysign, isinstanceof, isint, isstr
15from pygeodesy.constants import EPS, EPS0, INF, NAN, PI, PI2, PI_2, R_M, \
16 _M_KM, _M_NM, _M_SM, _0_0, _1__90, _0_5, _2__PI, \
17 _1_0, _N_1_0, _10_0, _90_0, _180_0, _360_0, \
18 _copysign_0_0, _float, _isfinite, isnan, isnear0, \
19 _over, _umod_360, _umod_PI2
20from pygeodesy.errors import _ValueError, _xkwds, _ALL_LAZY, _MODS
21from pygeodesy.internals import _passargs, typename
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, Lamd, \
25 Meter, Meter2, Radians # Radians_
27from math import acos, asin, atan2 as _atan2, cos, degrees, fabs, radians, \
28 sin, tan as _tan # pow
30__all__ = _ALL_LAZY.utily
31__version__ = '25.04.14'
33_G_DEG = _float( 400.0 / _360_0) # grades per degree
34_G_RAD = _float( 400.0 / PI2) # grades per radian
35_M_ACRE = _float( 4046.8564224) # square meter per acre, chain2m(1) * furlong2m(1)
36_M_CHAIN = _float( 20.1168) # meter per yard2m(1) * 22
37_M_FATHOM = _float( 1.8288) # meter per yard2m(1) * 2 or _M_NM * 1e-3
38_M_FOOT = _float( 0.3048) # meter per Int'l foot, 1 / 3.280_839_895_0131 = 10_000 / (254 * 12)
39_M_FOOT_GE = _float( 0.31608) # meter per German Fuss, 1 / 3.163_756_011_1364
40_M_FOOT_FR = _float( 0.3248406) # meter per French Pied-du-Roi or pied, 1 / 3.078_432_929_8739
41_M_FOOT_US = _float( 0.3048006096012192) # meter per US Survey foot, 1_200 / 3_937
42_M_FURLONG = _float( 201.168) # meter per furlong, 220 * yard2m(1) = 10 * m2chain(1)
43_M_HA = _float(10000.0) # square meter per hectare, 100 * 100
44# _M_KM = _float( 1000.0) # meter per kilo meter
45# _M_NM = _float( 1852.0) # meter per nautical mile
46# _M_SM = _float( 1609.344) # meter per statute mile
47_M_TOISE = _float( 1.9490436) # meter per French toise, 6 pieds = 6 / 3.078_432_929_8739
48_M_YARD_UK = _float( 0.9144) # meter per yard, 254 * 12 * 3 / 10_000 = 3 * _M_FOOT
49# sqrt(3) <https://WikiPedia.org/wiki/Square_root_of_3>
50_COS_30, _SIN_30 = 0.86602540378443864676, _0_5 # sqrt(3) / 2
51_COS_45 = _SIN_45 = 0.70710678118654752440 # sqrt(2) / 2
54def _abs1nan(x):
55 '''(INTERNAL) Bracket C{x}.
56 '''
57 return _N_1_0 < x < _1_0 or isnan(x)
60def acos1(x):
61 '''Return C{math.acos(max(-1, min(1, B{x})))}.
62 '''
63 return acos(x) if _abs1nan(x) else (PI if x < 0 else _0_0)
66def acre2ha(acres):
67 '''Convert acres to hectare.
69 @arg acres: Value in acres (C{scalar}).
71 @return: Value in C{hectare} (C{float}).
73 @raise ValueError: Invalid B{C{acres}}.
74 '''
75 return m2ha(acre2m2(acres))
78def acre2m2(acres):
79 '''Convert acres to I{square} meter.
81 @arg acres: Value in acres (C{scalar}).
83 @return: Value in C{meter^2} (C{float}).
85 @raise ValueError: Invalid B{C{acres}}.
86 '''
87 return Meter2(Float(acres=acres) * _M_ACRE)
90def asin1(x):
91 '''Return C{math.asin(max(-1, min(1, B{x})))}.
92 '''
93 return asin(x) if _abs1nan(x) else _copysign(PI_2, x)
96def atan1(y, x=_1_0):
97 '''Return C{atan(B{y} / B{x})} angle in C{radians} M{[-PI/2..+PI/2]}
98 using C{atan2} for consistency and to avoid C{ZeroDivisionError}.
99 '''
100 return _atan1u(y, x, atan2)
103def atan1d(y, x=_1_0):
104 '''Return C{atan(B{y} / B{x})} angle in C{degrees} M{[-90..+90]}
105 using C{atan2d} for consistency and to avoid C{ZeroDivisionError}.
107 @see: Function L{pygeodesy.atan2d}.
108 '''
109 return _atan1u(y, x, atan2d)
112def _atan1u(y, x, _2u):
113 '''(INTERNAL) Helper for functions C{atan1} and C{atan1d}.
114 '''
115 if x < 0:
116 x = -x
117 y = -y
118 return _2u(y, x or _0_0)
121atan2 = _atan2
122'''Return C{atan2(B{y}, B{x})} in radians M{[-PI..+PI]}.
124 @see: I{Karney}'s C++ function U{Math.atan2d
125 <https://GeographicLib.SourceForge.io/C++/doc/classGeographicLib_1_1Math.html>}.
126'''
129def atan2b(y, x):
130 '''Return C{atan2(B{y}, B{x})} in degrees M{[0..+360], counter-clockwise}.
132 @see: Function L{pygeodesy.atan2d}.
133 '''
134 b = atan2d(y, x)
135 if b < 0:
136 b += _360_0
137 return b or _0_0 # unsigned-0
140def atan2d(y, x, reverse=False):
141 '''Return C{atan2(B{y}, B{x})} in degrees M{[-180..+180]},
142 optionally I{reversed} (by 180 degrees for C{azimuth}s).
144 @see: I{Karney}'s C++ function U{Math.atan2d
145 <https://GeographicLib.SourceForge.io/C++/doc/classGeographicLib_1_1Math.html>}.
146 '''
147 d = degrees(_atan2(y, x)) # preserves signed-0
148 return _azireversed(d) if reverse else d
151def _azireversed(azi): # in .rhumbBase
152 '''(INTERNAL) Return the I{reverse} B{C{azi}} in degrees M{[-180..+180]}.
153 '''
154 return azi - _copysign(_180_0, azi)
157def chain2m(chains):
158 '''Convert I{UK} chains to meter.
160 @arg chains: Value in chains (C{scalar}).
162 @return: Value in C{meter} (C{float}).
164 @raise ValueError: Invalid B{C{chains}}.
165 '''
166 return Meter(Float(chains=chains) * _M_CHAIN)
169def circle4(earth, lat):
170 '''Get the equatorial or a parallel I{circle of latitude}.
172 @arg earth: The earth radius (C{meter}), ellipsoid or datum
173 (L{Ellipsoid}, L{Ellipsoid2}, L{Datum} or L{a_f2Tuple}).
174 @arg lat: Geodetic latitude (C{degrees90}, C{str}).
176 @return: A L{Circle4Tuple}C{(radius, height, lat, beta)}.
178 @raise RangeError: Latitude B{C{lat}} outside valid range and
179 L{rangerrors<pygeodesy.rangerrors>} is C{True}.
181 @raise TypeError: Invalid B{C{earth}}.
183 @raise ValueError: B{C{earth}} or B{C{lat}}.
184 '''
185 E = _MODS.datums._earth_ellipsoid(earth)
186 return E.circle4(lat)
189def cot(rad, **raiser_kwds):
190 '''Return the C{cotangent} of an angle in C{radians}.
192 @arg rad: Angle (C{radians}).
193 @kwarg raiser_kwds: Use C{B{raiser}=False} to avoid
194 ValueErrors or optionally, additional
195 ValueError keyword argments.
197 @return: C{cot(B{rad})}.
199 @raise Error: If L{pygeodesy.isnear0}C{(sin(B{rad})}.
200 '''
201 try:
202 return _cotu(*sincos2(rad), **raiser_kwds)
203 except ZeroDivisionError:
204 raise _valueError(cot, rad, **raiser_kwds)
207def cot_(*rads, **raiser_kwds):
208 '''Yield the C{cotangent} of angle(s) in C{radians}.
210 @arg rads: One or more angles (each in C{radians}).
212 @return: Yield C{cot(B{rad})} for each angle.
214 @see: Function L{pygeodesy.cot} for further details.
215 '''
216 try:
217 for r in rads:
218 yield _cotu(*sincos2(r), **raiser_kwds)
219 except ZeroDivisionError:
220 raise _valueError(cot_, r, **raiser_kwds)
223def cotd(deg, **raiser_kwds):
224 '''Return the C{cotangent} of an angle in C{degrees}.
226 @arg deg: Angle (C{degrees}).
227 @kwarg raiser_kwds: Use C{B{raiser}=False} to avoid
228 ValueErrors or optionally, additional
229 ValueError keyword argments.
231 @return: C{cot(B{deg})}.
233 @raise Error: If L{pygeodesy.isnear0}C{(sin(B{deg})}.
234 '''
235 try:
236 return _cotu(*sincos2d(deg), **raiser_kwds)
237 except ZeroDivisionError:
238 raise _valueError(cotd, deg, **raiser_kwds)
241def cotd_(*degs, **raiser_kwds):
242 '''Yield the C{cotangent} of angle(s) in C{degrees}.
244 @arg degs: One or more angles (each in C{degrees}).
246 @return: Yield C{cotd(B{deg})} for each angle.
248 @see: Function L{pygeodesy.cotd} for further details.
249 '''
250 try:
251 for d in degs:
252 yield _cotu(*sincos2d(d), **raiser_kwds)
253 except ZeroDivisionError:
254 raise _valueError(cotd_, d, **raiser_kwds)
257def _cotu(s, c, **raiser_kwds):
258 '''(INTERNAL) Helper for functions C{cot}, C{cotd}, C{cot_} and C{cotd_}.
259 '''
260 return _tanu(c, s, **raiser_kwds)
263def degrees90(rad):
264 '''Convert radians to degrees and wrap M{[-90..+90)}.
266 @arg rad: Angle (C{radians}).
268 @return: Angle, wrapped (C{degrees90}).
269 '''
270 return wrap90(degrees(rad))
273def degrees180(rad):
274 '''Convert radians to degrees and wrap M{[-180..+180)}.
276 @arg rad: Angle (C{radians}).
278 @return: Angle, wrapped (C{degrees180}).
279 '''
280 return wrap180(degrees(rad))
283def degrees360(rad):
284 '''Convert radians to degrees and wrap M{[0..+360)}.
286 @arg rad: Angle (C{radians}).
288 @return: Angle, wrapped (C{degrees360}).
289 '''
290 return _umod_360(degrees(rad))
293def degrees2grades(deg):
294 '''Convert degrees to I{grades} (aka I{gons} or I{gradians}).
296 @arg deg: Angle (C{degrees}).
298 @return: Angle (C{grades}).
299 '''
300 return Float(grades=Degrees(deg) * _G_DEG)
303def degrees2m(deg, radius=R_M, lat=0):
304 '''Convert an angle to a distance along the equator or along a parallel
305 at (geodetic) latitude.
307 @arg deg: The angle (C{degrees}).
308 @kwarg radius: Mean earth radius (C{meter}), an ellipsoid or datum
309 (L{Ellipsoid}, L{Ellipsoid2}, L{Datum} or L{a_f2Tuple}).
310 @kwarg lat: Parallel latitude (C{degrees90}, C{str}).
312 @return: Distance (C{meter}, same units as B{C{radius}} or polar and
313 equatorial radii) or C{0.0} for near-polar B{C{lat}}.
315 @raise RangeError: Latitude B{C{lat}} outside valid range and
316 L{rangerrors<pygeodesy.rangerrors>} is C{True}.
318 @raise TypeError: Invalid B{C{radius}}.
320 @raise ValueError: Invalid B{C{deg}}, B{C{radius}} or B{C{lat}}.
322 @see: Function L{radians2m} and L{m2degrees}.
323 '''
324 return _Radians2m(Lamd(deg=deg, clip=0), radius, lat)
327def fathom2m(fathoms):
328 '''Convert I{Imperial} fathom to meter.
330 @arg fathoms: Value in fathoms (C{scalar}).
332 @return: Value in C{meter} (C{float}).
334 @raise ValueError: Invalid B{C{fathoms}}.
336 @see: Function L{toise2m}, U{Fathom<https://WikiPedia.org/wiki/Fathom>}
337 and U{Klafter<https://WikiPedia.org/wiki/Klafter>}.
338 '''
339 return Meter(Float(fathoms=fathoms) * _M_FATHOM)
342def ft2m(feet, usurvey=False, pied=False, fuss=False):
343 '''Convert I{International}, I{US Survey}, I{French} or I{German}
344 B{C{feet}} to C{meter}.
346 @arg feet: Value in feet (C{scalar}).
347 @kwarg usurvey: If C{True}, convert I{US Survey} foot else ...
348 @kwarg pied: If C{True}, convert French I{pied-du-Roi} else ...
349 @kwarg fuss: If C{True}, convert German I{Fuss}, otherwise
350 I{International} foot to C{meter}.
352 @return: Value in C{meter} (C{float}).
354 @raise ValueError: Invalid B{C{feet}}.
355 '''
356 return Meter(Feet(feet) * (_M_FOOT_US if usurvey else
357 (_M_FOOT_FR if pied else
358 (_M_FOOT_GE if fuss else _M_FOOT))))
361def furlong2m(furlongs):
362 '''Convert a furlong to meter.
364 @arg furlongs: Value in furlongs (C{scalar}).
366 @return: Value in C{meter} (C{float}).
368 @raise ValueError: Invalid B{C{furlongs}}.
369 '''
370 return Meter(Float(furlongs=furlongs) * _M_FURLONG)
373def grades(rad):
374 '''Convert radians to I{grades} (aka I{gons} or I{gradians}).
376 @arg rad: Angle (C{radians}).
378 @return: Angle (C{grades}).
379 '''
380 return Float(grades=Radians(rad) * _G_RAD)
383def grades400(rad):
384 '''Convert radians to I{grades} (aka I{gons} or I{gradians}) and wrap M{[0..+400)}.
386 @arg rad: Angle (C{radians}).
388 @return: Angle, wrapped (C{grades}).
389 '''
390 return Float(grades400=wrapPI2(rad) * _G_RAD)
393def grades2degrees(gon):
394 '''Convert I{grades} (aka I{gons} or I{gradians}) to C{degrees}.
396 @arg gon: Angle (C{grades}).
398 @return: Angle (C{degrees}).
399 '''
400 return Degrees(Float(gon=gon) / _G_DEG)
403def grades2radians(gon):
404 '''Convert I{grades} (aka I{gons} or I{gradians}) to C{radians}.
406 @arg gon: Angle (C{grades}).
408 @return: Angle (C{radians}).
409 '''
410 return Radians(Float(gon=gon) / _G_RAD)
413def ha2acre(ha):
414 '''Convert hectare to acre.
416 @arg ha: Value in hectare (C{scalar}).
418 @return: Value in acres (C{float}).
420 @raise ValueError: Invalid B{C{ha}}.
421 '''
422 return m2acre(ha2m2(ha))
425def ha2m2(ha):
426 '''Convert hectare to I{square} meter.
428 @arg ha: Value in hectare (C{scalar}).
430 @return: Value in C{meter^2} (C{float}).
432 @raise ValueError: Invalid B{C{ha}}.
433 '''
434 return Meter2(Float(ha=ha) * _M_HA)
437def hav(rad):
438 '''Return the U{haversine<https://WikiPedia.org/wiki/Haversine_formula>} of an angle.
440 @arg rad: Angle (C{radians}).
442 @return: C{sin(B{rad} / 2)**2}.
443 '''
444 return sin(rad * _0_5)**2
447def km2m(km):
448 '''Convert kilo meter to meter (m).
450 @arg km: Value in kilo meter (C{scalar}).
452 @return: Value in meter (C{float}).
454 @raise ValueError: Invalid B{C{km}}.
455 '''
456 return Meter(Float(km=km) * _M_KM)
459def _loneg(lon):
460 '''(INTERNAL) "Complement" of C{lon}.
461 '''
462 return _180_0 - lon
465def m2acre(meter2):
466 '''Convert I{square} meter to acres.
468 @arg meter2: Value in C{meter^2} (C{scalar}).
470 @return: Value in acres (C{float}).
472 @raise ValueError: Invalid B{C{meter2}}.
473 '''
474 return Float(acre=Meter2(meter2) / _M_ACRE)
477def m2chain(meter):
478 '''Convert meter to I{UK} chains.
480 @arg meter: Value in meter (C{scalar}).
482 @return: Value in C{chains} (C{float}).
484 @raise ValueError: Invalid B{C{meter}}.
485 '''
486 return Float(chain=Meter(meter) / _M_CHAIN) # * 0.049_709_695_378_986_715
489def m2degrees(distance, radius=R_M, lat=0):
490 '''Convert a distance to an angle along the equator or along a parallel
491 at (geodetic) latitude.
493 @arg distance: Distance (C{meter}, same units as B{C{radius}}).
494 @kwarg radius: Mean earth radius (C{meter}), an ellipsoid or datum
495 (L{Ellipsoid}, L{Ellipsoid2}, L{Datum} or L{a_f2Tuple}).
496 @kwarg lat: Parallel latitude (C{degrees90}, C{str}).
498 @return: Angle (C{degrees}) or C{INF} for near-polar B{C{lat}}.
500 @raise RangeError: Latitude B{C{lat}} outside valid range and
501 L{rangerrors<pygeodesy.rangerrors>} is C{True}.
503 @raise TypeError: Invalid B{C{radius}}.
505 @raise ValueError: Invalid B{C{distance}}, B{C{radius}} or B{C{lat}}.
507 @see: Function L{m2radians} and L{degrees2m}.
508 '''
509 return degrees(m2radians(distance, radius=radius, lat=lat))
512def m2fathom(meter):
513 '''Convert meter to I{Imperial} fathoms.
515 @arg meter: Value in meter (C{scalar}).
517 @return: Value in C{fathoms} (C{float}).
519 @raise ValueError: Invalid B{C{meter}}.
521 @see: Function L{m2toise}, U{Fathom<https://WikiPedia.org/wiki/Fathom>}
522 and U{Klafter<https://WikiPedia.org/wiki/Klafter>}.
523 '''
524 return Float(fathom=Meter(meter) / _M_FATHOM) # * 0.546_806_649
527def m2ft(meter, usurvey=False, pied=False, fuss=False):
528 '''Convert meter to I{International}, I{US Survey}, I{French} or
529 or I{German} feet (C{ft}).
531 @arg meter: Value in meter (C{scalar}).
532 @kwarg usurvey: If C{True}, convert to I{US Survey} foot else ...
533 @kwarg pied: If C{True}, convert to French I{pied-du-Roi} else ...
534 @kwarg fuss: If C{True}, convert to German I{Fuss}, otherwise to
535 I{International} foot.
537 @return: Value in C{feet} (C{float}).
539 @raise ValueError: Invalid B{C{meter}}.
540 '''
541 # * 3.280_833_333_333_3333, US Survey 3_937 / 1_200
542 # * 3.280_839_895_013_1235, Int'l 10_000 / (254 * 12)
543 return Float(feet=Meter(meter) / (_M_FOOT_US if usurvey else
544 (_M_FOOT_FR if pied else
545 (_M_FOOT_GE if fuss else _M_FOOT))))
548def m2furlong(meter):
549 '''Convert meter to furlongs.
551 @arg meter: Value in meter (C{scalar}).
553 @return: Value in C{furlongs} (C{float}).
555 @raise ValueError: Invalid B{C{meter}}.
556 '''
557 return Float(furlong=Meter(meter) / _M_FURLONG) # * 0.004_970_969_54
560def m2ha(meter2):
561 '''Convert I{square} meter to hectare.
563 @arg meter2: Value in C{meter^2} (C{scalar}).
565 @return: Value in hectare (C{float}).
567 @raise ValueError: Invalid B{C{meter2}}.
568 '''
569 return Float(ha=Meter2(meter2) / _M_HA)
572def m2km(meter):
573 '''Convert meter to kilo meter (Km).
575 @arg meter: Value in meter (C{scalar}).
577 @return: Value in Km (C{float}).
579 @raise ValueError: Invalid B{C{meter}}.
580 '''
581 return Float(km=Meter(meter) / _M_KM)
584def m2NM(meter):
585 '''Convert meter to nautical miles (NM).
587 @arg meter: Value in meter (C{scalar}).
589 @return: Value in C{NM} (C{float}).
591 @raise ValueError: Invalid B{C{meter}}.
592 '''
593 return Float(NM=Meter(meter) / _M_NM) # * 5.399_568_04e-4
596def m2radians(distance, radius=R_M, lat=0):
597 '''Convert a distance to an angle along the equator or along a parallel
598 at (geodetic) latitude.
600 @arg distance: Distance (C{meter}, same units as B{C{radius}}).
601 @kwarg radius: Mean earth radius (C{meter}, an ellipsoid or datum
602 (L{Ellipsoid}, L{Ellipsoid2}, L{Datum} or L{a_f2Tuple}).
603 @kwarg lat: Parallel latitude (C{degrees90}, C{str}).
605 @return: Angle (C{radians}) or C{INF} for near-polar B{C{lat}}.
607 @raise RangeError: Latitude B{C{lat}} outside valid range and
608 L{rangerrors<pygeodesy.rangerrors>} is C{True}.
610 @raise TypeError: Invalid B{C{radius}}.
612 @raise ValueError: Invalid B{C{distance}}, B{C{radius}} or B{C{lat}}.
614 @see: Function L{m2degrees} and L{radians2m}.
615 '''
616 m = circle4(radius, lat).radius
617 return INF if m < EPS0 else Radians(Float(distance=distance) / m)
620def m2SM(meter):
621 '''Convert meter to statute miles (SM).
623 @arg meter: Value in meter (C{scalar}).
625 @return: Value in C{SM} (C{float}).
627 @raise ValueError: Invalid B{C{meter}}.
628 '''
629 return Float(SM=Meter(meter) / _M_SM) # * 6.213_699_49e-4 == 1 / 1_609.344
632def m2toise(meter):
633 '''Convert meter to French U{toises<https://WikiPedia.org/wiki/Toise>}.
635 @arg meter: Value in meter (C{scalar}).
637 @return: Value in C{toises} (C{float}).
639 @raise ValueError: Invalid B{C{meter}}.
641 @see: Function L{m2fathom}.
642 '''
643 return Float(toise=Meter(meter) / _M_TOISE) # * 0.513_083_632_632_119
646def m2yard(meter):
647 '''Convert meter to I{UK} yards.
649 @arg meter: Value in meter (C{scalar}).
651 @return: Value in C{yards} (C{float}).
653 @raise ValueError: Invalid B{C{meter}}.
654 '''
655 return Float(yard=Meter(meter) / _M_YARD_UK) # * 1.093_613_298_337_707_8
658def NM2m(nm):
659 '''Convert nautical miles to meter (m).
661 @arg nm: Value in nautical miles (C{scalar}).
663 @return: Value in meter (C{float}).
665 @raise ValueError: Invalid B{C{nm}}.
666 '''
667 return Meter(Float(nm=nm) * _M_NM)
670def radians2m(rad, radius=R_M, lat=0):
671 '''Convert an angle to a distance along the equator or along a parallel
672 at (geodetic) latitude.
674 @arg rad: The angle (C{radians}).
675 @kwarg radius: Mean earth radius (C{meter}) or an ellipsoid or datum
676 (L{Ellipsoid}, L{Ellipsoid2}, L{Datum} or L{a_f2Tuple}).
677 @kwarg lat: Parallel latitude (C{degrees90}, C{str}).
679 @return: Distance (C{meter}, same units as B{C{radius}} or polar and
680 equatorial radii) or C{0.0} for near-polar B{C{lat}}.
682 @raise RangeError: Latitude B{C{lat}} outside valid range and
683 L{rangerrors<pygeodesy.rangerrors>} is C{True}.
685 @raise TypeError: Invalid B{C{radius}}.
687 @raise ValueError: Invalid B{C{rad}}, B{C{radius}} or B{C{lat}}.
689 @see: Function L{degrees2m} and L{m2radians}.
690 '''
691 return _Radians2m(Lam(rad=rad, clip=0), radius, lat)
694def _Radians2m(rad, radius, lat):
695 '''(INTERNAL) Helper for C{degrees2m} and C{radians2m}.
696 '''
697 m = circle4(radius, lat).radius
698 return _0_0 if m < EPS0 else (rad * m)
701def radiansPI(deg):
702 '''Convert and wrap degrees to radians M{[-PI..+PI]}.
704 @arg deg: Angle (C{degrees}).
706 @return: Radians, wrapped (C{radiansPI})
707 '''
708 return wrapPI(radians(deg))
711def radiansPI2(deg):
712 '''Convert and wrap degrees to radians M{[0..+2PI)}.
714 @arg deg: Angle (C{degrees}).
716 @return: Radians, wrapped (C{radiansPI2})
717 '''
718 return _umod_PI2(radians(deg))
721def radiansPI_2(deg):
722 '''Convert and wrap degrees to radians M{[-3PI/2..+PI/2]}.
724 @arg deg: Angle (C{degrees}).
726 @return: Radians, wrapped (C{radiansPI_2})
727 '''
728 return wrapPI_2(radians(deg))
731def _sin0cos2(q, r, sign, a, Q): # Quarter turn
732 '''(INTERNAL) 2-tuple (C{sin(r), cos(r)}) in quadrant C{0 <= B{q} <= 3} and
733 C{sin} zero I{signed} with B{C{sign}}, like Karney's U{Math.sind and .cosd
734 <https://GeographicLib.SourceForge.io/C++/doc/classGeographicLib_1_1Math.html>}
735 '''
736 if r < PI_2:
737 s = sin(r)
738 if (a * 2) == Q:
739 s, c = _copysign(_SIN_45, s), _COS_45
740 elif (a * 3) == Q:
741 s, c = _copysign(_SIN_30, s), _COS_30
742 else:
743 c = cos(r)
744 else:
745 s, c = _1_0, _0_0
746 t = s, c, -s, -c, s
747# q &= 3
748 s = t[q] or _copysign_0_0(sign)
749 c = t[q + 1] or _0_0
750 return s, c
753def SinCos2(x):
754 '''Get C{sin} and C{cos} of I{typed} angle.
756 @arg x: Angle (L{Degrees}, L{Radians} or scalar C{radians}).
758 @return: 2-Tuple (C{sin(B{x})}, C{cos(B{x})}).
759 '''
760 return sincos2d(x) if isinstanceof(x, Degrees, Degrees_) else (
761# sincos2(x) if isinstanceof(x, Radians, Radians_) else
762 sincos2(Radians(x))) # assume C{radians}
765def sincos2(rad):
766 '''Return the C{sine} and C{cosine} of an angle in C{radians}.
768 @arg rad: Angle (C{radians}).
770 @return: 2-Tuple (C{sin(B{rad})}, C{cos(B{rad})}).
772 @see: U{GeographicLib<https://GeographicLib.SourceForge.io/C++/doc/
773 classGeographicLib_1_1Math.html#sincosd>} function U{sincosd
774 <https://SourceForge.net/p/geographiclib/code/ci/release/tree/
775 python/geographiclib/geomath.py#l155>} and C++ U{sincosd
776 <https://SourceForge.net/p/geographiclib/code/ci/release/tree/
777 include/GeographicLib/Math.hpp#l558>}.
778 '''
779 if _isfinite(rad):
780 q = int(rad * _2__PI) # int(math.floor)
781 if q < 0:
782 q -= 1
783 r = rad - q * PI_2
784 t = _sin0cos2(q & 3, r, rad, fabs(r), PI_2)
785 else:
786 t = NAN, NAN
787 return t
790def sincos2_(*rads):
791 '''Yield the C{sine} and C{cosine} of angle(s) in C{radians}.
793 @arg rads: One or more angles (C{radians}).
795 @return: Yield C{sin(B{rad})} and C{cos(B{rad})} for each angle.
797 @see: Function L{sincos2}.
798 '''
799 for r in rads:
800 s, c = sincos2(r)
801 yield s
802 yield c
805def sincos2d(deg, adeg=_0_0):
806 '''Return the C{sine} and C{cosine} of an angle in C{degrees}.
808 @arg deg: Angle (C{degrees}).
809 @kwarg adeg: Optional correction (C{degrees}).
811 @return: 2-Tuple (C{sin(B{deg_})}, C{cos(B{deg_})}, C{B{deg_} =
812 B{deg} + B{adeg}}).
814 @see: U{GeographicLib<https://GeographicLib.SourceForge.io/C++/doc/
815 classGeographicLib_1_1Math.html#sincosd>} function U{sincosd
816 <https://SourceForge.net/p/geographiclib/code/ci/release/tree/
817 python/geographiclib/geomath.py#l155>} and C++ U{sincosd
818 <https://SourceForge.net/p/geographiclib/code/ci/release/tree/
819 include/GeographicLib/Math.hpp#l558>}.
820 '''
821 if _isfinite(deg):
822 q = int(deg * _1__90) # int(math.floor)
823 if q < 0:
824 q -= 1
825 d = deg - q * _90_0
826 if adeg:
827 d = _MODS.karney._around(d + adeg)
828 t = _sin0cos2(q & 3, radians(d), deg, fabs(d), _90_0)
829 else:
830 t = NAN, NAN
831 return t
834def sincos2d_(*degs):
835 '''Yield the C{sine} and C{cosine} of angle(s) in C{degrees}.
837 @arg degs: One or more angles (C{degrees}).
839 @return: Yield C{sind(B{deg})} and C{cosd(B{deg})} for each angle.
841 @see: Function L{sincos2d}.
842 '''
843 for d in degs:
844 s, c = sincos2d(d)
845 yield s
846 yield c
849def sincostan3(rad):
850 '''Return the C{sine}, C{cosine} and C{tangent} of an angle in C{radians}.
852 @arg rad: Angle (C{radians}).
854 @return: 3-Tuple (C{sin(B{rad})}, C{cos(B{rad})}, C{tan(B{rad})}).
856 @see: Function L{sincos2}.
857 '''
858 return _sincostan3(*sincos2(float(rad)))
861def _sincostan3(s, c):
862 '''(INTERNAL) Helper for C{sincostan3} and C{sincostan3d}.
863 '''
864 return s, c, _tanu(s, c, raiser=False)
867def sincostan3d(deg):
868 '''Return the C{sine}, C{cosine} and C{tangent} of an angle in C{degrees}.
870 @arg deg: Angle (C{degrees}).
872 @return: 3-Tuple (C{sind(B{deg})}, C{cosd(B{deg})}, C{tand(B{deg})}).
874 @see: Function L{sincos2d}.
875 '''
876 return _sincostan3(*sincos2d(float(deg)))
879def SM2m(sm):
880 '''Convert statute miles to meter (m).
882 @arg sm: Value in statute miles (C{scalar}).
884 @return: Value in meter (C{float}).
886 @raise ValueError: Invalid B{C{sm}}.
887 '''
888 return Meter(Float(sm=sm) * _M_SM)
891def tan_2(rad, **semi): # edge=1
892 '''Compute the tangent of half angle.
894 @arg rad: Angle (C{radians}).
895 @kwarg semi: Angle or edge name and index
896 for semi-circular error.
898 @return: M{tan(rad / 2)} (C{float}).
900 @raise ValueError: If B{C{rad}} is semi-circular
901 and B{C{semi}} is given.
902 '''
903 # .formy.excessKarney_, .sphericalTrigonometry.areaOf
904 if semi and isnear0(fabs(rad) - PI):
905 for n, v in semi.items():
906 break
907 n = _SPACE_(n, _radians_) if not isint(v) else \
908 _SPACE_(_MODS.streprs.Fmt.SQUARE(**semi), _edge_)
909 raise _ValueError(n, rad, txt=_semi_circular_)
911 return _tan(rad * _0_5) if _isfinite(rad) else NAN
914def tan(rad, **raiser_kwds):
915 '''Return the C{tangent} of an angle in C{radians}.
917 @arg rad: Angle (C{radians}).
918 @kwarg raiser_kwds: Use C{B{raiser}=False} to avoid
919 ValueErrors or optionally, additional
920 ValueError keyword argments.
922 @return: C{tan(B{rad})}.
924 @raise Error: If L{pygeodesy.isnear0}C{(cos(B{rad})}.
925 '''
926 try:
927 return _tanu(*sincos2(rad), **raiser_kwds)
928 except ZeroDivisionError:
929 raise _valueError(tan, rad, **raiser_kwds)
932def tan_(*rads, **raiser_kwds):
933 '''Yield the C{tangent} of angle(s) in C{radians}.
935 @arg rads: One or more angles (each in C{radians}).
937 @return: Yield C{tan(B{rad})} for each angle.
939 @see: Function L{pygeodesy.tan} for futher details.
940 '''
941 try:
942 for r in rads:
943 yield _tanu(*sincos2(r), **raiser_kwds)
944 except ZeroDivisionError:
945 raise _valueError(tan_, r, **raiser_kwds)
948def tand(deg, **raiser_kwds):
949 '''Return the C{tangent} of an angle in C{degrees}.
951 @arg deg: Angle (C{degrees}).
952 @kwarg raiser_kwds: Use C{B{raiser}=False} to avoid
953 ValueErrors or optionally, additional
954 ValueError keyword argments.
956 @return: C{tan(B{deg})}.
958 @raise Error: If L{pygeodesy.isnear0}C{(cos(B{deg})}.
959 '''
960 try:
961 return _tanu(*sincos2d(deg), **raiser_kwds)
962 except ZeroDivisionError:
963 raise _valueError(tand, deg, **raiser_kwds)
966def tand_(*degs, **raiser_kwds):
967 '''Yield the C{tangent} of angle(s) in C{degrees}.
969 @arg degs: One or more angles (each in C{degrees}).
971 @return: Yield C{tand(B{deg})} for each angle.
973 @see: Function L{pygeodesy.tand} for futher details.
974 '''
975 try:
976 for d in degs:
977 yield _tanu(*sincos2d(d), **raiser_kwds)
978 except ZeroDivisionError:
979 raise _valueError(tand_, d, **raiser_kwds)
982def tanPI_2_2(rad):
983 '''Compute the tangent of half angle, 90 degrees rotated.
985 @arg rad: Angle (C{radians}).
987 @return: M{tan((rad + PI/2) / 2)} (C{float}).
988 '''
989 return _tan((rad + PI_2) * _0_5) if _isfinite(rad) else (
990 NAN if isnan(rad) else _copysign(_90_0, rad))
993def _tanu(s, c, raiser=True, **unused):
994 '''(INTERNAL) Helper for functions C{_cotu}, C{sincostan3},
995 C{sincostan3d}, C{tan}, C{tan_}, C{tand} and C{tand_}.
996 '''
997 if s is NAN or isnan(s):
998 s = NAN
999 elif s:
1000 if raiser and isnear0(c):
1001 raise ZeroDivisionError()
1002 s = _over(s, c) if fabs(s) != fabs(c) else \
1003 _copysign(_1_0, (-s) if c < 0 else s)
1004 elif c < 0:
1005 s = -s # negate-0
1006 return s
1009def toise2m(toises):
1010 '''Convert French U{toises<https://WikiPedia.org/wiki/Toise>} to meter.
1012 @arg toises: Value in toises (C{scalar}).
1014 @return: Value in C{meter} (C{float}).
1016 @raise ValueError: Invalid B{C{toises}}.
1018 @see: Function L{fathom2m}.
1019 '''
1020 return Meter(Float(toises=toises) * _M_TOISE)
1023def truncate(x, ndigits=None):
1024 '''Truncate to the given number of digits.
1026 @arg x: Value to truncate (C{scalar}).
1027 @kwarg ndigits: Number of digits (C{int}),
1028 aka I{precision}.
1030 @return: Truncated B{C{x}} (C{float}).
1032 @see: Python function C{round}.
1033 '''
1034 if isint(ndigits):
1035 p = _10_0**ndigits
1036 x = int(x * p) / p
1037 return x
1040def unroll180(lon1, lon2, wrap=True):
1041 '''Unroll longitudinal delta and wrap longitude in degrees.
1043 @arg lon1: Start longitude (C{degrees}).
1044 @arg lon2: End longitude (C{degrees}).
1045 @kwarg wrap: If C{True}, wrap and unroll to the M{(-180..+180]}
1046 range (C{bool}).
1048 @return: 2-Tuple C{(B{lon2}-B{lon1}, B{lon2})} unrolled (C{degrees},
1049 C{degrees}).
1051 @see: Capability C{LONG_UNROLL} in U{GeographicLib
1052 <https://GeographicLib.SourceForge.io/html/python/interface.html#outmask>}.
1053 '''
1054 d = lon2 - lon1
1055 if wrap:
1056 u = wrap180(d)
1057 if u != d:
1058 return u, (lon1 + u)
1059 return d, lon2
1062def _unrollon(p1, p2, wrap=False): # unroll180 == .karney._unroll2
1063 '''(INTERNAL) Wrap/normalize, unroll and replace longitude.
1064 '''
1065 lat, lon = p2.lat, p2.lon
1066 if wrap and _Wrap.normal:
1067 lat, lon = _Wrap.latlon(lat, lon)
1068 _, lon = unroll180(p1.lon, lon, wrap=True)
1069 if lat != p2.lat or fabs(lon - p2.lon) > EPS:
1070 p2 = p2.dup(lat=lat, lon=wrap180(lon))
1071 # p2 = p2.copy(); p2.latlon = lat, wrap180(lon)
1072 return p2
1075def _unrollon3(p1, p2, p3, wrap=False):
1076 '''(INTERNAL) Wrap/normalize, unroll 2 points.
1077 '''
1078 w = wrap
1079 if w:
1080 w = _Wrap.normal
1081 p2 = _unrollon(p1, p2, wrap=w)
1082 p3 = _unrollon(p1, p3, wrap=w)
1083 p2 = _unrollon(p2, p3)
1084 return p2, p3, w # was wrapped?
1087def unrollPI(rad1, rad2, wrap=True):
1088 '''Unroll longitudinal delta and wrap longitude in radians.
1090 @arg rad1: Start longitude (C{radians}).
1091 @arg rad2: End longitude (C{radians}).
1092 @kwarg wrap: If C{True}, wrap and unroll to the M{(-PI..+PI]}
1093 range (C{bool}).
1095 @return: 2-Tuple C{(B{rad2}-B{rad1}, B{rad2})} unrolled
1096 (C{radians}, C{radians}).
1098 @see: Capability C{LONG_UNROLL} in U{GeographicLib
1099 <https://GeographicLib.SourceForge.io/html/python/interface.html#outmask>}.
1100 '''
1101 r = rad2 - rad1
1102 if wrap:
1103 u = wrapPI(r)
1104 if u != r:
1105 return u, (rad1 + u)
1106 return r, rad2
1109def _valueError(where, x, raiser=True, **kwds):
1110 '''(INTERNAL) Return a C{_ValueError} or C{None}.
1111 '''
1112 t = _MODS.streprs.Fmt.PAREN(typename(where), x)
1113 return _ValueError(t, **kwds) if raiser else None
1116class _Wrap(object):
1118 _normal = False # default
1120 @property
1121 def normal(self):
1122 '''Get the current L{normal} setting (C{True},
1123 C{False} or C{None}).
1124 '''
1125 return self._normal
1127 @normal.setter # PYCHOK setter!
1128 def normal(self, setting):
1129 '''Set L{normal} to C{True}, C{False} or C{None}.
1130 '''
1131 m = _MODS.formy
1132 t = {True: (m.normal, m.normal_),
1133 False: (self.wraplatlon, self.wraphilam),
1134 None: (_passargs, _passargs)}.get(setting, ())
1135 if t:
1136 self.latlon, self.philam = t
1137 self._normal = setting
1139 def latlonDMS2(self, lat, lon, **DMS2_kwds):
1140 if isstr(lat) or isstr(lon):
1141 kwds = _xkwds(DMS2_kwds, clipLon=0, clipLat=0)
1142 lat, lon = _MODS.dms.parseDMS2(lat, lon, **kwds)
1143 return self.latlon(lat, lon)
1145# def normalatlon(self, *latlon):
1146# return _MODS.formy.normal(*latlon)
1148# def normalamphi(self, *philam):
1149# return _MODS.formy.normal_(*philam)
1151 def wraplatlon(self, lat, lon):
1152 return wrap90(lat), wrap180(lon)
1154 latlon = wraplatlon # default
1156 def latlon3(self, lon1, lat2, lon2, wrap):
1157 if wrap:
1158 lat2, lon2 = self.latlon(lat2, lon2)
1159 lon21, lon2 = unroll180(lon1, lon2)
1160 else:
1161 lon21 = lon2 - lon1
1162 return lon21, lat2, lon2
1164 def _latlonop(self, wrap):
1165 if wrap and self._normal is not None:
1166 return self.latlon
1167 else:
1168 return _passargs
1170 def wraphilam(self, phi, lam):
1171 return wrapPI_2(phi), wrapPI(lam)
1173 philam = wraphilam # default
1175 def philam3(self, lam1, phi2, lam2, wrap):
1176 if wrap:
1177 phi2, lam2 = self.philam(phi2, lam2)
1178 lam21, lam2 = unrollPI(lam1, lam2)
1179 else:
1180 lam21 = lam2 - lam1
1181 return lam21, phi2, lam2
1183 def _philamop(self, wrap):
1184 if wrap and self._normal is not None:
1185 return self.philam
1186 else:
1187 return _passargs
1189 def point(self, ll, wrap=True): # in .points._fractional, -.PointsIter.iterate, ...
1190 '''Return C{ll} or a copy, I{normalized} or I{wrap}'d.
1191 '''
1192 if wrap and self._normal is not None:
1193 lat, lon = ll.latlon
1194 if fabs(lon) > _180_0 or fabs(lat) > _90_0:
1195 _n = self.latlon
1196 ll = ll.copy(name=typename(_n))
1197 ll.latlon = _n(lat, lon)
1198 return ll
1200_Wrap = _Wrap() # PYCHOK singleton
1203# def _wrap(angle, wrap, modulo):
1204# '''(INTERNAL) Angle wrapper M{((wrap-modulo)..+wrap]}.
1205#
1206# @arg angle: Angle (C{degrees}, C{radians} or C{grades}).
1207# @arg wrap: Range (C{degrees}, C{radians} or C{grades}).
1208# @arg modulo: Upper limit (360 C{degrees}, PI2 C{radians} or 400 C{grades}).
1209#
1210# @return: The B{C{angle}}, wrapped (C{degrees}, C{radians} or C{grades}).
1211# '''
1212# a = float(angle)
1213# if not (wrap - modulo) <= a < wrap:
1214# # math.fmod(-1.5, 3.14) == -1.5, but -1.5 % 3.14 == 1.64
1215# # math.fmod(-1.5, 360) == -1.5, but -1.5 % 360 == 358.5
1216# a %= modulo
1217# if a > wrap:
1218# a -= modulo
1219# return a
1222def wrap90(deg):
1223 '''Wrap degrees to M{[-90..+90]}.
1225 @arg deg: Angle (C{degrees}).
1227 @return: Degrees, wrapped (C{degrees90}).
1228 '''
1229 return _wrapu(wrap180(deg), _180_0, _90_0)
1232def wrap180(deg):
1233 '''Wrap degrees to M{[-180..+180]}.
1235 @arg deg: Angle (C{degrees}).
1237 @return: Degrees, wrapped (C{degrees180}).
1238 '''
1239 d = float(deg)
1240 w = _umod_360(d)
1241 if w > _180_0:
1242 w -= _360_0
1243 elif d < 0 and w == _180_0:
1244 w = -w
1245 return w
1248def wrap360(deg): # see .streprs._umod_360
1249 '''Wrap degrees to M{[0..+360)}.
1251 @arg deg: Angle (C{degrees}).
1253 @return: Degrees, wrapped (C{degrees360}).
1254 '''
1255 return _umod_360(float(deg))
1258def wrapPI(rad):
1259 '''Wrap radians to M{[-PI..+PI]}.
1261 @arg rad: Angle (C{radians}).
1263 @return: Radians, wrapped (C{radiansPI}).
1264 '''
1265 r = float(rad)
1266 w = _umod_PI2(r)
1267 if w > PI:
1268 w -= PI2
1269 elif r < 0 and w == PI:
1270 w = -PI
1271 return w
1274def wrapPI2(rad):
1275 '''Wrap radians to M{[0..+2PI)}.
1277 @arg rad: Angle (C{radians}).
1279 @return: Radians, wrapped (C{radiansPI2}).
1280 '''
1281 return _umod_PI2(float(rad))
1284def wrapPI_2(rad):
1285 '''Wrap radians to M{[-PI/2..+PI/2]}.
1287 @arg rad: Angle (C{radians}).
1289 @return: Radians, wrapped (C{radiansPI_2}).
1290 '''
1291 return _wrapu(wrapPI(rad), PI, PI_2)
1294# def wraplatlon(lat, lon):
1295# '''Both C{wrap90(B{lat})} and C{wrap180(B{lon})}.
1296# '''
1297# return wrap90(lat), wrap180(lon)
1300def wrap_normal(*normal):
1301 '''Define the operation for the keyword argument C{B{wrap}=True},
1302 across L{pygeodesy}: I{wrap}, I{normalize} or I{no-op}. For
1303 backward compatibility, the default is I{wrap}.
1305 @arg normal: If C{True}, I{normalize} lat- and longitude using
1306 L{normal} or L{normal_}, if C{False}, I{wrap} the
1307 lat- and longitude individually by L{wrap90} or
1308 L{wrapPI_2} respectively L{wrap180}, L{wrapPI} or
1309 if C{None}, leave lat- and longitude I{unchanged}.
1310 To get the current setting, do not specify.
1312 @return: The previous L{wrap_normal} setting (C{bool} or C{None}).
1313 '''
1314 t = _Wrap.normal
1315 if normal:
1316 _Wrap.normal = normal[0]
1317 return t
1320# def wraphilam(phi, lam,):
1321# '''Both C{wrapPI_2(B{phi})} and C{wrapPI(B{lam})}.
1322# '''
1323# return wrapPI_2(phi), wrapPI(lam)
1326def _wrapu(w, H, Q):
1327 '''(INTERNAL) Helper for functions C{wrap180} and C{wrapPI}.
1328 '''
1329 return (w - H) if w > Q else ((w + H) if w < (-Q) else w)
1332def yard2m(yards):
1333 '''Convert I{UK} yards to meter.
1335 @arg yards: Value in yards (C{scalar}).
1337 @return: Value in C{meter} (C{float}).
1339 @raise ValueError: Invalid B{C{yards}}.
1340 '''
1341 return Float(yards=yards) * _M_YARD_UK
1343# **) MIT License
1344#
1345# Copyright (C) 2016-2025 -- mrJean1 at Gmail -- All Rights Reserved.
1346#
1347# Permission is hereby granted, free of charge, to any person obtaining a
1348# copy of this software and associated documentation files (the "Software"),
1349# to deal in the Software without restriction, including without limitation
1350# the rights to use, copy, modify, merge, publish, distribute, sublicense,
1351# and/or sell copies of the Software, and to permit persons to whom the
1352# Software is furnished to do so, subject to the following conditions:
1353#
1354# The above copyright notice and this permission notice shall be included
1355# in all copies or substantial portions of the Software.
1356#
1357# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
1358# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
1359# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
1360# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
1361# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
1362# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
1363# OTHER DEALINGS IN THE SOFTWARE.