-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathGeodesics.jl
256 lines (206 loc) · 10.5 KB
/
Geodesics.jl
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
module Geodesics
"Earth ellipsoid semi-axes in WGS84"
const EARTH_R_MAJOR_WGS84 = 6378137.0000
const EARTH_R_MINOR_WGS84 = 6356752.3142
"Flattening of the Earth in WGS84"
const F_WGS84 = (EARTH_R_MAJOR_WGS84 - EARTH_R_MINOR_WGS84)/EARTH_R_MAJOR_WGS84
"""
local Earth radius in WGS84
"""
function localEarthRadius(lat,degrees=true)
if degrees lat = deg2rad(lat) end
r1 = EARTH_R_MAJOR_WGS84
r2 = EARTH_R_MINOR_WGS84
return √(((r1^2 * cos(lat))^2 + (r2^2 * sin(lat))^2 ) / ( (r1 * cos(lat))^2 + (r2 * sin(lat))^2 ))
end
"""
angular_distance(lon0, lat0, lon1, lat1, degrees=true; f=F_WGS84) -> Δ
Return the angular distance between points (`lon0`,`lat0`) and (`lon1`,`lat1`)
on a flattened sphere. The default flattening is $F_WGS84. By default,
input and output are in degrees, but specify `degrees` as `false` to use radians.
Use `f=0` to perform computations on a sphere.
"""
function angular_distance(lon0, lat0, lon1, lat1, degrees::Bool=true; f=F_WGS84)
if degrees
lon0, lat0, lon1, lat1 = deg2rad(lon0), deg2rad(lat0), deg2rad(lon1), deg2rad(lat1)
end
gcarc, az, baz = inverse(lon0, lat0, lon1, lat1, 1.0, f)
degrees ? rad2deg(gcarc) : gcarc
end
"""
azimuth(lon0, lat0, lon1, lat1, degrees=true, f=F_WGS84)
Return the azimuth from point (`lon0`,`lat0`) to point (`lon1`,`lat1`) on a
flattened sphere. The default flattening is $F_WGS84. By default,
input and output are in degrees, but specify `degrees` as `false` to use radians.
Use `f=0` to perform computations on a sphere.
"""
function azimuth(lon0::T1, lat0::T2, lon1::T3, lat1::T4, degrees::Bool=true; f=F_WGS84) where {T1,T2,T3,T4}
# Promote to Float64 at least to avoid error when one of the points is on the pole
T = promote_type(Float64, T1, T2, T3, T4)
lon0, lat0, lon1, lat1 = T(lon0), T(lat0), T(lon1), T(lat1)
if degrees
lon0, lat0, lon1, lat1 = deg2rad(lon0), deg2rad(lat0), deg2rad(lon1), deg2rad(lat1)
end
gcarc, az, baz = inverse(lon0, lat0, lon1, lat1, 1.0, f)
degrees ? rad2deg(az) : az
end
"""
step(lon, lat, azimuth, distance, degrees=true; f=F_WGS84) -> lon′, lat′, backazimuth
Return the longitude `lon′`, latitude `lat′` and backazimuth `baz` reached by
travelling an angular `distance` along `azimuth` from the starting point at
(`lon`,`lat`). The default flattening is $F_WGS84. By default,
input and output are in degrees, but specify `degrees` as `false` to use radians.
Use `f=0` to perform computations on a sphere.
"""
function angular_step(lon, lat, azimuth, distance, degrees::Bool=true; f=F_WGS84)
if degrees
lon, lat, azimuth, distance = deg2rad(lon), deg2rad(lat), deg2rad(azimuth), deg2rad(distance)
end
lon′, lat′, baz = forward(lon, lat, azimuth, distance, 1.0, f)
if degrees
rad2deg(lon′), rad2deg(lat′), rad2deg(baz)
else
lon′, lat′, baz
end
end
"""
surface_distance(lon0, lat0, lon1, lat1, a, degrees::Bool=false; f=F_WGS84)
Return the physical distance between points (`lon0`,`lat0`) and (`lon1`,`lat1`) on
the flattened sphere with flattening `f` and semimajor radius `a`. Distance is given
in the same units as `a`. By default, input angles are in degrees, but specify `degrees`
as `false` to use radians. Use `f=0` to perform computations on a sphere.
"""
function surface_distance(lon0, lat0, lon1, lat1, a, degrees::Bool=true; f=F_WGS84)
if degrees
lon0, lat0, lon1, lat1 = deg2rad(lon0), deg2rad(lat0), deg2rad(lon1), deg2rad(lat1)
end
distance, az, baz = inverse(lon0, lat0, lon1, lat1, a, f)
distance
end
"""
forward(lon, lat, azimuth, distance, a, f) -> lon′, lat′, backazimuth
Return the longitude `lon′` and latitude `lat′` and `backazimuth` of a projected
point, reached by travelling along an `azimuth` for `distance` from an original
point at (`lon`, `lat`). Specify the spheroid with the semimajor radius `a` and
flattening `f`.
Coordinates and azimuth are in radians.
Calculations use Vincenty's forward formula [1].
#### References
1. Vincenty, T. (1975). "Direct and Inverse Solutions of Geodesics on the Ellipsoid
with application of nested equations" (PDF). Survey Review. XXIII (176): 88–93.
doi:10.1179/sre.1975.23.176.88
"""
function forward(lon, lat, azimuth, distance, a, f)::Tuple{Float64,Float64,Float64}
abs(lat <= π/2) || throw(ArgumentError("Latitude ($lat) must be in range [-π/2, π/2]"))
a > 0 || throw(ArgumentError("Semimajor axis ($a) must be positive"))
abs(f) < 1 || throw(ArgumentError("Magnitude of flattening ($f) must be less than 1"))
# Calculations are done with Float64s internally as the tolerances are hard-wired
lambda1, phi1, alpha12, s = Float64(lon), Float64(lat), Float64(azimuth), Float64(distance)
a, f = Float64(a), Float64(f)
alpha12 = mod(alpha12, 2π)
b = a*(1 - f)
TanU1 = (1 - f)*tan(phi1)
U1 = atan(TanU1)
sigma1 = atan( TanU1, cos(alpha12) )
Sinalpha = cos(U1)*sin(alpha12)
cosalpha_sq = 1.0 - Sinalpha*Sinalpha
u2 = cosalpha_sq*(a*a - b*b )/(b*b)
A = 1.0 + (u2/16384)*(4096 + u2*(-768 + u2*(320 - 175*u2)))
B = (u2/1024)*(256 + u2*(-128 + u2*(74 - 47*u2)))
# Starting with the approximation
sigma = (s/(b*A))
# Not moving anywhere. We can return the location that was passed in.
if sigma == 0
return lambda1, phi1, mod(alpha12 + π, 2π)
end
last_sigma = 2*sigma + 2 # something impossible
# Iterate the following three equations
# until there is no significant change in sigma
# two_sigma_m , delta_sigma
while abs((last_sigma - sigma)/sigma) > 1.0e-9
global two_sigma_m = 2*sigma1 + sigma
delta_sigma = B*sin(sigma)*(cos(two_sigma_m) + (B/4)*(cos(sigma)*(-1 + 2*cos(two_sigma_m)^2 - (B/6)*cos(two_sigma_m)*(-3 + 4*sin(sigma)^2)*(-3 + 4*cos(two_sigma_m)^2 ))))
last_sigma = sigma
sigma = (s/(b*A)) + delta_sigma
end
phi2 = atan((sin(U1)*cos(sigma) + cos(U1)*sin(sigma)*cos(alpha12)),
((1-f)*sqrt(Sinalpha^2 + (sin(U1)*sin(sigma) - cos(U1)*cos(sigma)*cos(alpha12))^2)))
lambda = atan((sin(sigma)*sin(alpha12)),
(cos(U1)*cos(sigma) - sin(U1)*sin(sigma)*cos(alpha12)))
C = (f/16)*cosalpha_sq*(4 + f*(4 - 3*cosalpha_sq))
omega = lambda - (1-C)*f*Sinalpha*(sigma + C*sin(sigma)*(
cos(two_sigma_m) + C*cos(sigma)*(-1 + 2*cos(two_sigma_m)^2)))
lambda2 = lambda1 + omega
alpha21 = atan(Sinalpha, (-sin(U1)*sin(sigma) + cos(U1)*cos(sigma)*cos(alpha12)))
alpha21 = mod(alpha21 + π, 2π)
return lambda2, phi2, alpha21
end
"""
inverse(lon1, lat1, lon2, lat2, a, f) -> distance, azimuth, backazimuth
Return the `distance`, `azimuth` and `backazimuth` between two points with longitudes
`lon1` and `lon2`, and latitudes `lat1` and `lat2`. Specify the spheroid with the
semimajor radius `a` and flattening `f`.
Coordinates and angles are in radians, whilst `distance` is in the same units as `a`.
Calculations use Vincenty's inverse formula [1].
#### References
1. Vincenty, T. (1975). "Direct and Inverse Solutions of Geodesics on the Ellipsoid
with application of nested equations" (PDF). Survey Review. XXIII (176): 88–93.
doi:10.1179/sre.1975.23.176.88
"""
function inverse(lon1, lat1, lon2, lat2, a, f)::Tuple{Float64,Float64,Float64}
for lat in (lat1, lat2)
abs(lat <= π/2) || throw(ArgumentError("Latitude ($lat) must be in range [-π/2, π/2]"))
end
a > 0 || throw(ArgumentError("Semimajor axis ($a) must be positive"))
abs(f) < 1 || throw(ArgumentError("Magnitude of flattening ($f) must be less than 1"))
lambda1, phi1, lambda2, phi2 = Float64(lon1), Float64(lat1), Float64(lon2), Float64(lat2)
a, f = Float64(a), Float64(f)
tol = 1.0e-8
if (abs(phi2 - phi1) < tol) && (abs(lambda2 - lambda1) < tol)
return 0.0, 0.0, 0.0
end
b = a*(1 - f)
TanU1 = (1 - f)*tan(phi1)
TanU2 = (1 - f)*tan(phi2)
U1 = atan(TanU1)
U2 = atan(TanU2)
lambda = lambda2 - lambda1
last_lambda = -4000000.0 # an impossibe value
omega = lambda
# Iterate the following equations until there is no significant change in lambda
alpha, sigma, Sin_sigma, Cos2sigma_m, Cos_sigma, sqr_sin_sigma =
-999999., -999999., -999999., -999999., -999999., -999999.
while ((last_lambda < -3000000.0) || (lambda != 0)) &&
(abs((last_lambda - lambda)/lambda) > 1.0e-9)
sqr_sin_sigma = (cos(U2)*sin(lambda))^2 +
((cos(U1)*sin(U2) - sin(U1)*cos(U2)*cos(lambda)))^2
Sin_sigma = sqrt(sqr_sin_sigma)
Cos_sigma = sin(U1)*sin(U2) + cos(U1)*cos(U2)*cos(lambda)
sigma = atan(Sin_sigma, Cos_sigma)
Sin_alpha = cos(U1)*cos(U2)*sin(lambda)/sin(sigma)
if Sin_alpha >= 1
Sin_alpha = 1.0
elseif Sin_alpha <= -1
Sin_alpha = -1.0
end
alpha = asin(Sin_alpha)
Cos2sigma_m = cos(sigma) - 2*sin(U1)*sin(U2)/cos(alpha)^2
C = (f/16)*cos(alpha)^2*(4 + f*(4 - 3*cos(alpha)^2))
last_lambda = lambda
lambda = omega + (1 - C)*f*sin(alpha)*(sigma +
C*sin(sigma)*(Cos2sigma_m + C*cos(sigma)*(-1 + 2*Cos2sigma_m^2)))
end
u2 = cos(alpha)^2*(a*a - b*b)/(b*b)
A = 1 + (u2/16384)*(4096 + u2*(-768 + u2*(320 - 175*u2)))
B = (u2/1024)*(256 + u2*(-128 + u2*(74 - 47*u2)))
delta_sigma = B*Sin_sigma*(Cos2sigma_m + (B/4)*(
Cos_sigma*(-1 + 2*Cos2sigma_m^2) -
(B/6)*Cos2sigma_m*(-3 + 4*sqr_sin_sigma)*(-3 + 4*Cos2sigma_m^2)))
s = b*A*(sigma - delta_sigma)
alpha12 = atan((cos(U2)*sin(lambda)), ( cos(U1)*sin(U2) - sin(U1)*cos(U2)*cos(lambda)))
alpha21 = atan((cos(U1)*sin(lambda)), (-sin(U1)*cos(U2) + cos(U1)*sin(U2)*cos(lambda)))
alpha12 = mod(alpha12, 2π)
alpha21 = mod(alpha21 + π, 2π)
return s, alpha12, alpha21
end
end # module