nakas commited on
Commit
aebc78c
·
verified ·
1 Parent(s): 727338e

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +143 -309
app.py CHANGED
@@ -4,7 +4,7 @@ import folium
4
  from datetime import datetime, timedelta
5
  import tempfile
6
  import os
7
- from PIL import Image
8
  import io
9
 
10
  # Montana Mountain Peaks coordinates
@@ -14,252 +14,139 @@ MONTANA_PEAKS = {
14
  "Pioneer Mountain": (45.231835, -111.450505) # 45°13′55″N 111°27′2″W
15
  }
16
 
17
- def download_image(url):
18
- """Download image from URL and return as PIL Image."""
19
- try:
20
- response = requests.get(url, timeout=10)
21
- response.raise_for_status()
22
- image = Image.open(io.BytesIO(response.content))
23
- return image
24
- except Exception as e:
25
- print(f"Error downloading image from {url}: {str(e)}")
26
- return None
27
-
28
  def get_snow_forecast(lat, lon):
29
- """Get snow forecast data from NOAA's snow forecast API."""
30
  try:
31
- # Get the forecast URL for the coordinates
32
  points_url = f"https://api.weather.gov/points/{lat},{lon}"
33
  response = requests.get(points_url, timeout=10)
34
  response.raise_for_status()
35
  forecast_url = response.json()['properties']['forecast']
36
 
37
- # Get the actual forecast with snow data
38
  forecast_response = requests.get(forecast_url, timeout=10)
39
  forecast_response.raise_for_status()
40
  forecast_data = forecast_response.json()
41
 
42
- # Process forecast periods to extract snow data
43
- snow_data = []
44
-
45
  for period in forecast_data['properties']['periods']:
46
- period_data = {
47
- 'time': period['startTime'],
48
- 'temperature': period['temperature'],
49
- 'snowfall': 0 # Default to no snow
50
- }
51
-
52
- # Check if snow is mentioned in the forecast
53
- forecast_text = period['detailedForecast'].lower()
54
-
55
- if any(word in forecast_text for word in ['snow', 'flurries', 'wintry mix']):
56
- import re
57
- # Look for snow amounts in the text
58
- amount_patterns = [
59
- r'(\d+(?:\.\d+)?)\s*(?:to\s*\d+(?:\.\d+)?)?\s*inches? of snow',
60
- r'snow accumulation of (\d+(?:\.\d+)?)\s*(?:to\s*\d+(?:\.\d+)?)?\s*inches',
61
- r'accumulation of (\d+(?:\.\d+)?)\s*(?:to\s*\d+(?:\.\d+)?)?\s*inches',
62
- ]
63
-
64
- for pattern in amount_patterns:
65
- match = re.search(pattern, forecast_text)
66
- if match:
67
- try:
68
- snow_amount = float(match.group(1))
69
- period_data['snowfall'] = snow_amount / 39.37 # Convert inches to meters
70
- break
71
- except (ValueError, IndexError):
72
- continue
73
 
74
- snow_data.append(period_data)
 
 
 
 
 
75
 
76
- return snow_data
77
- except Exception as e:
78
- print(f"Error in snow forecast: {str(e)}")
79
- return []
80
  except Exception as e:
81
- print(f"Error fetching snow forecast: {str(e)}")
82
- return None
83
-
84
- def calculate_snow_density(temp_c, is_new_snow=True):
85
- """Calculate approximate snow density based on temperature.
86
- Returns density in kg/m³."""
87
- if is_new_snow:
88
- if temp_c <= -20:
89
- return 50 # Very cold, light powder
90
- elif temp_c <= -10:
91
- return 70 # Cold powder
92
- elif temp_c <= -5:
93
- return 100 # Moderate temperature powder
94
- elif temp_c <= 0:
95
- return 150 # Near freezing powder
96
- else:
97
- return 200 # Wet snow
98
- else:
99
- # For existing snowpack, density would be higher
100
- return 350 # Average settled snow density
101
 
102
- def get_noaa_forecast(lat, lon):
103
- """Get NOAA forecast data for given coordinates."""
104
- points_url = f"https://api.weather.gov/points/{lat},{lon}"
105
-
106
  try:
107
- # Get the forecast URL for the coordinates
108
- response = requests.get(points_url, timeout=10)
109
- response.raise_for_status()
110
- forecast_url = response.json()['properties']['forecast']
111
-
112
- # Get the actual forecast
113
- forecast_response = requests.get(forecast_url, timeout=10)
114
- forecast_response.raise_for_status()
115
- forecast_data = forecast_response.json()
116
-
117
- # Get snow forecast data
118
- snow_data = get_snow_forecast(lat, lon)
119
-
120
- # Format the forecast text
121
- periods = forecast_data['properties']['periods']
122
- forecast_text = ""
123
 
124
- for i, period in enumerate(periods[:6]): # Show next 3 days (day and night)
125
- forecast_text += f"\n{period['name']}:\n"
126
- forecast_text += f"Temperature: {period['temperature']}°{period['temperatureUnit']}\n"
127
- forecast_text += f"Wind: {period['windSpeed']} {period['windDirection']}\n"
 
 
128
 
129
- # Convert temperature to Celsius for snow density calculation
130
- temp_c = (period['temperature'] - 32) * 5/9 if period['temperatureUnit'] == 'F' else period['temperature']
 
 
131
 
132
- # Add snow-specific forecasts if available
133
- if i < len(snow_data) and snow_data[i]['snowfall'] > 0:
134
- snow_density = calculate_snow_density(temp_c)
135
- snow_amount = snow_data[i]['snowfall'] # in meters
136
- swe = snow_amount * (snow_density / 1000) # Convert density to g/cm³
137
-
138
- # Convert to inches for display
139
- snow_inches = snow_amount * 39.37
140
- swe_inches = swe * 39.37
141
-
142
- forecast_text += f"Predicted Snowfall: {snow_inches:.1f} inches\n"
143
- forecast_text += f"Snow Water Equivalent: {swe_inches:.2f} inches\n"
144
- forecast_text += f"Estimated Snow Density: {snow_density} kg/m³ "
145
- forecast_text += f"({get_snow_quality_description(snow_density)})\n"
146
 
147
- forecast_text += f"{period['detailedForecast']}\n"
148
-
149
- return forecast_text.strip()
150
- except requests.exceptions.RequestException as e:
151
- return f"Error fetching forecast: {str(e)}"
152
-
153
- def get_snow_quality_description(density):
154
- """Return a description of snow quality based on density."""
155
- if density <= 50:
156
- return "Ultra-light powder"
157
- elif density <= 70:
158
- return "Light powder"
159
- elif density <= 100:
160
- return "Moderate powder"
161
- elif density <= 150:
162
- return "Packed powder"
163
- elif density <= 200:
164
- return "Wet snow"
165
- else:
166
- return "Dense/settled snow"
167
-
168
- import gradio as gr
169
- import requests
170
- import folium
171
- from datetime import datetime, timedelta
172
- import tempfile
173
- import os
174
- from PIL import Image, ImageDraw, ImageFont
175
- import io
176
-
177
- # Montana Mountain Peaks coordinates
178
- MONTANA_PEAKS = {
179
- "Lone Peak (Big Sky)": (45.27806, -111.45028), # 45°16′41″N 111°27′01″W
180
- "Sacajawea Peak": (45.89583, -110.96861), # 45°53′45″N 110°58′7″W
181
- "Pioneer Mountain": (45.231835, -111.450505) # 45°13′55″N 111°27′2″W
182
- }
183
-
184
- def get_radar_and_snow_forecasts():
185
- """Get current radar and forecast loop."""
186
- try:
187
- # For the Montana region, get:
188
- # 1. Current base reflectivity
189
- # 2. Short-term forecast (0-6 hours)
190
- # 3. Medium-term forecast (6-12 hours)
191
- base_url = "https://radar.weather.gov/ridge/Overlays/Foregrounds"
192
- overlay_url = "https://radar.weather.gov/ridge/Overlays"
193
- background_url = "https://radar.weather.gov/ridge/Overlays/Backgrounds"
194
-
195
- # Build image frames
196
- frames = []
197
 
198
- # Current conditions plus 6 forecast periods (7 total frames)
199
- for hour in range(7):
200
  try:
201
- # Start with the static background
202
- background = Image.open(requests.get(f"{background_url}/Topo/Short/KMSX_Topo_Short.jpg", timeout=10).content)
203
- background = background.convert('RGB')
204
-
205
- # Add county overlays
206
- counties = Image.open(requests.get(f"{overlay_url}/Counties/Short/KMSX_Counties_Short.gif", timeout=10).content)
207
- counties = counties.convert('RGBA')
208
- background.paste(counties, (0, 0), counties)
209
-
210
- # Add highways
211
- highways = Image.open(requests.get(f"{overlay_url}/Highways/Short/KMSX_Highways_Short.gif", timeout=10).content)
212
- highways = highways.convert('RGBA')
213
- background.paste(highways, (0, 0), highways)
214
-
215
- # Add current radar or forecast overlay
216
- if hour == 0:
217
- # Current radar
218
- radar = Image.open(requests.get(f"{base_url}/Base/KMSX_Base_0.gif", timeout=10).content)
219
- else:
220
- # Forecast radar (using NDFD data)
221
- forecast_time = datetime.now() + timedelta(hours=hour)
222
- forecast_str = forecast_time.strftime("%Y%m%d_%H")
223
- radar = Image.open(requests.get(f"https://forecast.weather.gov/wrf_images/KMSX/Reflectivity/Reflectivity_{forecast_str}.gif", timeout=10).content)
224
-
225
- radar = radar.convert('RGBA')
226
- background.paste(radar, (0, 0), radar)
227
-
228
- # Add timestamp
229
- draw = ImageDraw.Draw(background)
230
- if hour == 0:
231
- time_text = "Current Conditions"
232
- else:
233
- forecast_time = datetime.now() + timedelta(hours=hour)
234
- time_text = f"Forecast +{hour}h ({forecast_time.strftime('%I%p')})"
235
-
236
- try:
237
- font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", 20)
238
- except:
239
- font = ImageFont.load_default()
240
-
241
- # Draw text with outline
242
- x, y = 10, 10
243
- for dx, dy in [(-1,-1), (-1,1), (1,-1), (1,1)]:
244
- draw.text((x+dx, y+dy), time_text, fill='black', font=font)
245
- draw.text((x, y), time_text, fill='white', font=font)
246
-
247
- frames.append(background)
248
-
 
 
 
 
 
 
249
  except Exception as e:
250
- print(f"Error processing frame {hour}: {str(e)}")
251
  continue
252
 
253
  if not frames:
254
- raise Exception("No frames could be generated")
255
-
256
- # Save as animated GIF
257
  with tempfile.NamedTemporaryFile(suffix='.gif', delete=False) as tmp_file:
258
  frames[0].save(
259
  tmp_file.name,
260
  save_all=True,
261
  append_images=frames[1:],
262
- duration=1500, # 1.5 seconds per frame
263
  loop=0
264
  )
265
  return tmp_file.name
@@ -270,34 +157,26 @@ def get_radar_and_snow_forecasts():
270
  img = Image.new('RGB', (800, 600), color='black')
271
  draw = ImageDraw.Draw(img)
272
  message = (
273
- "Weather radar and forecast temporarily unavailable\n"
274
  f"{str(e)}\n"
275
  "Please check weather.gov for current conditions\n"
276
  f"Last attempt: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
277
  )
278
  try:
279
  font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", 20)
 
280
  except:
281
- font = ImageFont.load_default()
282
-
283
- try:
284
- draw.text((400, 300), message,
285
- fill='white', anchor="mm", align="center", font=font)
286
- except:
287
- # Fallback if font fails
288
- draw.text((400, 300), message,
289
- fill='white', anchor="mm", align="center")
290
 
291
  with tempfile.NamedTemporaryFile(suffix='.gif', delete=False) as tmp_file:
292
  img.save(tmp_file.name, format='GIF')
293
  return tmp_file.name
294
 
295
- def create_map():
296
- """Create a folium map centered on Montana with optional markers."""
297
- # Center on Montana
298
- m = folium.Map(location=[45.5, -111.0], zoom_start=7)
299
 
300
- # Add markers for Montana peaks
301
  for peak_name, coords in MONTANA_PEAKS.items():
302
  folium.Marker(
303
  coords,
@@ -305,9 +184,20 @@ def create_map():
305
  tooltip=peak_name
306
  ).add_to(m)
307
 
 
 
 
 
308
  m.add_child(folium.ClickForLatLng()) # Enable click events
309
  return m._repr_html_()
310
 
 
 
 
 
 
 
 
311
  def update_weather(lat, lon):
312
  """Update weather information based on coordinates."""
313
  try:
@@ -315,53 +205,21 @@ def update_weather(lat, lon):
315
  lat = float(lat)
316
  lon = float(lon)
317
  if not (-90 <= lat <= 90 and -180 <= lon <= 180):
318
- return "Invalid coordinates. Latitude must be between -90 and 90, longitude between -180 and 180.", None, get_map(lat, lon)
319
-
320
- # Get forecast
321
- forecast = get_noaa_forecast(lat, lon)
322
 
323
- # Get animated snow forecast
324
- animated_forecast = get_radar_and_snow_forecasts()
325
 
326
- # Create updated map
327
  map_html = get_map(lat, lon)
328
 
329
- return forecast, animated_forecast, map_html
330
- except ValueError as e:
331
- return f"Error: Invalid coordinate format - {str(e)}", None, get_map(lat, lon)
332
- except Exception as e:
333
- return f"Error: {str(e)}", None, get_map(lat, lon)
334
-
335
- def get_map(lat, lon):
336
- """Create a map centered on the given coordinates with markers."""
337
- try:
338
- # Create map with appropriate zoom level
339
- m = folium.Map(location=[lat, lon], zoom_start=9)
340
-
341
- # Add all Montana peaks
342
- for peak_name, coords in MONTANA_PEAKS.items():
343
- folium.Marker(
344
- coords,
345
- popup=f"{peak_name}<br>Lat: {coords[0]:.4f}, Lon: {coords[1]:.4f}",
346
- tooltip=peak_name
347
- ).add_to(m)
348
-
349
- # Add current location marker if not a peak
350
- if (lat, lon) not in MONTANA_PEAKS.values():
351
- folium.Marker([lat, lon], popup=f"Selected Location<br>Lat: {lat:.4f}, Lon: {lon:.4f}").add_to(m)
352
 
353
- m.add_child(folium.ClickForLatLng()) # Enable click events
354
- return m._repr_html_()
355
  except Exception as e:
356
- print(f"Error creating map: {str(e)}")
357
- return None
358
-
359
- def make_peak_click_handler(peak_name):
360
- """Creates a click handler for a specific peak."""
361
- def handler():
362
- coords = MONTANA_PEAKS[peak_name]
363
- return coords[0], coords[1]
364
- return handler
365
 
366
  # Create Gradio interface
367
  with gr.Blocks(title="Montana Mountain Weather") as demo:
@@ -394,17 +252,15 @@ with gr.Blocks(title="Montana Mountain Weather") as demo:
394
  map_display = gr.HTML(get_map(45.5, -111.0))
395
 
396
  with gr.Row():
397
- with gr.Column(scale=1):
398
  forecast_output = gr.Textbox(
399
  label="Forecast",
400
  lines=12,
401
- placeholder="Select a location or mountain peak to see the forecast..."
402
  )
403
- with gr.Column(scale=1):
404
- snow_forecast = gr.Image(
405
- label="12-Hour Snow Forecast Animation",
406
  show_label=True,
407
- container=True,
408
  type="filepath"
409
  )
410
 
@@ -414,7 +270,7 @@ with gr.Blocks(title="Montana Mountain Weather") as demo:
414
  inputs=[lat_input, lon_input],
415
  outputs=[
416
  forecast_output,
417
- snow_forecast,
418
  map_display
419
  ]
420
  )
@@ -430,7 +286,7 @@ with gr.Blocks(title="Montana Mountain Weather") as demo:
430
  inputs=[lat_input, lon_input],
431
  outputs=[
432
  forecast_output,
433
- snow_forecast,
434
  map_display
435
  ]
436
  )
@@ -439,46 +295,24 @@ with gr.Blocks(title="Montana Mountain Weather") as demo:
439
  ## Instructions
440
  1. Use the quick access buttons to check specific Montana peaks
441
  2. Or enter coordinates manually / click on the map
442
- 3. Click "Get Weather" to see the forecast and radar imagery
443
 
444
  **Montana Peaks Included:**
445
  - Lone Peak (Big Sky): 45°16′41″N 111°27′01″W
446
  - Sacajawea Peak: 45°53′45″N 110°58′7″W
447
  - Pioneer Mountain: 45°13′55″N 111°27′2″W
448
 
449
- **Weather Display Information:**
450
- The display rotates through three views:
451
- 1. Local Radar (KMSX - Missoula)
452
- - Coverage: Western Montana mountain regions
453
- - Updates: Every ~10 minutes
454
- 2. National Radar Loop
455
- - Shows broader weather patterns
456
- - Helps track incoming systems
457
- 3. Short Term Forecast
458
- - Predicted conditions
459
- - Weather warnings and advisories
460
-
461
- **Radar Color Legend:**
462
- - Light green: Light precipitation
463
- - Dark green/Yellow: Moderate precipitation
464
- - Red: Heavy precipitation
465
- - Pink/White: Possible snow/mixed precipitation
466
- - Blue: Light snow
467
- - Dark blue: Heavy snow
468
 
469
- **Snow Quality Guide:**
470
- - Ultra-light powder: ≤ 50 kg/m³
471
- - Light powder: 51-70 kg/m³
472
- - Moderate powder: 71-100 kg/m³
473
- - Packed powder: 101-150 kg/m³
474
- - Wet snow: 151-200 kg/m³
475
- - Dense/settled snow: > 200 kg/m³
476
 
477
  **Note**: This app uses the NOAA Weather API and may have occasional delays or service interruptions.
478
  Mountain weather can change rapidly - always check multiple sources for safety.
479
- Snow density and quality estimates are approximate and based on temperature only.
480
- Local conditions, wind, and other factors can significantly affect actual snow conditions.
481
  """)
482
 
483
- # For Hugging Face Spaces, we need to export the demo
484
  demo.queue().launch()
 
4
  from datetime import datetime, timedelta
5
  import tempfile
6
  import os
7
+ from PIL import Image, ImageDraw, ImageFont
8
  import io
9
 
10
  # Montana Mountain Peaks coordinates
 
14
  "Pioneer Mountain": (45.231835, -111.450505) # 45°13′55″N 111°27′2″W
15
  }
16
 
 
 
 
 
 
 
 
 
 
 
 
17
  def get_snow_forecast(lat, lon):
18
+ """Get snow forecast data from NOAA."""
19
  try:
 
20
  points_url = f"https://api.weather.gov/points/{lat},{lon}"
21
  response = requests.get(points_url, timeout=10)
22
  response.raise_for_status()
23
  forecast_url = response.json()['properties']['forecast']
24
 
 
25
  forecast_response = requests.get(forecast_url, timeout=10)
26
  forecast_response.raise_for_status()
27
  forecast_data = forecast_response.json()
28
 
29
+ # Format text forecast
30
+ forecast_text = "Weather Forecast:\n\n"
 
31
  for period in forecast_data['properties']['periods']:
32
+ forecast_text += f"{period['name']}:\n"
33
+ forecast_text += f"Temperature: {period['temperature']}°{period['temperatureUnit']}\n"
34
+ forecast_text += f"Wind: {period['windSpeed']} {period['windDirection']}\n"
35
+ forecast_text += f"{period['detailedForecast']}\n\n"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
36
 
37
+ # Check for snow-related keywords
38
+ if any(word in period['detailedForecast'].lower() for word in
39
+ ['snow', 'flurries', 'wintry mix', 'blizzard', 'winter storm']):
40
+ forecast_text += "⚠️ SNOW EVENT PREDICTED ⚠️\n\n"
41
+
42
+ return forecast_text
43
 
 
 
 
 
44
  except Exception as e:
45
+ return f"Error getting forecast: {str(e)}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
46
 
47
+ def get_forecast_images():
48
+ """Get forecast images and combine them into an animation."""
 
 
49
  try:
50
+ frames = []
51
+ current_time = datetime.utcnow()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
52
 
53
+ # List of forecast product URLs
54
+ forecast_urls = [
55
+ # National Forecast Products
56
+ f"https://graphical.weather.gov/images/conus/MaxT1_conus.png",
57
+ f"https://graphical.weather.gov/images/conus/MinT1_conus.png",
58
+ f"https://graphical.weather.gov/images/conus/Wx1_conus.png",
59
 
60
+ # Winter Weather Probability
61
+ "https://www.wpc.ncep.noaa.gov/pwpf/24hr_pwpf_fill.gif",
62
+ "https://www.wpc.ncep.noaa.gov/pwpf/48hr_pwpf_fill.gif",
63
+ "https://www.wpc.ncep.noaa.gov/pwpf/72hr_pwpf_fill.gif",
64
 
65
+ # Winter Weather Maps
66
+ "https://www.wpc.ncep.noaa.gov/wwd/24wp_d1_psnow.gif",
67
+ "https://www.wpc.ncep.noaa.gov/wwd/48wp_d2_psnow.gif",
68
+ "https://www.wpc.ncep.noaa.gov/wwd/72wp_d3_psnow.gif",
 
 
 
 
 
 
 
 
 
 
69
 
70
+ # Precipitation Probability
71
+ f"https://graphical.weather.gov/images/conus/PoP1_conus.png",
72
+ f"https://graphical.weather.gov/images/conus/PoP2_conus.png",
73
+
74
+ # Snow Amount Forecasts
75
+ f"https://graphical.weather.gov/images/conus/Snow1_conus.png",
76
+ f"https://graphical.weather.gov/images/conus/Snow2_conus.png"
77
+ ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
78
 
79
+ # Try to get each forecast image
80
+ for i, url in enumerate(forecast_urls):
81
  try:
82
+ response = requests.get(url, timeout=10)
83
+ if response.status_code == 200:
84
+ img = Image.open(io.BytesIO(response.content))
85
+
86
+ # Convert to RGB if needed
87
+ if img.mode != 'RGB':
88
+ img = img.convert('RGB')
89
+
90
+ # Resize to consistent dimensions
91
+ img = img.resize((800, 600), Image.Resampling.LANCZOS)
92
+
93
+ # Add descriptive text
94
+ draw = ImageDraw.Draw(img)
95
+ try:
96
+ font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", 20)
97
+ except:
98
+ font = ImageFont.load_default()
99
+
100
+ # Determine image type
101
+ if "MaxT" in url:
102
+ text = "Maximum Temperature Forecast"
103
+ elif "MinT" in url:
104
+ text = "Minimum Temperature Forecast"
105
+ elif "Wx" in url:
106
+ text = "Weather Type Forecast"
107
+ elif "pwpf" in url:
108
+ hours = url.split('/')[-1].split('hr')[0]
109
+ text = f"{hours}-hour Winter Precipitation Forecast"
110
+ elif "psnow" in url:
111
+ if "24wp" in url:
112
+ text = "24-hour Snowfall Probability"
113
+ elif "48wp" in url:
114
+ text = "48-hour Snowfall Probability"
115
+ else:
116
+ text = "72-hour Snowfall Probability"
117
+ elif "PoP" in url:
118
+ text = "Precipitation Probability Forecast"
119
+ elif "Snow" in url:
120
+ text = "Snowfall Amount Forecast"
121
+ else:
122
+ text = f"Weather Forecast Product {i+1}"
123
+
124
+ timestamp = f"Valid: {current_time.strftime('%Y-%m-%d %H:%M UTC')}"
125
+
126
+ # Draw text with outline for visibility
127
+ x, y = 10, 10
128
+ for dx, dy in [(-1,-1), (-1,1), (1,-1), (1,1)]:
129
+ draw.text((x+dx, y+dy), text, fill='black', font=font)
130
+ draw.text((x+dx, y+dy+25), timestamp, fill='black', font=font)
131
+ draw.text((x, y), text, fill='white', font=font)
132
+ draw.text((x, y+25), timestamp, fill='white', font=font)
133
+
134
+ frames.append(img)
135
+ print(f"Successfully added frame from {url}")
136
  except Exception as e:
137
+ print(f"Error processing forecast image from {url}: {str(e)}")
138
  continue
139
 
140
  if not frames:
141
+ raise Exception("No forecast images could be loaded")
142
+
143
+ # Create animated GIF
144
  with tempfile.NamedTemporaryFile(suffix='.gif', delete=False) as tmp_file:
145
  frames[0].save(
146
  tmp_file.name,
147
  save_all=True,
148
  append_images=frames[1:],
149
+ duration=3000, # 3 seconds per frame
150
  loop=0
151
  )
152
  return tmp_file.name
 
157
  img = Image.new('RGB', (800, 600), color='black')
158
  draw = ImageDraw.Draw(img)
159
  message = (
160
+ "Weather forecast images temporarily unavailable\n"
161
  f"{str(e)}\n"
162
  "Please check weather.gov for current conditions\n"
163
  f"Last attempt: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
164
  )
165
  try:
166
  font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", 20)
167
+ draw.text((400, 300), message, fill='white', anchor="mm", align="center", font=font)
168
  except:
169
+ draw.text((400, 300), message, fill='white', anchor="mm", align="center")
 
 
 
 
 
 
 
 
170
 
171
  with tempfile.NamedTemporaryFile(suffix='.gif', delete=False) as tmp_file:
172
  img.save(tmp_file.name, format='GIF')
173
  return tmp_file.name
174
 
175
+ def get_map(lat, lon):
176
+ """Create a map centered on the given coordinates with markers."""
177
+ m = folium.Map(location=[lat, lon], zoom_start=9)
 
178
 
179
+ # Add all Montana peaks
180
  for peak_name, coords in MONTANA_PEAKS.items():
181
  folium.Marker(
182
  coords,
 
184
  tooltip=peak_name
185
  ).add_to(m)
186
 
187
+ # Add current location marker if not a peak
188
+ if (lat, lon) not in MONTANA_PEAKS.values():
189
+ folium.Marker([lat, lon], popup=f"Selected Location<br>Lat: {lat:.4f}, Lon: {lon:.4f}").add_to(m)
190
+
191
  m.add_child(folium.ClickForLatLng()) # Enable click events
192
  return m._repr_html_()
193
 
194
+ def make_peak_click_handler(peak_name):
195
+ """Creates a click handler for a specific peak."""
196
+ def handler():
197
+ coords = MONTANA_PEAKS[peak_name]
198
+ return coords[0], coords[1]
199
+ return handler
200
+
201
  def update_weather(lat, lon):
202
  """Update weather information based on coordinates."""
203
  try:
 
205
  lat = float(lat)
206
  lon = float(lon)
207
  if not (-90 <= lat <= 90 and -180 <= lon <= 180):
208
+ return "Invalid coordinates", None, get_map(45.5, -111.0)
209
+
210
+ # Get text forecast
211
+ forecast_text = get_snow_forecast(lat, lon)
212
 
213
+ # Get forecast images animation
214
+ forecast_animation = get_forecast_images()
215
 
216
+ # Get map
217
  map_html = get_map(lat, lon)
218
 
219
+ return forecast_text, forecast_animation, map_html
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
220
 
 
 
221
  except Exception as e:
222
+ return f"Error: {str(e)}", None, get_map(45.5, -111.0)
 
 
 
 
 
 
 
 
223
 
224
  # Create Gradio interface
225
  with gr.Blocks(title="Montana Mountain Weather") as demo:
 
252
  map_display = gr.HTML(get_map(45.5, -111.0))
253
 
254
  with gr.Row():
255
+ with gr.Column():
256
  forecast_output = gr.Textbox(
257
  label="Forecast",
258
  lines=12,
259
+ placeholder="Select a location to see the forecast..."
260
  )
261
+ forecast_display = gr.Image(
262
+ label="Weather Forecast Products",
 
263
  show_label=True,
 
264
  type="filepath"
265
  )
266
 
 
270
  inputs=[lat_input, lon_input],
271
  outputs=[
272
  forecast_output,
273
+ forecast_display,
274
  map_display
275
  ]
276
  )
 
286
  inputs=[lat_input, lon_input],
287
  outputs=[
288
  forecast_output,
289
+ forecast_display,
290
  map_display
291
  ]
292
  )
 
295
  ## Instructions
296
  1. Use the quick access buttons to check specific Montana peaks
297
  2. Or enter coordinates manually / click on the map
298
+ 3. Click "Get Weather" to see the forecast and weather products
299
 
300
  **Montana Peaks Included:**
301
  - Lone Peak (Big Sky): 45°16′41″N 111°27′01″W
302
  - Sacajawea Peak: 45°53′45″N 110°58′7″W
303
  - Pioneer Mountain: 45°13′55″N 111°27′2″W
304
 
305
+ **Forecast Products Include:**
306
+ - Temperature forecasts (max/min)
307
+ - Precipitation probability
308
+ - Snow amount predictions
309
+ - Winter weather probability
310
+ - 24/48/72-hour snowfall forecasts
 
 
 
 
 
 
 
 
 
 
 
 
 
311
 
312
+ Each forecast image will be shown for 3 seconds in the animation.
 
 
 
 
 
 
313
 
314
  **Note**: This app uses the NOAA Weather API and may have occasional delays or service interruptions.
315
  Mountain weather can change rapidly - always check multiple sources for safety.
 
 
316
  """)
317
 
 
318
  demo.queue().launch()