-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathflight_service.py
More file actions
237 lines (186 loc) · 7.83 KB
/
flight_service.py
File metadata and controls
237 lines (186 loc) · 7.83 KB
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
"""
Web service for finding closest flights using FlightRadarAPI
"""
from dotenv import load_dotenv
from flask import Flask, jsonify, request
from FlightRadar24 import FlightRadar24API
from math import radians, cos
import os
load_dotenv()
app = Flask(__name__)
fr_api = FlightRadar24API()
API_KEY = os.getenv("SERVICE_API_KEY", None)
def calculate_bounds(lat: float, lon: float, radius_km: float) -> str:
"""Calculate bounding box for search area."""
lat_offset = radius_km / 111.0
lon_offset = radius_km / (111.0 * cos(radians(lat)))
north = lat + lat_offset
south = lat - lat_offset
west = lon - lon_offset
east = lon + lon_offset
return f"{north},{south},{west},{east}"
def validate_api_key():
"""Validate API key if configured."""
if API_KEY is None:
return True
provided_key = request.headers.get('X-API-Key')
return provided_key == API_KEY
def serialize_flight(flight):
"""Convert FlightRadar24 flight object to a serializable dictionary."""
flight_data = {
"id": flight.id,
"number": flight.number,
"callsign": flight.callsign,
"icao_24bit": flight.icao_24bit,
"position": {
"latitude": flight.latitude,
"longitude": flight.longitude,
"altitude": flight.altitude,
"heading": flight.heading,
"ground_speed": flight.ground_speed,
"vertical_speed": flight.vertical_speed
},
"aircraft": {
"code": flight.aircraft_code,
"registration": flight.registration
},
"airline": {
"icao": flight.airline_icao,
"iata": flight.airline_iata
},
"route": {
"origin_iata": flight.origin_airport_iata,
"destination_iata": flight.destination_airport_iata
}
}
# add detailed info if available
if hasattr(flight, 'aircraft_model'):
flight_data["aircraft"]["model"] = flight.aircraft_model
if hasattr(flight, 'origin_airport_name'):
flight_data["route"]["origin_name"] = flight.origin_airport_name
if hasattr(flight, 'destination_airport_name'):
flight_data["route"]["destination_name"] = flight.destination_airport_name
return flight_data
def parse_and_validate_params():
"""Parse query parameters and validate them."""
try:
lat = float(request.args.get('lat'))
lon = float(request.args.get('lon'))
radius_km = float(request.args.get('radius', 10))
if not (-90 <= lat <= 90):
return None, None, None, jsonify({"error": "Latitude must be between -90 and 90"}), 400
if not (-180 <= lon <= 180):
return None, None, None, jsonify({"error": "Longitude must be between -180 and 180"}), 400
if not (1 <= radius_km <= 500):
return None, None, None, jsonify({"error": "Radius must be between 1 and 500 km"}), 400
return lat, lon, radius_km, None, None
except (TypeError, ValueError):
return None, None, None, jsonify({"error": "Invalid parameters. Required: lat, lon. Optional: radius"}), 400
@app.route('/health', methods=['GET'])
def health_check():
"""Simple health check endpoint."""
return jsonify({"status": "ok"}), 200
@app.route('/closest-flight', methods=['GET'])
def get_closest_flight():
"""Find the closest flight to given coordinates."""
if not validate_api_key():
return jsonify({"error": "Unauthorized"}), 401
lat, lon, radius_km, error_response, status = parse_and_validate_params()
if error_response:
return error_response, status
try:
bounds = calculate_bounds(lat, lon, radius_km)
flights = fr_api.get_flights(bounds=bounds)
if not flights:
return jsonify({"found": False, "message": "No flights found in search area"}), 200
closest_flight = None
min_distance = float('inf')
class SearchPoint:
def __init__(self, lat, lon):
self.latitude = lat
self.longitude = lon
search_point = SearchPoint(lat, lon)
for flight in flights:
if flight.on_ground or flight.latitude is None or flight.longitude is None:
continue
distance = flight.get_distance_from(search_point)
if distance < min_distance:
min_distance = distance
closest_flight = flight
if not closest_flight:
return jsonify({"found": False, "message": "No airborne flights found in search area"}), 200
try:
flight_details = fr_api.get_flight_details(closest_flight)
closest_flight.set_flight_details(flight_details)
except:
pass # proceed without detailed flight info if fetching fails
response = {
"found": True,
"distance_km": round(min_distance, 2),
"flight": serialize_flight(closest_flight)
}
return jsonify(response), 200
except Exception as e:
return jsonify({"error": f"Server error: {str(e)}"}), 500
@app.route('/flights-in-radius', methods=['GET'])
def get_flights_in_radius():
"""Find all flights within a given radius of coordinates."""
if not validate_api_key():
return jsonify({"error": "Unauthorized"}), 401
lat, lon, radius_km, error_response, status = parse_and_validate_params()
if error_response:
return error_response, status
try:
bounds = calculate_bounds(lat, lon, radius_km)
flights = fr_api.get_flights(bounds=bounds)
if not flights:
return jsonify({"found": False, "message": "No flights found in search area"}), 200
response = {"found": True, "flights": []}
for flight in flights:
if flight.on_ground or flight.latitude is None or flight.longitude is None:
continue
try:
flight_details = fr_api.get_flight_details(flight)
flight.set_flight_details(flight_details)
except:
pass # proceed without detailed flight info if fetching fails
response["flights"].append(serialize_flight(flight))
if not response["flights"]:
return jsonify({"found": False, "message": "No airborne flights found in search area"}), 200
return jsonify(response), 200
except Exception as e:
return jsonify({"error": f"Server error: {str(e)}"}), 500
@app.route('/', methods=['GET'])
def index():
"""Root endpoint with API documentation."""
return jsonify({
"service": "Flight Finder API",
"version": "1.0",
"endpoints": {
"/health": {"method": "GET", "description": "Health check"},
"/closest-flight": {
"method": "GET",
"description": "Find closest flight to coordinates",
"parameters": {
"lat": "Latitude (required, -90 to 90)",
"lon": "Longitude (required, -180 to 180)",
"radius": "Search radius in km (optional, default 10, max 500)"
},
"example": "/closest-flight?lat=37.7749&lon=-122.4194&radius=10"
},
"/flights-in-radius": {
"method": "GET",
"description": "Find all flights within a given radius of coordinates",
"parameters": {
"lat": "Latitude (required, -90 to 90)",
"lon": "Longitude (required, -180 to 180)",
"radius": "Search radius in km (optional, default 10, max 500)"
},
"example": "/flights-in-radius?lat=37.7749&lon=-122.4194&radius=10"
}
}
}), 200
if __name__ == '__main__':
# for development, in production use the Procfile instead
port = int(os.getenv('PORT', 7478))
app.run(host='0.0.0.0', port=port, debug=False)