Coverage for pyguymer3/geo/calc_dist_between_two_locs.py: 92%
50 statements
« prev ^ index » next coverage.py v7.9.2, created at 2025-07-08 18:47 +0000
« prev ^ index » next coverage.py v7.9.2, created at 2025-07-08 18:47 +0000
1#!/usr/bin/env python3
3# Define function ...
4def calc_dist_between_two_locs(
5 lon1_deg,
6 lat1_deg,
7 lon2_deg,
8 lat2_deg,
9 /,
10 *,
11 eps = 1.0e-12,
12 nIter = 100,
13):
14 """Calculate the distance between two coordinates.
16 This function reads in two coordinates (in degrees) on the surface of the
17 Earth and calculates the Geodesic distance (in metres) between them and the
18 headings (in degrees) from each coordinate to the other one.
20 Parameters
21 ----------
22 lon1_deg : float
23 the longitude of the first coordinate (in degrees)
24 lat1_deg : float
25 the latitude of the first coordinate (in degrees)
26 lon2_deg : float
27 the longitude of the second coordinate (in degrees)
28 lat2_deg : float
29 the latitude of the second coordinate (in degrees)
30 eps : float, optional
31 the tolerance of the Vincenty formula iterations
32 nIter : int, optional
33 the maximum number of iterations (particularly the Vincenty formula)
35 Returns
36 -------
37 s_m : float
38 the distance between the two coordinates (in metres)
39 alpha1_deg : float
40 the heading to the second coordinate from the first coordinate (in
41 degrees)
42 alpha2_deg : float
43 the heading to the first coordinate from the second coordinate (in
44 degrees)
46 Notes
47 -----
48 This function uses `Vincenty's formulae
49 <https://en.wikipedia.org/wiki/Vincenty%27s_formulae>`_ ; there is a
50 `JavaScript implementation
51 <https://www.movable-type.co.uk/scripts/latlong-vincenty.html>`_ online too.
53 ``lambda`` is a reserved word in Python so I use ``lam`` as my variable name
54 instead.
56 Copyright 2017 Thomas Guymer [1]_
58 References
59 ----------
60 .. [1] PyGuymer3, https://github.com/Guymer/PyGuymer3
61 """
63 # Import standard modules ...
64 import math
66 # **************************************************************************
68 # Skip if the start- and end-points are the same ...
69 if lon1_deg == lon2_deg and lat1_deg == lat2_deg:
70 return 0.0, 0.0, 0.0
72 # Convert to radians ...
73 lon1 = math.radians(lon1_deg) # [rad]
74 lat1 = math.radians(lat1_deg) # [rad]
75 lon2 = math.radians(lon2_deg) # [rad]
76 lat2 = math.radians(lat2_deg) # [rad]
78 # Set constants ...
79 a = 6378137.0 # [m]
80 f = 1.0 / 298.257223563
81 b = (1.0 - f) * a # [m]
82 l = lon2 - lon1 # [rad]
83 u1 = math.atan((1.0 - f) * math.tan(lat1)) # [rad]
84 u2 = math.atan((1.0 - f) * math.tan(lat2)) # [rad]
86 # Set initial value of lambda and initialize counter ...
87 lam = l
88 iIter = 0 # [#]
90 # Start infinite loop ...
91 while True:
92 # Stop looping if the function has been called too many times ...
93 if iIter >= nIter:
94 raise Exception(f"failed to converge; loc1 = ({lon1_deg:+.9f}°,{lat1_deg:+.9f}°); loc2 = ({lon2_deg:+.9f}°,{lat2_deg:+.9f}°); eps = {eps:.15e}; nIter = {nIter:,d}") from None
96 # Calculate new lambda and increment counter ...
97 sin_sigma = math.hypot(
98 math.cos(u2) * math.sin(lam),
99 math.cos(u1) * math.sin(u2) - math.sin(u1) * math.cos(u2) * math.cos(lam)
100 )
101 if sin_sigma == 0.0:
102 # NOTE: co-incident points
103 return 0.0, 0.0, 0.0
104 cos_sigma = math.sin(u1) * math.sin(u2) + math.cos(u1) * math.cos(u2) * math.cos(lam)
105 sigma = math.atan2(
106 sin_sigma,
107 cos_sigma
108 )
109 sin_alpha = math.cos(u1) * math.cos(u2) * math.sin(lam) / sin_sigma
110 cosSq_alpha = 1.0 - sin_alpha ** 2
111 cos_two_sigma_m = cos_sigma - 2.0 * math.sin(u1) * math.sin(u2) / cosSq_alpha
112 if math.isnan(cos_two_sigma_m):
113 # NOTE: equatorial line
114 cos_two_sigma_m = 0.0
115 c = f * cosSq_alpha * (4.0 + f * (4.0 - 3.0 * cosSq_alpha)) / 16.0
116 lamNew = l + (1.0 - c) * f * sin_alpha * (sigma + c * sin_sigma * (cos_two_sigma_m + c * cos_sigma * (2.0 * cos_two_sigma_m ** 2 - 1.0)))
117 iIter += 1 # [#]
119 # Only check the solution after at least 3 function calls ...
120 if iIter >= 3:
121 if lamNew == lam:
122 # Replace old lambda with new lambda ...
123 lam = lamNew
125 break
126 if abs(lamNew - lam) / abs(lamNew) <= eps:
127 # Replace old lambda with new lambda ...
128 lam = lamNew
130 break
132 # Replace old lambda with new lambda ...
133 lam = lamNew
135 # Calculate ellipsoidal distance and forward azimuths ...
136 uSq = cosSq_alpha * (a ** 2 - b ** 2) / b ** 2
137 bigA = 1.0 + uSq * (4096.0 + uSq * (-768.0 + uSq * (320.0 - 175.0 * uSq))) / 16384.0
138 bigB = uSq * (256.0 + uSq * (-128.0 + uSq * (74.0 - 47.0 * uSq))) / 1024.0
139 delta_sigma = bigB * sin_sigma * (cos_two_sigma_m + 0.25 * bigB * (cos_sigma * (2.0 * cos_two_sigma_m ** 2 - 1.0) - bigB * cos_two_sigma_m * (4.0 * sin_sigma ** 2 - 3.0) * (4.0 * cos_two_sigma_m ** 2 - 3.0) / 6.0))
140 s = b * bigA * (sigma - delta_sigma) # [m]
141 alpha1 = math.atan2(
142 math.cos(u2) * math.sin(lam),
143 math.cos(u1) * math.sin(u2) - math.sin(u1) * math.cos(u2) * math.cos(lam)
144 ) # [rad]
145 alpha2 = math.atan2(
146 math.cos(u1) * math.sin(lam),
147 math.sin(u1) * math.cos(u2) - math.cos(u1) * math.sin(u2) * math.cos(lam)
148 ) # [rad]
149 alpha1 = (alpha1 + 2.0 * math.pi) % (2.0 * math.pi) # NOTE: Normalize to +0° <--> +360°
150 alpha2 = (alpha2 + 2.0 * math.pi) % (2.0 * math.pi) # NOTE: Normalize to +0° <--> +360°
152 # Return distance and forward azimuths ...
153 return s, math.degrees(alpha1), math.degrees(alpha2)