Azie88 commited on
Commit
9f9ebbd
·
1 Parent(s): a10749f

latest app folium

Browse files
Files changed (1) hide show
  1. app.py +681 -78
app.py CHANGED
@@ -2,100 +2,703 @@ import gradio as gr
2
  import numpy as np
3
  import pandas as pd
4
  import os, joblib
5
- import re
 
 
 
 
6
 
7
- # load model pipeline
8
  file_path = os.path.abspath('toolkit/pipeline.joblib')
9
  pipeline = joblib.load(file_path)
10
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
 
12
- #function to calculate week hour from weekday and hour
13
  def calculate_pickup_week_hour(pickup_hour, pickup_weekday):
14
  return pickup_weekday * 24 + pickup_hour
15
 
16
- def predict(origin_lat, origin_lon, Destination_lat, Destination_lon,
17
- Trip_distance, dewpoint_2m_temperature,
18
- minimum_2m_air_temperature, pickup_weekday, pickup_hour,
19
- cluster_id, temperature_range, rain):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
20
 
21
  # Calculate pickup_week_hour
22
  pickup_week_hour = calculate_pickup_week_hour(pickup_hour, pickup_weekday)
23
-
24
- # Modeling
 
 
 
 
25
  try:
26
- model_output = abs(int(pipeline.predict(pd.DataFrame([[origin_lat, origin_lon, Destination_lat, Destination_lon,
27
- Trip_distance, dewpoint_2m_temperature,
28
- minimum_2m_air_temperature, pickup_weekday, pickup_hour,
29
- pickup_week_hour, cluster_id, temperature_range,
30
- rain]], columns=['Origin_lat', 'Origin_lon', 'Destination_lat',
31
- 'Destination_lon', 'Trip_distance',
32
- 'dewpoint_2m_temperature',
33
- 'minimum_2m_air_temperature',
34
- 'pickup_weekday', 'pickup_hour',
35
- 'pickup_week_hour', 'cluster_id',
36
- 'temperature_range', 'rain']))))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
37
  except Exception as e:
38
- print(f"Error during prediction: {str(e)}")
39
- model_output = 0
40
 
41
- output_str = 'Hey there, Your ETA is'
42
- dist = 'seconds'
 
 
 
 
 
 
 
 
 
 
 
43
 
44
- return f"{output_str} {model_output} {dist}"
 
 
 
45
 
46
- # UI layout
47
- with gr.Blocks(theme=gr.themes.Soft()) as app:
48
- gr.Markdown("# ETA PREDICTION")
49
- gr.Markdown("""This app uses a machine learning model to predict the ETA of trips on the Yassir Hailing App.Refer to the expander at the bottom for more information on the inputs.""")
50
 
51
- with gr.Row():
52
- origin_lat = gr.Slider(2.806, 3.373, step=0.001, interactive=True, value=2.806, label='Origin latitude')
53
- origin_lon = gr.Slider(36.589, 36.820, step=0.001, interactive=True, value=36.589, label='Origin longitude')
54
- Destination_lat = gr.Slider(2.807, 3.381, step=0.001, interactive=True, value=2.810, label='Destination latitude')
55
- Destination_lon = gr.Slider(36.592, 36.819, step=0.001, interactive=True, value=36.596, label='Destination longitude')
56
- Trip_distance = gr.Slider(0, 62028, step=1, interactive=True, value=1000, label='Trip distance (M)')
57
- cluster_id = gr.Dropdown([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], label="Cluster ID", value=4)
58
-
59
- with gr.Column():
60
- pickup_weekday = gr.Dropdown([0, 1, 2, 3, 4, 5, 6], value=3, label='Pickup weekday')
61
- pickup_hour = gr.Dropdown([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23],
62
- value=13, label='Pickup hour')
63
-
64
- with gr.Column():
65
- dewpoint_2m_temperature = gr.Slider(279.129, 286.327, step=0.001, interactive=True, value=282.201,
66
- label='dewpoint_2m_temperature')
67
- minimum_2m_air_temperature = gr.Slider(282.037, 292.543, step=0.01, interactive=True, value=285.203,
68
- label='minimum_2m_air_temperature')
69
- temperature_range = gr.Slider(1.663, 10.022, step=0.01, interactive=True, value=5.583, label='temperature_range')
70
- rain = gr.Dropdown([0, 1], label='Is it raining (0=No, 1=Yes)')
71
 
 
 
 
 
 
 
 
 
 
 
 
 
72
  with gr.Row():
73
- btn = gr.Button("Predict")
74
- output = gr.Textbox(label="Prediction")
75
-
76
- # Expander for more info on columns
77
- with gr.Accordion("Information on inputs"):
78
- gr.Markdown("""These are information on the inputs the app takes for predicting a rides ETA.
79
- - Origin latitude: Origin in degree latitude)
80
- - Origin longitude: Origin in degree longitude
81
- - Destination latitude: Destination latitude
82
- - Destination longitude: Destination logitude
83
- - Trip distance (M): Distance in meters on a driving route
84
- - Cluster ID: Select the cluster within which you started your trip
85
- - Pickup weekday: Day of the week
86
- Monday=0, Tuesday=1, Wednesday=2, Thursday=3, Friday=4, Saturday=5, Sunday=6
87
- - Pickup hour: The hour of the day (24hr clock)
88
- - dewpoint_2m_temperature: The temperature at 2 meters above the ground where the air temperature would be
89
- low enough for dew to form. It gives an indication of humidity.
90
- - minimum_2m_air_temperature: The lowest air temperature recorded at 2 meters above the ground during the specified date.
91
- - temperature_range: The air temperature range recorded at 2 meters above the ground on the day
92
- - rain: Is it raining? yes=1, no=2
93
- """)
94
-
95
- btn.click(fn=predict, inputs=[origin_lat, origin_lon, Destination_lat, Destination_lon,
96
- Trip_distance, dewpoint_2m_temperature,
97
- minimum_2m_air_temperature, pickup_weekday, pickup_hour,
98
- cluster_id, temperature_range,
99
- rain], outputs=output)
100
-
101
- app.launch(share=True, debug=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
  import numpy as np
3
  import pandas as pd
4
  import os, joblib
5
+ from datetime import datetime
6
+ from math import radians, cos, sin, asin, sqrt
7
+ import folium
8
+ from folium import plugins
9
+ import requests
10
 
11
+ # Load model pipeline
12
  file_path = os.path.abspath('toolkit/pipeline.joblib')
13
  pipeline = joblib.load(file_path)
14
 
15
+ # Global variables for dynamic bounds
16
+ current_bounds = {
17
+ 'lat_min': -90,
18
+ 'lat_max': 90,
19
+ 'lon_min': -180,
20
+ 'lon_max': 180,
21
+ 'center_lat': 0,
22
+ 'center_lon': 0
23
+ }
24
+
25
+ # Cluster centroids - these will be auto-generated based on location
26
+ CLUSTER_CENTROIDS = {}
27
+
28
+ def geocode_location(place_name):
29
+ """
30
+ Convert place name/address to coordinates using Nominatim (OpenStreetMap)
31
+ Free and no API key required!
32
+ """
33
+ try:
34
+ url = "https://nominatim.openstreetmap.org/search"
35
+ params = {
36
+ 'q': place_name,
37
+ 'format': 'json',
38
+ 'limit': 1
39
+ }
40
+ headers = {
41
+ 'User-Agent': 'ETA-Prediction-App/1.0'
42
+ }
43
+
44
+ response = requests.get(url, params=params, headers=headers, timeout=5)
45
+
46
+ if response.status_code == 200:
47
+ data = response.json()
48
+ if data:
49
+ lat = float(data[0]['lat'])
50
+ lon = float(data[0]['lon'])
51
+ display_name = data[0]['display_name']
52
+ return lat, lon, display_name, None
53
+ else:
54
+ return None, None, None, "Location not found. Please try a different search term."
55
+ else:
56
+ return None, None, None, f"Geocoding service error: {response.status_code}"
57
+ except Exception as e:
58
+ return None, None, None, f"Error: {str(e)}"
59
+
60
+ def reverse_geocode(lat, lon):
61
+ """
62
+ Convert coordinates to place name
63
+ """
64
+ try:
65
+ url = "https://nominatim.openstreetmap.org/reverse"
66
+ params = {
67
+ 'lat': lat,
68
+ 'lon': lon,
69
+ 'format': 'json'
70
+ }
71
+ headers = {
72
+ 'User-Agent': 'ETA-Prediction-App/1.0'
73
+ }
74
+
75
+ response = requests.get(url, params=params, headers=headers, timeout=5)
76
+
77
+ if response.status_code == 200:
78
+ data = response.json()
79
+ return data.get('display_name', 'Unknown location')
80
+ else:
81
+ return f"Lat: {lat:.4f}, Lon: {lon:.4f}"
82
+ except:
83
+ return f"Lat: {lat:.4f}, Lon: {lon:.4f}"
84
+
85
+ def haversine_distance(lat1, lon1, lat2, lon2):
86
+ """
87
+ Calculate the great circle distance between two points
88
+ on the earth (specified in decimal degrees)
89
+ Returns distance in meters
90
+ """
91
+ lon1, lat1, lon2, lat2 = map(radians, [lon1, lat1, lon2, lat2])
92
+
93
+ dlon = lon2 - lon1
94
+ dlat = lat2 - lat1
95
+ a = sin(dlat/2)**2 + cos(lat1) * cos(lat2) * sin(dlon/2)**2
96
+ c = 2 * asin(sqrt(a))
97
+ r = 6371000 # Radius of earth in meters
98
+ return c * r
99
+
100
+ def generate_clusters_for_area(center_lat, center_lon, radius_km=50):
101
+ """
102
+ Generate cluster centroids around a center point
103
+ Creates a grid of 10 clusters
104
+ """
105
+ clusters = {}
106
+ # Create a 3x3 grid plus center point = 10 clusters
107
+ positions = [
108
+ (0, 0), # Center - cluster 0
109
+ (-1, -1), (-1, 0), (-1, 1), # Top row - clusters 1,2,3
110
+ (0, -1), (0, 1), # Middle row - clusters 4,5
111
+ (1, -1), (1, 0), (1, 1) # Bottom row - clusters 6,7,8
112
+ ]
113
+
114
+ # Convert km to degrees (approximate)
115
+ lat_offset = radius_km / 111.0 # 1 degree latitude ≈ 111 km
116
+ lon_offset = radius_km / (111.0 * cos(radians(center_lat)))
117
+
118
+ for i, (lat_mult, lon_mult) in enumerate(positions):
119
+ if i < 10: # Only 10 clusters
120
+ clusters[i] = (
121
+ center_lat + (lat_mult * lat_offset / 3),
122
+ center_lon + (lon_mult * lon_offset / 3)
123
+ )
124
+
125
+ # Add 10th cluster if needed
126
+ if len(clusters) < 10:
127
+ clusters[9] = (center_lat + lat_offset/6, center_lon + lon_offset/6)
128
+
129
+ return clusters
130
+
131
+ def assign_cluster(lat, lon, clusters):
132
+ """
133
+ Assign cluster ID based on nearest centroid
134
+ """
135
+ if not clusters:
136
+ return 4 # default middle cluster
137
+
138
+ min_dist = float('inf')
139
+ assigned_cluster = 4
140
+
141
+ for cluster_id, (c_lat, c_lon) in clusters.items():
142
+ dist = haversine_distance(lat, lon, c_lat, c_lon)
143
+ if dist < min_dist:
144
+ min_dist = dist
145
+ assigned_cluster = cluster_id
146
+
147
+ return assigned_cluster
148
+
149
+ def create_interactive_map(origin_coords=None, dest_coords=None, zoom=12):
150
+ """
151
+ Create an interactive Folium map for visualizing route
152
+ """
153
+ if origin_coords:
154
+ center = origin_coords
155
+ elif dest_coords:
156
+ center = dest_coords
157
+ else:
158
+ center = [0, 0]
159
+
160
+ m = folium.Map(
161
+ location=center,
162
+ zoom_start=zoom,
163
+ tiles='OpenStreetMap'
164
+ )
165
+
166
+ # Add markers if coordinates are provided
167
+ if origin_coords:
168
+ folium.Marker(
169
+ origin_coords,
170
+ popup=f"<b>Origin</b><br>{origin_coords[0]:.4f}, {origin_coords[1]:.4f}",
171
+ tooltip="Pickup Location",
172
+ icon=folium.Icon(color='green', icon='play', prefix='fa')
173
+ ).add_to(m)
174
+
175
+ if dest_coords:
176
+ folium.Marker(
177
+ dest_coords,
178
+ popup=f"<b>Destination</b><br>{dest_coords[0]:.4f}, {dest_coords[1]:.4f}",
179
+ tooltip="Dropoff Location",
180
+ icon=folium.Icon(color='red', icon='stop', prefix='fa')
181
+ ).add_to(m)
182
+
183
+ # Draw line between origin and destination
184
+ if origin_coords and dest_coords:
185
+ folium.PolyLine(
186
+ [origin_coords, dest_coords],
187
+ color='blue',
188
+ weight=4,
189
+ opacity=0.8,
190
+ popup=f"Distance: {haversine_distance(origin_coords[0], origin_coords[1], dest_coords[0], dest_coords[1])/1000:.2f} km"
191
+ ).add_to(m)
192
+
193
+ # Fit bounds to show both markers
194
+ m.fit_bounds([origin_coords, dest_coords])
195
+
196
+ return m._repr_html_()
197
 
 
198
  def calculate_pickup_week_hour(pickup_hour, pickup_weekday):
199
  return pickup_weekday * 24 + pickup_hour
200
 
201
+ def format_eta_output(seconds):
202
+ """
203
+ Convert seconds to human-readable format
204
+ """
205
+ if seconds < 60:
206
+ return f"{seconds} seconds"
207
+ elif seconds < 3600:
208
+ minutes = seconds // 60
209
+ secs = seconds % 60
210
+ return f"{minutes} min {secs} sec" if secs > 0 else f"{minutes} min"
211
+ else:
212
+ hours = seconds // 3600
213
+ minutes = (seconds % 3600) // 60
214
+ return f"{hours}h {minutes}min"
215
+
216
+ def search_origin(search_term):
217
+ """Handle origin location search"""
218
+ if not search_term.strip():
219
+ return None, None, "Please enter a location to search"
220
+
221
+ lat, lon, display_name, error = geocode_location(search_term)
222
+
223
+ if error:
224
+ return None, None, f"❌ {error}"
225
+
226
+ return lat, lon, f"✅ Found: {display_name}"
227
+
228
+ def search_destination(search_term):
229
+ """Handle destination location search"""
230
+ if not search_term.strip():
231
+ return None, None, "Please enter a location to search"
232
+
233
+ lat, lon, display_name, error = geocode_location(search_term)
234
+
235
+ if error:
236
+ return None, None, f"❌ {error}"
237
+
238
+ return lat, lon, f"✅ Found: {display_name}"
239
+
240
+ def predict(origin_lat, origin_lon, dest_lat, dest_lon,
241
+ dewpoint_temp, min_temp, temp_range, rain,
242
+ pickup_weekday, pickup_hour, manual_distance, auto_calc_distance, cluster_override):
243
+
244
+ # Validate that coordinates are provided
245
+ if origin_lat is None or origin_lon is None:
246
+ return "❌ Please provide origin coordinates", ""
247
+ if dest_lat is None or dest_lon is None:
248
+ return "❌ Please provide destination coordinates", ""
249
+
250
+ # Generate clusters around origin area
251
+ clusters = generate_clusters_for_area(origin_lat, origin_lon)
252
+
253
+ # Calculate or use manual distance
254
+ if auto_calc_distance:
255
+ trip_distance = haversine_distance(origin_lat, origin_lon, dest_lat, dest_lon)
256
+ distance_info = f"📏 Auto-calculated distance: {trip_distance:.0f}m ({trip_distance/1000:.2f}km)"
257
+ else:
258
+ trip_distance = manual_distance
259
+ distance_info = f"📏 Manual distance: {trip_distance:.0f}m ({trip_distance/1000:.2f}km)"
260
+
261
+ # Auto-assign cluster or use override
262
+ if cluster_override == "Auto":
263
+ cluster_id = assign_cluster(origin_lat, origin_lon, clusters)
264
+ cluster_info = f"🗺️ Auto-assigned cluster: {cluster_id}"
265
+ else:
266
+ cluster_id = int(cluster_override)
267
+ cluster_info = f"🗺️ Manual cluster: {cluster_id}"
268
 
269
  # Calculate pickup_week_hour
270
  pickup_week_hour = calculate_pickup_week_hour(pickup_hour, pickup_weekday)
271
+
272
+ # Get location names
273
+ origin_name = reverse_geocode(origin_lat, origin_lon)
274
+ dest_name = reverse_geocode(dest_lat, dest_lon)
275
+
276
+ # Prediction
277
  try:
278
+ model_output = abs(int(pipeline.predict(pd.DataFrame([[
279
+ origin_lat, origin_lon, dest_lat, dest_lon,
280
+ trip_distance, dewpoint_temp, min_temp,
281
+ pickup_weekday, pickup_hour, pickup_week_hour,
282
+ cluster_id, temp_range, rain
283
+ ]], columns=[
284
+ 'Origin_lat', 'Origin_lon', 'Destination_lat', 'Destination_lon',
285
+ 'Trip_distance', 'dewpoint_2m_temperature', 'minimum_2m_air_temperature',
286
+ 'pickup_weekday', 'pickup_hour', 'pickup_week_hour',
287
+ 'cluster_id', 'temperature_range', 'rain'
288
+ ]))[0]))
289
+
290
+ # Format output
291
+ eta_formatted = format_eta_output(model_output)
292
+
293
+ # Calculate average speed
294
+ if model_output > 0:
295
+ avg_speed_mps = trip_distance / model_output
296
+ avg_speed_kmh = avg_speed_mps * 3.6
297
+ speed_info = f"🚗 Average speed: {avg_speed_kmh:.1f} km/h"
298
+ else:
299
+ speed_info = "⚠️ Invalid ETA prediction"
300
+
301
+ weekdays = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday']
302
+
303
+ result = f"""
304
+ # 🎯 Estimated Time of Arrival
305
+
306
+ ## ⏱️ **ETA: {eta_formatted}** ({model_output} seconds)
307
+
308
+ ---
309
+
310
+ ### 📍 Route Information
311
+
312
+ **Origin:** {origin_name}
313
+ *Coordinates:* {origin_lat:.6f}, {origin_lon:.6f}
314
+
315
+ **Destination:** {dest_name}
316
+ *Coordinates:* {dest_lat:.6f}, {dest_lon:.6f}
317
+
318
+ {distance_info}
319
+ {cluster_info}
320
+
321
+ ---
322
+
323
+ ### 🕐 Trip Details
324
+
325
+ - **Day:** {weekdays[pickup_weekday]}
326
+ - **Time:** {pickup_hour:02d}:00
327
+ - {speed_info}
328
+
329
+ ---
330
+
331
+ ### 🌤️ Weather Conditions
332
+
333
+ - **Dewpoint:** {dewpoint_temp:.1f}K ({dewpoint_temp-273.15:.1f}°C)
334
+ - **Min Temperature:** {min_temp:.1f}K ({min_temp-273.15:.1f}°C)
335
+ - **Temperature Range:** {temp_range:.1f}K
336
+ - **Rain:** {"Yes ☔" if rain == 1 else "No ☀️"}
337
+ """
338
+
339
+ # Update map
340
+ map_html = create_interactive_map(
341
+ origin_coords=[origin_lat, origin_lon],
342
+ dest_coords=[dest_lat, dest_lon]
343
+ )
344
+
345
+ return result, map_html
346
+
347
  except Exception as e:
348
+ return f" Prediction failed: {str(e)}\n\nPlease check your inputs and ensure the model file is loaded correctly.", ""
 
349
 
350
+ def update_map_only(origin_lat, origin_lon, dest_lat, dest_lon):
351
+ """Update map visualization without prediction"""
352
+ if origin_lat and origin_lon and dest_lat and dest_lon:
353
+ return create_interactive_map(
354
+ origin_coords=[origin_lat, origin_lon],
355
+ dest_coords=[dest_lat, dest_lon]
356
+ )
357
+ elif origin_lat and origin_lon:
358
+ return create_interactive_map(origin_coords=[origin_lat, origin_lon])
359
+ elif dest_lat and dest_lon:
360
+ return create_interactive_map(dest_coords=[dest_lat, dest_lon])
361
+ else:
362
+ return create_interactive_map()
363
 
364
+ def use_current_time():
365
+ """Get current day and hour"""
366
+ now = datetime.now()
367
+ return now.weekday(), now.hour
368
 
369
+ def use_sample_nairobi():
370
+ """Load sample Nairobi coordinates"""
371
+ return 2.950, 36.700, 3.000, 36.750, "Sample: Nairobi area loaded ✅"
 
372
 
373
+ def use_sample_newyork():
374
+ """Load sample New York coordinates"""
375
+ return 40.7580, -73.9855, 40.7128, -74.0060, "Sample: New York (Times Square → Downtown) loaded ✅"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
376
 
377
+ def use_sample_london():
378
+ """Load sample London coordinates"""
379
+ return 51.5074, -0.1278, 51.5155, -0.0922, "Sample: London (Westminster → Tower Bridge) loaded ✅"
380
+
381
+ # UI Layout
382
+ with gr.Blocks(title="Universal ETA Prediction") as app:
383
+ gr.Markdown("# 🌍 Universal ETA Prediction App")
384
+ gr.Markdown("""
385
+ Predict ride ETA for **any location worldwide**. Search for places by name or enter coordinates manually.
386
+ """)
387
+
388
+ # First Row - Two Columns for Inputs
389
  with gr.Row():
390
+ # Left Column - Location & Time Settings
391
+ with gr.Column(scale=1):
392
+ gr.Markdown("### 📍 Location Settings")
393
+
394
+ # Quick sample locations
395
+ gr.Markdown("**Quick Load Samples:**")
396
+ with gr.Row():
397
+ sample_nairobi = gr.Button("📍 Nairobi", size="sm")
398
+ sample_ny = gr.Button("📍 New York", size="sm")
399
+ sample_london = gr.Button("📍 London", size="sm")
400
+
401
+ sample_status = gr.Textbox(label="Status", interactive=False, visible=False)
402
+
403
+ # Origin section
404
+ gr.Markdown("**🟢 Origin (Pickup Location)**")
405
+ origin_search = gr.Textbox(
406
+ label="Search for origin",
407
+ placeholder="e.g., Times Square, New York OR Nairobi CBD",
408
+ info="Type a place name, address, or landmark"
409
+ )
410
+ origin_search_btn = gr.Button("🔍 Search Origin", size="sm", variant="secondary")
411
+ origin_search_status = gr.Textbox(label="Search Result", interactive=False, visible=False)
412
+
413
+ with gr.Row():
414
+ origin_lat = gr.Number(label='Origin Latitude', precision=6, value=None)
415
+ origin_lon = gr.Number(label='Origin Longitude', precision=6, value=None)
416
+
417
+ gr.Markdown("---")
418
+
419
+ # Destination section
420
+ gr.Markdown("**🔴 Destination (Dropoff Location)**")
421
+ dest_search = gr.Textbox(
422
+ label="Search for destination",
423
+ placeholder="e.g., Central Park, New York OR JKIA Airport",
424
+ info="Type a place name, address, or landmark"
425
+ )
426
+ dest_search_btn = gr.Button("🔍 Search Destination", size="sm", variant="secondary")
427
+ dest_search_status = gr.Textbox(label="Search Result", interactive=False, visible=False)
428
+
429
+ with gr.Row():
430
+ dest_lat = gr.Number(label='Destination Latitude', precision=6, value=None)
431
+ dest_lon = gr.Number(label='Destination Longitude', precision=6, value=None)
432
+
433
+ gr.Markdown("---")
434
+
435
+ with gr.Row():
436
+ auto_calc_distance = gr.Checkbox(
437
+ value=True,
438
+ label="✓ Auto-calculate distance",
439
+ info="Recommended for accuracy"
440
+ )
441
+ manual_distance = gr.Number(
442
+ value=5000, label='Manual Distance (meters)',
443
+ visible=False
444
+ )
445
+
446
+ cluster_override = gr.Dropdown(
447
+ choices=["Auto"] + [str(i) for i in range(10)],
448
+ value="Auto",
449
+ label="Cluster ID",
450
+ info="Auto-assigns based on location"
451
+ )
452
+
453
+ gr.Markdown("### 🕐 Time Settings")
454
+ with gr.Row():
455
+ use_current = gr.Button("📅 Use Current Date/Time", size="sm")
456
+
457
+ with gr.Row():
458
+ pickup_weekday = gr.Dropdown(
459
+ choices=[
460
+ ("Monday", 0), ("Tuesday", 1), ("Wednesday", 2),
461
+ ("Thursday", 3), ("Friday", 4), ("Saturday", 5), ("Sunday", 6)
462
+ ],
463
+ value=datetime.now().weekday(),
464
+ label='Pickup Day'
465
+ )
466
+ pickup_hour = gr.Dropdown(
467
+ choices=list(range(24)),
468
+ value=datetime.now().hour,
469
+ label='Pickup Hour (24h)'
470
+ )
471
+
472
+ # Right Column - Weather Settings
473
+ with gr.Column(scale=1):
474
+ gr.Markdown("### 🌤️ Weather Conditions")
475
+ gr.Markdown("**⚠️ Manual Input Required** - Adjust sliders based on current weather")
476
+
477
+ dewpoint_temp = gr.Slider(
478
+ minimum=279.129,
479
+ maximum=286.327,
480
+ step=0.1,
481
+ value=282.201,
482
+ label='Dewpoint Temperature (Kelvin)',
483
+ info="Humidity indicator: 279K = 6°C | 286K = 13°C"
484
+ )
485
+
486
+ min_temp = gr.Slider(
487
+ minimum=282.037,
488
+ maximum=292.543,
489
+ step=0.1,
490
+ value=285.203,
491
+ label='Minimum Air Temperature (Kelvin)',
492
+ info="Daily minimum: 282K = 9°C | 293K = 20°C"
493
+ )
494
+
495
+ temp_range = gr.Slider(
496
+ minimum=1.663,
497
+ maximum=10.022,
498
+ step=0.1,
499
+ value=5.0,
500
+ label='Temperature Range (Kelvin)',
501
+ info="Daily variation: Typical = 5-8K"
502
+ )
503
+
504
+ rain = gr.Dropdown(
505
+ choices=[("No Rain ☀️", 0), ("Raining ☔", 1)],
506
+ value=0,
507
+ label='Precipitation Status'
508
+ )
509
+
510
+ gr.Markdown("---")
511
+ gr.Markdown("**Quick Temperature Conversions:**")
512
+ gr.Markdown("""
513
+ - **Dewpoint:** 282K ≈ 9°C ≈ 48°F
514
+ - **Min Temp:** 285K ≈ 12°C ≈ 54°F
515
+ - **To convert:** °C = K - 273.15
516
+ """)
517
+
518
+ # Action Buttons Row
519
+ with gr.Row():
520
+ update_map_btn = gr.Button("🗺️ Update Map", variant="secondary", scale=1)
521
+ predict_btn = gr.Button("🚀 Predict ETA", variant="primary", size="lg", scale=2)
522
+
523
+ # Results Row - Full Width
524
+ with gr.Row():
525
+ with gr.Column():
526
+ gr.Markdown("### 📊 Prediction Results")
527
+ output = gr.Markdown(value="*Enter locations and click 'Predict ETA' to see results*")
528
+
529
+ # Map Row - Full Width
530
+ with gr.Row():
531
+ with gr.Column():
532
+ gr.Markdown("### 🗺️ Route Visualization")
533
+ map_output = gr.HTML(value=create_interactive_map())
534
+
535
+ # Expander for help
536
+ with gr.Accordion("ℹ️ How to Use This App", open=False):
537
+ gr.Markdown("""
538
+ ### 🎯 Three Ways to Set Locations:
539
+
540
+ 1. **Search by Name (Easiest - AUTOMATIC):**
541
+ - Type any place name: "Eiffel Tower", "Tokyo Station", "Central Park"
542
+ - Type addresses: "123 Main St, Boston"
543
+ - Click the search button to get coordinates automatically
544
+ - ✅ Fully automated - no manual input needed!
545
+
546
+ 2. **Use Quick Samples (AUTOMATIC):**
547
+ - Click Nairobi, New York, or London buttons for instant examples
548
+ - ✅ One-click automation
549
+
550
+ 3. **Enter Coordinates Manually (MANUAL):**
551
+ - If you know exact lat/lon, enter them directly
552
+ - Useful for precise locations or GPS coordinates
553
+ - ⚠️ Requires manual entry
554
+
555
+ ### 🔍 Location Search Tips:
556
+ - Be specific: "JFK Airport" is better than just "airport"
557
+ - Include city/country for common names: "Central Park, New York"
558
+ - Landmarks work great: "Statue of Liberty", "Big Ben"
559
+ - The geocoder uses OpenStreetMap (free, no API key needed!)
560
+
561
+ ### 📏 Distance & Clusters (AUTOMATIC):
562
+ - **Distance**: Auto-calculated using Haversine formula (great circle distance)
563
+ - **Clusters**: Automatically assigned based on your origin location
564
+ - Works anywhere in the world - no geographic restrictions!
565
+ - ✅ No manual input required
566
+
567
+ ### 🕐 Time Settings (SEMI-AUTOMATIC):
568
+ - Click "Use Current Date/Time" for instant population ✅
569
+ - Or manually select day and hour if predicting for future trips ⚠️
570
+
571
+ ### 🌤️ Weather Parameters (MANUAL - See Details Below):
572
+
573
+ **⚠️ These require manual input currently:**
574
+
575
+ 1. **Dewpoint Temperature (K):**
576
+ - The temperature at which air becomes saturated and dew forms
577
+ - Indicates humidity level - higher dewpoint = more humid
578
+ - Range: 279-286K (6-13°C) based on training data
579
+ - To convert from Celsius: K = °C + 273.15
580
+ - Example: 10°C = 283.15K
581
+ - **Where to get it:** Weather websites/APIs (OpenWeatherMap, WeatherAPI)
582
+
583
+ 2. **Minimum 2m Air Temperature (K):**
584
+ - The lowest temperature at 2 meters above ground for that day
585
+ - Range: 282-293K (9-20°C) based on training data
586
+ - Usually occurs early morning (5-7 AM)
587
+ - **Where to get it:** Historical weather data or daily forecast minimums
588
+
589
+ 3. **Temperature Range (K):**
590
+ - Daily temperature variation (max temp - min temp)
591
+ - Range: 1.7-10K based on training data
592
+ - Typical values: 5-8K for moderate climates
593
+ - Example: If max=25°C and min=15°C, range = 10K
594
+ - **Where to get it:** Calculate from daily max/min temps
595
+
596
+ 4. **Rain (0 or 1):**
597
+ - Binary indicator: 0 = No rain, 1 = Rain/Precipitation
598
+ - Check current weather or forecast
599
+ - **Where to get it:** Weather apps, look outside, or weather APIs
600
+
601
+ ### 🔧 Future Enhancement - Weather API Integration:
602
+ For fully automated weather data, consider integrating:
603
+ - **OpenWeatherMap API** (free tier: 1000 calls/day)
604
+ - **WeatherAPI** (free tier: 1M calls/month)
605
+ - **Tomorrow.io** (free tier available)
606
+
607
+ These APIs can auto-populate all weather fields based on location and time!
608
+
609
+ ### 📊 Summary - What's Automatic vs Manual:
610
+
611
+ **✅ AUTOMATIC (No manual input needed):**
612
+ - Location coordinates (via search)
613
+ - Trip distance
614
+ - Cluster assignment
615
+ - Current date/time (optional button)
616
+
617
+ **⚠️ MANUAL (Requires user input):**
618
+ - Weather parameters (4 fields)
619
+ - Future date/time (if not using current)
620
+ - Coordinates (if not using search)
621
+
622
+ ### ⚠️ Important Notes:
623
+ - This model was trained on specific data (East Africa region)
624
+ - Predictions for other cities are **extrapolations** - accuracy may vary
625
+ - Weather values should match your location for best results
626
+ - For production use, retrain the model on data from your target area
627
+ - Temperature values are in Kelvin (K) - subtract 273.15 for Celsius
628
+ """)
629
+
630
+ # Event handlers
631
+ auto_calc_distance.change(
632
+ fn=lambda x: gr.update(visible=not x),
633
+ inputs=[auto_calc_distance],
634
+ outputs=[manual_distance]
635
+ )
636
+
637
+ use_current.click(
638
+ fn=use_current_time,
639
+ outputs=[pickup_weekday, pickup_hour]
640
+ )
641
+
642
+ # Sample location buttons
643
+ sample_nairobi.click(
644
+ fn=use_sample_nairobi,
645
+ outputs=[origin_lat, origin_lon, dest_lat, dest_lon, sample_status]
646
+ ).then(
647
+ fn=lambda: gr.update(visible=True),
648
+ outputs=[sample_status]
649
+ )
650
+
651
+ sample_ny.click(
652
+ fn=use_sample_newyork,
653
+ outputs=[origin_lat, origin_lon, dest_lat, dest_lon, sample_status]
654
+ ).then(
655
+ fn=lambda: gr.update(visible=True),
656
+ outputs=[sample_status]
657
+ )
658
+
659
+ sample_london.click(
660
+ fn=use_sample_london,
661
+ outputs=[origin_lat, origin_lon, dest_lat, dest_lon, sample_status]
662
+ ).then(
663
+ fn=lambda: gr.update(visible=True),
664
+ outputs=[sample_status]
665
+ )
666
+
667
+ # Search functionality
668
+ origin_search_btn.click(
669
+ fn=search_origin,
670
+ inputs=[origin_search],
671
+ outputs=[origin_lat, origin_lon, origin_search_status]
672
+ ).then(
673
+ fn=lambda: gr.update(visible=True),
674
+ outputs=[origin_search_status]
675
+ )
676
+
677
+ dest_search_btn.click(
678
+ fn=search_destination,
679
+ inputs=[dest_search],
680
+ outputs=[dest_lat, dest_lon, dest_search_status]
681
+ ).then(
682
+ fn=lambda: gr.update(visible=True),
683
+ outputs=[dest_search_status]
684
+ )
685
+
686
+ predict_btn.click(
687
+ fn=predict,
688
+ inputs=[
689
+ origin_lat, origin_lon, dest_lat, dest_lon,
690
+ dewpoint_temp, min_temp, temp_range, rain,
691
+ pickup_weekday, pickup_hour, manual_distance,
692
+ auto_calc_distance, cluster_override
693
+ ],
694
+ outputs=[output, map_output]
695
+ )
696
+
697
+ update_map_btn.click(
698
+ fn=update_map_only,
699
+ inputs=[origin_lat, origin_lon, dest_lat, dest_lon],
700
+ outputs=[map_output]
701
+ )
702
+
703
+ if __name__ == "__main__":
704
+ app.launch(share=True, debug=True, theme=gr.themes.Soft())