-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathiss_tracker.py
383 lines (298 loc) · 14.1 KB
/
iss_tracker.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
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
from flask import Flask, request
from geopy.geocoders import Nominatim
import requests, xmltodict, math, time
app = Flask(__name__)
@app.route('/delete-data', methods=['DELETE'])
def delete_data() -> str:
"""
This function deletes the data and replaces the data with a blank dictionary.
Returns:
message (str): Message saying that the data was deleted.
"""
#making DATA a global variable
global DATA
#simply setting DATA equal to nothing so that it "deletes" the data
DATA = {}
message = 'Successfully deleted all the data from the dictionary!\n'
return message
@app.route('/post-data', methods=['POST'])
def post_data() -> str:
"""
This function reloads the DATA dictionary object with the data from the website so that it can always
use the most updated data set.
Returns:
message (str): Message saying that the data was successfully reloaded.
"""
#making DATA a global variable
global DATA
#stores the data from the get request into the data variable and converts it into a dictionary
DATA = requests.get(url='https://nasa-public-data.s3.amazonaws.com/iss-coords/current/ISS_OEM/ISS.OEM_J2K_EPH.xml')
DATA = xmltodict.parse(DATA.text)
message = 'Successfully reloaded the dictionary with the data from the web!\n'
return message
@app.route('/', methods=['GET'])
def data() -> dict:
"""
This function returns the data in the form of a dictionary. If it hasn't been posted at all yet, it will
return a message saying that it doesn't exist. If the post method was called, it will print the data. Lastly,
if the delete method was called, it will return a blank dictionary.
Returns:
data (dict): The iss data in its current state.
"""
#try-except block that returns if the data doesn't exist and an error occurs because of it
try:
DATA
except NameError:
return 'The data set does not exist yet!\n'
return DATA
@app.route('/comment', methods=['GET'])
def get_comment() -> list:
"""
This function returns the comment list object from the ISS data. If it does not exist or is
empty, it returns a message saying so.
Returns:
comment (list): The comment list object from the ISS data.
"""
#try-except block that makes sure it returns a message if the data is empty or doesn't exist
try:
#stores the comment list object into the comment variable
comment = data()['ndm']['oem']['body']['segment']['data']['COMMENT']
except TypeError:
return 'The data set does not exist yet!\n'
except KeyError:
return 'The data is empty!\n'
return comment
@app.route('/header', methods=['GET'])
def get_header() -> dict:
"""
This function returns the header dictionary object from the ISS data. If it does not exist or
is empty, it returns a message saying so.
Returns:
header (dict): The header dictionary object from the ISS data.
"""
#try-except block that makes sure it returns a message if the data is empty or doesn't exist
try:
#stores the header dictionary object into the comment variable
header = data()['ndm']['oem']['header']
except TypeError:
return 'The data set does not exist yet!\n'
except KeyError:
return 'The data is empty!\n'
return header
@app.route('/metadata', methods=['GET'])
def get_metadata() -> dict:
"""
This function returns the metadata dictionary object from the ISS data. If it does not exist
or is empty, it returns a message saying so.
Returns:
metadata (dict): The metadata dictionary object from the ISS data.
"""
#try-except block that makes sure it returns a message if the data is empty or doesn't exist
try:
#stores the metadata dictionary object into the comment variable
metadata = data()['ndm']['oem']['body']['segment']['metadata']
except TypeError:
return 'The data set does not exist yet!\n'
except KeyError:
return 'The data is empty!\n'
return metadata
@app.route('/now', methods=['GET'])
def current_location() -> dict:
"""
This function returns the epoch data that is closest to the current time. It calculates the current time
and the epoch times into a UNIX timestamp then finds the epoch data that is closest by calculating the difference.
Returns:
locationData (dict): The location data for the specific epoch that is closest to the current time.
"""
#pulling the list of epochs from the epoch_data function
listOfEpochs = epoch_data()
#using the time python library to find the current time
timeNow = time.time()
#finding the time for the first item in the list to use in the loop
try:
timeEpoch = time.mktime(time.strptime(listOfEpochs[0][:-5], '%Y-%jT%H:%M:%S'))
except ValueError:
return 'The data does not exist or is empty!\n'
closestEpoch = listOfEpochs[0]
previousDifference = abs(timeNow - timeEpoch)
#for-loop that iterates through every epoch and finds the one with the smallest difference
for epoch in listOfEpochs:
timeEpoch = time.mktime(time.strptime(epoch[:-5], '%Y-%jT%H:%M:%S'))
difference = abs(timeNow - timeEpoch)
#if the new difference is smaller than the previous difference, change it to the current values
if difference < previousDifference:
closestEpoch = epoch
previousDifference = difference
#storing the location data for the closestEpoch into the locationData variable
locationData = location(closestEpoch)
return locationData
@app.route('/epochs', methods=['GET'])
def epoch_data() -> list:
"""
This function retrieves the entire stateVector data set and returns the results variable. It can take in query
parameters of offset and limit which will cause the data to start at a different point and limit the amount of data returned.
Returns:
results (list): The results of the entire list of Epochs from the iss data considering the offset and
limit parameters.
"""
#try-except block that makes sure it returns a message if the data is empty or doesn't exist
try:
#stores the entire epoch data by navigating through the entire data dictionary
listOfEpochs = data()['ndm']['oem']['body']['segment']['data']['stateVector']
except TypeError:
return 'The data set does not exist yet!\n'
except KeyError:
return 'The data is empty!\n'
#try and except blocks for the limit and offset variables so that it can only be an integer
try:
limit = int(request.args.get('limit', len(listOfEpochs)))
except ValueError:
return 'ERROR: Please send an integer for the limit!\n', 400
try:
offset = int(request.args.get('offset', 0))
except ValueError:
return 'ERROR: Please send an integer for the offset!\n', 400
#initializing a new blank list to store the "new" data
results = []
#for loop that stores the requested Epoch data
for i in range(limit):
results.append(listOfEpochs[i+offset]['EPOCH'])
return results
@app.route('/epochs/<string:epoch>', methods=['GET'])
def specific_epoch_data(epoch: str) -> dict:
"""
This function returns the specific epoch data that was requested by the user.
Args:
epoch (str): The specific epoch key to find the requested epoch data.
Returns:
epochData (dict): The epoch data for the given epoch key.
"""
#try-except block to make sure the data has information
try:
#stores the entire epoch data by navigating through the entire data dictionary
epochData = data()['ndm']['oem']['body']['segment']['data']['stateVector']
except NameError:
return 'The data seems to be empty or does not exist...\n'
except KeyError:
return 'The data seems to be empty or does not exist...\n'
except TypeError:
return 'The data seems to be empty or does not exist...\n'
#sorts through the list to match the epoch key and returns the data for it
for i in range(len(epochData)):
if epochData[i]['EPOCH'] == epoch:
return epochData[i]
#if it doesn't find it, returns this prompt
return 'Could not find the epoch for the given key.\n'
@app.route('/epochs/<string:epoch>/location', methods=['GET'])
def location(epoch: str) -> dict:
"""
This function returns the location of a specific epoch that the user requested. It
should return the Epoch key, the latitude, the longitude, the altitude, the geographical
location information, and the speed at which the ISS is traveling at that moment.
Args:
epoch (str): The specific epoch key to find the requested epoch data.
Returns:
epochLocation (dict): The location data for the specific epoch.
"""
#using the already existing specific_epoch_data function to pull its data
specificEpoch = specific_epoch_data(epoch)
#the mean earth radius that was found from a google search
MEAN_EARTH_RADIUS = 6371
#try-except block to return a message if the data is empty or doesn't exist
try:
#setting the x, y, z, units, and epoch key to its corresponding variables
x = float(specificEpoch['X']['#text'])
except TypeError:
return 'The data seems to be empty or does not exist...\n'
y = float(specificEpoch['Y']['#text'])
z = float(specificEpoch['Z']['#text'])
units = specificEpoch['X']['@units']
epoch = specificEpoch['EPOCH']
#indexing part of the epoch key to find the hrs and mins
hrs = int(epoch[9:11])
mins = int(epoch[12:14])
#the equations to calculate the latitude, longitude, and the altitude
lat = math.degrees(math.atan2(z, math.sqrt(x**2 + y**2)))
lon = math.degrees(math.atan2(y, x)) - ((hrs-12)+(mins/60))*(360/24) + 32
alt = math.sqrt(x**2 + y**2 + z**2) - MEAN_EARTH_RADIUS
#since latitude and longitude doesn't go past 180, change it to the corresponding negative value
if lat > 180:
lat = lat - 360
if lon > 180:
lon = lon - 360
#using the GeoPy library
geocoder = Nominatim(user_agent='iss_tracker')
#try-except block in case the server for the geopy is down
try:
geoloc = geocoder.reverse((lat, lon), zoom = 10, language = 'en')
except Error as e:
return f'Geopy returned an error - {e}\n'
#try-except that is executed whenever the ISS is over an ocean
try:
geoloc = geoloc.raw
except AttributeError:
geoloc = 'The ISS must be over an ocean...'
#pulling the speed of the epoch at the location
speed = calculate_epoch_speed(epoch)['speed']
#putting all the data into one dictionary to return
epochLocation = {'Epoch': epoch, 'Location': {'latitude': lat, 'longitude': lon, 'altitude': {'value': alt, 'units': units}}, 'geo': geoloc, 'speed': speed}
return epochLocation
@app.route('/epochs/<string:epoch>/speed', methods=['GET'])
def calculate_epoch_speed(epoch: str) -> dict:
"""
This function calculates the speed for the specific epoch and returns it.
Args:
epoch (str): The specific epoch key to find the requested epoch data.
Returns:
speedData (dict): The speed data for the specific epoch requested.
"""
#stores the specific epoch using the pre-existing function
specificEpoch = specific_epoch_data(epoch)
#try-except block to make sure the data has information
try:
specificEpoch['X_DOT']['#text']
except TypeError:
return 'Could not calculate the speed of the epoch for the given key.\n'
#stores the X_DOT, Y_DOT, and Z_DOT for the specific epoch into corresponding variables and converts them to float
xDot = float(specificEpoch['X_DOT']['#text'])
yDot = float(specificEpoch['Y_DOT']['#text'])
zDot = float(specificEpoch['Z_DOT']['#text'])
#the units for the vector
units = specificEpoch['X_DOT']['@units']
#calculates the speed using the magnitude of a vector formula
speed = math.sqrt(xDot**2 + yDot**2 + zDot**2)
#storing the output into a new dictionary
speedData = {'speed': {'value': speed, 'units': units}}
return speedData
@app.route('/help', methods=['GET'])
def help() -> str:
"""
This function returns a human readable string that explains all the available
routes in this API.
Returns:
helpOutput (str): The string that explains the routes.
"""
helpOutput = '''usage: curl localhost:5000[<route>][?<query parameter>]\n
The different possible routes:
/post-data Loads/reloads the dictionary with data from the website
/delete-data Deletes all the data from the dictionary
/ Returns the entire data set (if it exists)
/now Returns the current location of the ISS
/epochs Returns the list of all Epochs in the data set
/epochs/<epoch> Returns the state vectors for a specific Epoch from the data set
/epochs/<epoch>/location Returns the location for a specific Epoch in the data set
/epochs/<epoch>/speed Returns the instantaneous speed for a specific Epoch in the data set
/comment Returns the comments in the ISS data
/header Returns the header in the ISS data
/metadata Returns the metadata in the ISS data
/help Returns the help text that describes each route
The different query parameters (only works for the "/epochs" route):
limit=<int> Returns a specific integer amount of Epochs from the data set
offset=<int> Returns the entire data set starting offset by a certain integer amount
limit=<int>'&'offset=<int> Combining the limit and offset query parameters
example:
/epochs?limit=15'&'offset=3 Returns the 15 Epochs from the data set offset by 3
'''
return helpOutput
if __name__ == '__main__':
app.run(debug=True, host='0.0.0.0')