-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathgeo_utils.py
343 lines (306 loc) · 13.8 KB
/
geo_utils.py
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
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
"""
Python utility functions for geodetic calculations.
Author: Chris Oswald
"""
# Import packages
from typing import List, Tuple, Union
import numpy as np
# Define constants
EARTH_RADIUS_KM = 6378
# Define functions
def km_to_miles(km: float) -> float:
"""Converts distance in kilometers to miles."""
return km * 0.62137
def km_to_meters(km: float) -> float:
"""Converts distance in kilometers to meters."""
return km * 1000
def meters_to_km(meters: float) -> float:
"""Converts distance in meters to kilometers."""
return meters / 1000
def deg_to_rad(degrees: float) -> float:
"""Converts angle in degrees to radians."""
return degrees * np.pi / 180
def rad_to_deg(radians: float) -> float:
"""Converts angle in radians to degrees."""
return radians * 180 / np.pi
def numeric_DMS_to_DD_coords(
degrees: Union[int, float],
minutes: Union[int, float],
seconds: Union[int, float],
) -> float:
"""Convert numeric Degree/Minute/Second (DMS) coordinates
to decimal degrees (DD)."""
return (degrees + minutes/60 + seconds/3600)
def str_DMS_to_DD_coords(DMS_str: str) -> Tuple[float, float]:
"""Parse Degree/Minute/Second (DMS) coordinate string and convert to
decimal degrees (DD)."""
# import pdb; pdb.set_trace()
lat_deg = int(DMS_str[:DMS_str.find('°')])
lat_min = int(DMS_str[DMS_str.find('°')+1:DMS_str.find('\'')])
lat_sec = float(DMS_str[DMS_str.find('\'')+1:DMS_str.find('"')])
lon_deg = int(DMS_str[DMS_str.rfind(' ')+1:DMS_str.rfind('°')])
lon_min = int(DMS_str[DMS_str.rfind('°')+1:DMS_str.rfind('\'')])
lon_sec = float(DMS_str[DMS_str.rfind('\'')+1:DMS_str.rfind('"')])
lat_decimal = round(numeric_DMS_to_DD_coords(lat_deg, lat_min, lat_sec), 5)
lon_decimal = round(numeric_DMS_to_DD_coords(lon_deg, lon_min, lon_sec), 5)
return (lat_decimal, lon_decimal)
def convert_trig_to_compass_angle(trig_angle: float, radians: bool = True):
"""Converts a trigonometric angle (measured counterclockwise from East)
to compass angle (measured clockwise from North). Angle returned is in
original units (e.g., radians are returned if polar_angle is given in radians).
Arguments
trig_angle: angle measured counterclockwise from East
radians: indicates that angle is given in radians (False=degrees)
Returns
compass_angle: angle measured clockwise from North
"""
if radians:
compass_angle = ((np.pi / 2) - trig_angle) % (2 * np.pi)
else:
compass_angle = (90 - trig_angle) % 360
return compass_angle
def convert_lat_lon_alt_to_nvector(lat_deg: float, lon_deg: float, alt_km: float = 0.0,
) -> Tuple[float, float, float]:
"""Convert a point with latitude/longitude (decimal degrees) and altitude
(kilometers) to an orthogonal (normal) vector <x, y, z>. Note: right-handed
coordinate system is used (e.g., positive x-axis points toward 0 degrees
North, 0 degrees East; positive y-axis points toward 0 degrees North, 90
degrees East; positive z-axis points toward 90 degrees North).
Source: https://www.movable-type.co.uk/scripts/latlong-vectors.html.
Arguments
lat_deg: latitude in decimal degrees
lon_deg: longitude in decimal degrees
alt_km: altitude in kilometers (measured from ground level)
Returns
n_vector: orthogonal vector <x, y, z>
"""
lat_rad, lon_rad = deg_to_rad(lat_deg), deg_to_rad(lon_deg)
x = (EARTH_RADIUS_KM + alt_km) * np.cos(lat_rad) * np.cos(lon_rad)
y = (EARTH_RADIUS_KM + alt_km) * np.cos(lat_rad) * np.sin(lon_rad)
z = (EARTH_RADIUS_KM + alt_km) * np.sin(lat_rad)
return (x, y, z)
def convert_nvector_to_lat_lon(nvector: Tuple[float, float, float],
) -> Tuple[float, float]:
"""Convert an orthogonal (normal) vector <x, y, z> to latitude/longitude
(decimal degrees). Note: right-handed coordinate system is used (e.g.,
positive x-axis points toward 0 degrees North, 0 degrees East; positive
y-axis points toward 0 degrees North, 90 degrees East; positive z-axis
points toward 90 degrees North).
Source: https://www.movable-type.co.uk/scripts/latlong-vectors.html.
Arguments
nvector: orthogonal vector <x, y, z>
Returns
(lat_deg, lon_deg): tuple containing latitude/longitude (decimal degrees)
"""
x, y, z = nvector[0], nvector[1], nvector[2]
lat_deg = rad_to_deg(np.arctan2(z, np.sqrt(x**2 + y**2)))
lon_deg = rad_to_deg(np.arctan2(y, x))
return (lat_deg, lon_deg)
def calculate_magnitude_dist_bt_vectors(
vector1: Tuple[float, float, float],
vector2: Tuple[float, float, float],
) -> float:
"""Calculate the magnitude of the Euclidean distance between two vectors
in R3.
Arguments
vector1: first vector with components <x, y, z>
vector2: second vector with components <x, y, z>
"""
squared_dist = 0
for i in np.arange(len(vector1)):
squared_dist += (vector2[i] - vector1[i])**2
return np.sqrt(squared_dist)
def calculate_great_circle_distance(
origin_lat_deg: float, origin_lon_deg: float,
dest_lat_deg: float, dest_lon_deg: float,
) -> float:
"""Calculate great-circle distance between two points using the haversine
formula. Source: https://www.movable-type.co.uk/scripts/latlong.html.
Arguments
origin_lat_deg: float latitude of origin location in decimal degrees
origin_lon_deg: float longitude of origin location in decimal degrees
dest_lat_deg: float latitude of destination location in decimal degrees
dest_lon_deg: float longitude of destination location in decimal degrees
Returns
distance_km: float distance in kilometers between location #1 and #2
"""
# Convert degrees to radians
origin_lat_rad = deg_to_rad(origin_lat_deg)
origin_lon_rad = deg_to_rad(origin_lon_deg)
dest_lat_rad = deg_to_rad(dest_lat_deg)
dest_lon_rad = deg_to_rad(dest_lon_deg)
# Calculate distance
delta_lat_rad = dest_lat_rad - origin_lat_rad
delta_lon_rad = dest_lon_rad - origin_lon_rad
a = (
np.sin(delta_lat_rad/2) ** 2
+ np.cos(origin_lat_rad) * np.cos(dest_lat_rad) * np.sin(delta_lon_rad/2) ** 2
)
ang_dist_rad = 2 * np.arctan2(np.sqrt(a), np.sqrt(1-a))
distance_km = EARTH_RADIUS_KM * ang_dist_rad
return distance_km
def calculate_great_circle_distance_vec(
origin_lat_deg: float, origin_lon_deg: float,
dest_lat_deg: float, dest_lon_deg: float,
) -> float:
"""Calculate great-circle distance between two points using vectors.
Source: https://www.movable-type.co.uk/scripts/latlong-vectors.html.
Arguments
origin_lat_deg: float latitude of origin location in decimal degrees
origin_lon_deg: float longitude of origin location in decimal degrees
dest_lat_deg: float latitude of destination location in decimal degrees
dest_lon_deg: float longitude of destination location in decimal degrees
Returns
distance_km: float distance in kilometers between location #1 and #2
"""
# Convert lat/lon coordinates to vectors
origin_vector = convert_lat_lon_alt_to_nvector(origin_lat_deg, origin_lon_deg)
dest_vector = convert_lat_lon_alt_to_nvector(dest_lat_deg, dest_lon_deg)
# Calculate distance
ang_dist_rad = np.arctan2(
np.linalg.norm(np.cross(origin_vector, dest_vector)),
np.dot(origin_vector, dest_vector)
)
distance_km = EARTH_RADIUS_KM * ang_dist_rad
return distance_km
def calculate_initial_bearing(
origin_lat_deg: float, origin_lon_deg: float,
dest_lat_deg: float, dest_lon_deg: float,
) -> float:
"""Calculate forward azimuth/initial bearing between two points, in degrees.
Source: https://www.movable-type.co.uk/scripts/latlong.html.
Arguments
origin_lat_deg: float latitude of origin location in decimal degrees
origin_lon_deg: float longitude of origin location in decimal degrees
dest_lat_deg: float latitude of destination location in decimal degrees
dest_lon_deg: float longitude of destination location in decimal degrees
Returns
bearing_deg: float initial bearing in degrees
"""
# Convert degrees to radians
origin_lat_rad = deg_to_rad(origin_lat_deg)
origin_lon_rad = deg_to_rad(origin_lon_deg)
dest_lat_rad = deg_to_rad(dest_lat_deg)
dest_lon_rad = deg_to_rad(dest_lon_deg)
# Calculate bearing
delta_lon_rad = dest_lon_rad - origin_lon_rad
x = (np.cos(origin_lat_rad) * np.sin(dest_lat_rad)
- np.sin(origin_lat_rad) * np.cos(dest_lat_rad) * np.cos(delta_lon_rad)
)
y = np.sin(delta_lon_rad) * np.cos(dest_lat_rad)
theta = np.arctan2(y, x)
bearing_deg = (rad_to_deg(theta) + 360) % 360
return bearing_deg
def determine_destination_coords(
origin_lat_deg: float, origin_lon_deg: float,
distance_km: float, initial_bearing_deg: float
) -> Tuple[float, float]:
"""Determine the latitude and longitude of a destination point given an
initial latitude, longitude, and bearing (clockwise from 0 degrees North).
Source: https://www.movable-type.co.uk/scripts/latlong.html.
Arguments
origin_lat_deg: float latitude of origin location in decimal degrees
origin_lon_deg: float longitude of origin location in decimal degrees
distance_km: float distance in km from origin to destination
initial_bearing_deg: float bearing in degrees from origin to destination
Returns
dest_lat_deg, dest_lon_deg: destination latitude/longitude (decimal degrees)
"""
# Convert degrees to radians
origin_lat_rad = deg_to_rad(origin_lat_deg)
origin_lon_rad = deg_to_rad(origin_lon_deg)
bearing_rad = deg_to_rad(initial_bearing_deg)
# Determine latitude/longitude of destination
ang_dist_rad = distance_km / EARTH_RADIUS_KM
dest_lat_rad = np.arcsin(np.sin(origin_lat_rad) * np.cos(ang_dist_rad)
+ np.cos(origin_lat_rad) * np.sin(ang_dist_rad) * np.cos(bearing_rad)
)
dest_lon_rad = (origin_lon_rad
+ np.arctan2(
np.sin(bearing_rad) * np.sin(ang_dist_rad) * np.cos(origin_lat_rad),
np.cos(ang_dist_rad) - np.sin(origin_lat_rad) * np.sin(dest_lat_rad)
)
)
# Convert radians to degrees
dest_lat_deg = rad_to_deg(dest_lat_rad)
dest_lon_deg = rad_to_deg(dest_lon_rad)
return (dest_lat_deg, dest_lon_deg)
def calculate_cross_track_distance(
origin_lat_deg: float, origin_lon_deg: float,
dest_lat_deg: float, dest_lon_deg: float,
cross_lat_deg: float, cross_lon_deg: float,
) -> float:
"""Calculate shortest distance from a third point to a great-circle path
between origin and destination points, in kilometers.
Source: https://www.movable-type.co.uk/scripts/latlong.html.
Arguments
origin_lat_deg: float latitude of origin location in decimal degrees
origin_lon_deg: float longitude of origin location in decimal degrees
dest_lat_deg: float latitude of destination location in decimal degrees
dest_lon_deg: float longitude of destination location in decimal degrees
cross_lat_deg: float longitude of cross-track location in decimal degrees
cross_lon_deg: float longitude of cross-track location in decimal degrees
Returns
distance_km: float cross-track distance in kilometers
"""
# Calculate initial bearing between origin/destination and origin/cross location
bearing_origin_dest_rad = deg_to_rad(
calculate_initial_bearing(
origin_lat_deg=origin_lat_deg,
origin_lon_deg=origin_lon_deg,
dest_lat_deg=dest_lat_deg,
dest_lon_deg=dest_lon_deg,
)
)
bearing_origin_cross_rad = deg_to_rad(
calculate_initial_bearing(
origin_lat_deg=origin_lat_deg,
origin_lon_deg=origin_lon_deg,
dest_lat_deg=cross_lat_deg,
dest_lon_deg=cross_lon_deg,
)
)
# Calculate angular distance from origin to cross location
dist_origin_to_cross_km = calculate_great_circle_distance(
origin_lat_deg=origin_lat_deg,
origin_lon_deg=origin_lon_deg,
dest_lat_deg=cross_lat_deg,
dest_lon_deg=cross_lon_deg,
)
ang_dist_origin_cross_rad = dist_origin_to_cross_km / EARTH_RADIUS_KM
# Calculate cross-track distance
distance_km = np.arcsin(
np.sin(ang_dist_origin_cross_rad)
* np.sin(bearing_origin_cross_rad - bearing_origin_dest_rad)
) * EARTH_RADIUS_KM
return distance_km
def determine_square_coords(
origin_lat_deg: float,
origin_lon_deg: float,
side_length_meters: float,
rotation_radians: float = 0,
) -> List[Tuple[float, float]]:
"""Determine the latitude and longitude of four points that form a square
centered around the origin point.
Arguments
origin_lat_deg: float latitude of center point in decimal degrees
origin_lon_deg: float longitude of center point in decimal degrees
side_length_meters: float length of square in meters
rotation_radians: float radians indicating rotation of square around origin
Returns
coords_list: list containing four tuples of latitude-longitude pairs
indicating the corners of the square around the origin
"""
coords_list = []
dist_origin_to_corner_meters = np.sqrt(2 * (side_length_meters/2)**2)
angle_rad_list = [(((n*2+1)/4)*np.pi + rotation_radians) for n in np.arange(4)]
for angle_rad in angle_rad_list:
bearing_rad = convert_trig_to_compass_angle(angle_rad, radians=True)
latlon_list = determine_destination_coords(
origin_lat_deg=origin_lat_deg,
origin_lon_deg=origin_lon_deg,
distance_km=meters_to_km(dist_origin_to_corner_meters),
initial_bearing_deg=rad_to_deg(bearing_rad),
)
coords_list.append((latlon_list[1], latlon_list[0])) # Longitude first for simplekml
return coords_list