forked from amattheisen/notams
/
plot_notams.py
311 lines (257 loc) · 9.66 KB
/
plot_notams.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
"""
plot_notams.py
==============
Plot NOTAMS on a map.
Usage:
plot_notams.py [--date DATE] [--init] [--marble|--etopo|--basic] [--infile FILE] [--outfile FILE] [-h]
Options:
-h --help Show this screen.
-d --date DATE Specify UTC date in ISO format YYYY-MM-DD. Default is
today's UTC date.
--init Force regeneration of the background map.
--basic Use minimalist map background instead of the default
shadedrelief.
--marble Use the bluemarble map background instead of the default
shadedrelief.
--etopo Use the etopo relief map background instead of the default
shadedrelief.
--infile FILE Read NOTAMs from YAML formatted file FILE. If not
specified, the input file name will be derrived from
the --date option as <YYYY-MM-DD_notams.yaml>.
--outfile FILE Save the output plot as FILE. If not specified, the
output file name will be derrived from the --date option
as <YYYY-MM-DD_notams.png>.
"""
# Standard Imports
import datetime
from docopt import docopt
import math
import matplotlib
matplotlib.use('Agg')
import matplotlib.patheffects as PathEffects
import matplotlib.pyplot as plt
from mpl_toolkits.basemap import Basemap
import numpy as np
import os
import pytz
import gc
# Custom Imports
from lib_notam_yaml import import_notams, validate_ident, validate_lat, validate_lon, validate_radius
# Constants
NOTAM_PLOT_KEYS = ['idents', 'latitudes', 'longitudes', 'radii']
DATA_DIR = [os.path.dirname(__file__), 'static_notams', 'data']
PLOT_DIR = [os.path.dirname(__file__), 'static_notams', 'images']
# Functions
def main(options):
"""
Opens a yaml dump, validates the notams within the file, and generates a
plot of the notams.
"""
print("Opening %s ..." % options['--infile'])
print("Collecting notams...")
notam_list = import_notams(yaml_file=options['--infile'])
notams = create_plot_dictionary(notam_list=notam_list)
print(notams)
print("Computing Circles ...")
notams['circles'] = compute_circles(notams)
if options['--init']:
prepare_background(map_type=options['map-type'])
print("Generating Plot %s ..." % options['--outfile'])
make_plot(notams=notams,
day=options['--date'],
outfile=options['--outfile'],
map_type=options['map-type'])
print("Success")
return
def create_plot_dictionary(notam_list):
"""
Create a dictionary of lists for plotting using a previously validated
`notam_list`.
"""
notams = {}
for key in NOTAM_PLOT_KEYS:
notams[key] = []
for notam in notam_list:
notams['idents'].append(validate_ident(notam['ident']))
notams['latitudes'].append(validate_lat(lat=notam['lat'].upper()))
notams['longitudes'].append(validate_lon(lon=notam['lon'].upper()))
notams['radii'].append(validate_radius(r=notam['rad'].upper()))
return notams
def prepare_background(map_type):
"""
Create an cylindrical equidistant map projection using low resolution
coastlines.
"""
outfile = os.path.join(*PLOT_DIR, '%s_map.png' % map_type)
print("Generating Background Map %s ..." % outfile)
fig = plt.figure(1)
left = 0.0
bottom = 0.0
width = 1.0
height = 1.0
ax = fig.add_axes([left, bottom, width, height])
print(' Creating Generic Basemap...')
map = Basemap(projection='cyl', llcrnrlon=-180, llcrnrlat=-90, urcrnrlon=180, urcrnrlat=90, resolution='l', ax=ax)
print(' Adding features - lines...')
# draw coastlines, country boundaries, state boundaries
map.drawcoastlines(linewidth=0.25)
map.drawcountries(linewidth=0.25)
map.drawstates(linewidth=0.15)
print(' Adding features - land/ocean...')
if map_type == 'basic':
map.drawlsmask(land_color='white', ocean_color='aqua', resolution='l')
map.drawlsmask(resolution='l')
elif map_type == 'marble':
map.bluemarble()
elif map_type == 'etopo':
map.etopo()
else: # default 'shaded'
map.shadedrelief()
print(' Saving...')
ax.axis('off')
fig.savefig(outfile, frameon=False, bbox_inches='tight', pad_inches=0, dpi=600)
plt.clf()
plt.close()
gc.collect()
def warp_map_image(map_type):
"""
Create an orthographic map projection from the perspective of a satellite
looking down at 45N, 100W using low resolution coastlines.
"""
infile = os.path.join(*PLOT_DIR, '%s_map.png' % map_type)
fig = plt.figure(2, frameon=False)
left = 0.0
bottom = 0.05
width = 1.0
height = 0.9
ax = fig.add_axes([left, bottom, width, height])
print(' Creating Basemap...')
map = Basemap(projection='ortho', lat_0=45, lon_0=-100, resolution='l', ax=ax)
try:
map.warpimage(infile)
except FileNotFoundError:
prepare_background(map_type)
map.warpimage(infile)
return fig, map
def make_plot(notams, day, outfile, map_type):
"""
Plot NOTAMs.
"""
fig, map = warp_map_image(map_type=map_type)
print(' Adding features - 30 degree lat/lon grid...')
map.drawmeridians(np.arange(0, 360, 30), zorder=2)
map.drawparallels(np.arange(-90, 90, 30), zorder=2)
print(' Adding Notams...')
idents = notams['idents']
latitudes = notams['latitudes']
longitudes = notams['longitudes']
# radii = notams['radii']
# Add Circles
for ii in range(len(idents)):
circle_lats, circle_lons = notams['circles'][ii]
x, y = map(circle_lons, circle_lats)
map.plot(x, y, marker=None, color='red', linewidth=1, zorder=15)
# Add labels
for ii in range(len(idents)):
x, y = map(longitudes[ii], latitudes[ii])
plt.text(
x, y, idents[ii], fontsize=2, fontweight='bold', ha='center',
va='center', color='white',
path_effects=[PathEffects.withStroke(
linewidth=3, foreground="black")],
zorder=20)
plt.title(day + ' NOTAMs')
print(' Saving...')
fig.savefig(outfile, dpi=300)
plt.clf()
plt.close()
gc.collect()
def compute_circles(notams):
"""
Returns `circles`, a list of tuples - each tuple contains an array of
latitudes and an array of longitudes defining one circle.
`notams` is a dictionary containing keys 'idents', 'latitudes',
'longitudes', and 'radii'.
"""
circles = []
idents = notams['idents']
latitudes = notams['latitudes']
longitudes = notams['longitudes']
radii = notams['radii']
for ii in range(len(idents)):
circle_lats, circle_lons = compute_circle(latitudes[ii], longitudes[ii], radii[ii])
circles.append((circle_lats, circle_lons))
return circles
def compute_circle(lat, lon, radius_nautical_miles):
"""
Returns a tuple containing an array of longitudes and latitudes defining
locations at the given radius around the location,
lon using the Haversine formula.
Based on https://stochasticcoder.com/2016/04/06/python-custom-distance-radius-with-basemap/
Adapted to use nautical miles instead of miles
"""
lats = []
lons = []
for bearing in range(0, 360):
lat2, lon2 = get_location(lat, lon, bearing, radius_nautical_miles)
lats.append(lat2)
lons.append(lon2)
return lats, lons
def get_location(lat1, lon1, bearing, distance_nautical_miles):
"""
Return the lat and lon of a location that has specified bearing and distance
from lat1, lon1.
Based on https://stochasticcoder.com/2016/04/06/python-custom-distance-radius-with-basemap/
Adapted to use distances in nautical miles
"""
lat1 = lat1 * math.pi / 180.0
lon1 = lon1 * math.pi / 180.0
# Earth's radius in nautical miles - ref http://science.answers.com/Q/What_is_the_radius_of_earth
R = 3440.07
distance_nautical_miles = distance_nautical_miles / R
bearing = (bearing / 90.0) * math.pi / 2.0
lat2 = math.asin(
math.sin(lat1) * math.cos(distance_nautical_miles) +
math.cos(lat1) * math.sin(distance_nautical_miles) * math.cos(bearing))
lon2 = lon1 + math.atan2(
math.sin(bearing) * math.sin(distance_nautical_miles) * math.cos(lat1),
math.cos(distance_nautical_miles) - math.sin(lat1) * math.sin(lat2))
lon2 = 180.0 * lon2 / math.pi
lat2 = 180.0 * lat2 / math.pi
return lat2, lon2
def utc_today():
"""
Return ISO formatted date for this day in UTC timezone.
"""
UTC = pytz.timezone('UTC')
return datetime.datetime.now(UTC).date().isoformat()
def build_options(day=False):
"""
Return dictionary options build from docopts.
If day is specified, argv will be ignored.
"""
if day:
options = docopt(__doc__, argv=['--date', day])
else:
options = docopt(__doc__)
if '--date' not in options or options['--date'] is None:
if day:
options['--date'] = day
else:
options['--date'] = utc_today()
if options['--basic']:
options['map-type'] = 'basic'
elif options['--marble']:
options['map-type'] = 'marble'
elif options['--etopo']:
options['map-type'] = 'etopo'
else: # default
options['map-type'] = 'shaded'
# build infile name based on date
if options['--infile'] is None:
options['--infile'] = os.path.join(*DATA_DIR, '_'.join([options['--date'], 'notams.yaml']))
if options['--outfile'] is None:
options['--outfile'] = os.path.join(*PLOT_DIR, '_'.join([options['--date'], 'notams.png']))
return options
if __name__ == '__main__':
main(options=build_options())