nakas commited on
Commit
58c44f9
·
verified ·
1 Parent(s): 6319e3a

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +213 -249
app.py CHANGED
@@ -9,236 +9,207 @@ import io
9
 
10
  # Montana Mountain Peaks coordinates
11
  MONTANA_PEAKS = {
12
- "Lone Peak (Big Sky)": (45.27806, -111.45028), # 45°16′41″N 111°27′01″W
13
- "Sacajawea Peak": (45.89583, -110.96861), # 45°53′45″N 110°58′7″W
14
- "Pioneer Mountain": (45.231835, -111.450505) # 45°13′55″N 111°27′2″W
15
  }
16
 
17
- def get_satellite_animation(lat, lon, hours_back=6):
18
- """Get GOES satellite data and create animation."""
19
- try:
20
- frames = []
21
- frame_files = []
22
- current_time = datetime.utcnow()
23
-
24
- # GOES-West (covers Montana)
25
- base_url = "https://cdn.star.nesdis.noaa.gov/GOES18"
26
-
27
- # Get multiple products
28
- products = [
29
- {
30
- "path": "/ABI/SECTOR/np/BAND02/", # Visible
31
- "title": "Visible Cloud Cover"
32
- },
33
- {
34
- "path": "/ABI/SECTOR/np/BAND13/", # IR Clean
35
- "title": "Infrared Clean"
36
- },
37
- {
38
- "path": "/ABI/SECTOR/np/GEOCOLOR/", # True Color
39
- "title": "True Color"
40
- },
41
- {
42
- "path": "/ABI/SECTOR/np/BAND08/", # Water Vapor
43
- "title": "Water Vapor"
44
- },
45
- {
46
- "path": "/ABI/SECTOR/np/BAND09/", # Mid-Level Water Vapor
47
- "title": "Mid-Level Water Vapor"
48
- },
49
- {
50
- "path": "/ABI/SECTOR/np/BAND10/", # Low-Level Water Vapor
51
- "title": "Low-Level Water Vapor"
52
- }
53
- ]
54
-
55
- for product in products:
56
- product_frames = []
57
-
58
- # Get last N hours of data
59
- for hour in range(hours_back, -1, -1):
60
- time = current_time - timedelta(hours=hour)
61
-
62
- # Format time for GOES URL (updates every 10 minutes)
63
- for minute in range(0, 60, 10):
64
- try:
65
- timestamp = time.replace(minute=minute).strftime("%Y%j%H%M")
66
- url = f"{base_url}{product['path']}{timestamp}_GOES18-ABI-np-{product['path'].split('/')[-2]}-1000x1000.jpg"
67
-
68
- response = requests.get(url, timeout=5)
69
- if response.status_code == 200:
70
- img = Image.open(io.BytesIO(response.content))
71
-
72
- if img.mode != 'RGB':
73
- img = img.convert('RGB')
74
-
75
- # Crop to region
76
- img = crop_to_region(img, lat, lon, zoom=2.0) # Higher zoom for satellite
77
-
78
- # Add timestamp
79
- draw = ImageDraw.Draw(img)
80
- try:
81
- font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", 20)
82
- except:
83
- font = ImageFont.load_default()
84
-
85
- text = f"{product['title']}\n{time.strftime('%Y-%m-%d %H:%M UTC')}"
86
-
87
- # Draw text with outline
88
- x, y = 10, 10
89
- for dx, dy in [(-1,-1), (-1,1), (1,-1), (1,1)]:
90
- draw.text((x+dx, y+dy), text, fill='black', font=font)
91
- draw.text((x, y), text, fill='white', font=font)
92
-
93
- product_frames.append(img)
94
-
95
- # Save individual frame
96
- with tempfile.NamedTemporaryFile(delete=False, suffix='.png') as tmp_file:
97
- img.save(tmp_file.name, format='PNG')
98
- frame_files.append(tmp_file.name)
99
-
100
- except Exception as e:
101
- print(f"Error getting frame for {timestamp}: {str(e)}")
102
- continue
103
-
104
- if product_frames:
105
- # Create animated GIF for this product
106
- with tempfile.NamedTemporaryFile(delete=False, suffix='.gif') as tmp_file:
107
- product_frames[0].save(
108
- tmp_file.name,
109
- save_all=True,
110
- append_images=product_frames[1:],
111
- duration=500, # Half second per frame
112
- loop=0
113
- )
114
- frames.append(tmp_file.name)
115
- print(f"Successfully created animation for {product['title']}")
116
-
117
- return frames, frame_files
118
- except Exception as e:
119
- print(f"Error creating satellite animation: {str(e)}")
120
- return [], []
121
-
122
- def get_forecast_products(lat, lon):
123
- """Get various forecast products."""
124
- try:
125
- gallery_data = []
126
- timestamp = datetime.utcnow().strftime('%Y-%m-%d %H:%M UTC')
127
 
128
- # List of forecast products
 
129
  products = [
130
- # Temperature Forecasts
131
- {
132
- "url": "https://graphical.weather.gov/images/conus/MaxT1_conus.png",
133
- "title": "Maximum Temperature"
134
- },
135
- {
136
- "url": "https://graphical.weather.gov/images/conus/MinT1_conus.png",
137
- "title": "Minimum Temperature"
138
- },
139
- {
140
- "url": "https://graphical.weather.gov/images/conus/MaxT2_conus.png",
141
- "title": "Maximum Temperature (Day 2)"
142
- },
143
- {
144
- "url": "https://graphical.weather.gov/images/conus/MinT2_conus.png",
145
- "title": "Minimum Temperature (Day 2)"
146
- },
147
-
148
- # Precipitation Forecasts
149
- {
150
- "url": "https://graphical.weather.gov/images/conus/QPF06_conus.png",
151
- "title": "6-Hour Precipitation"
152
- },
153
- {
154
- "url": "https://graphical.weather.gov/images/conus/QPF12_conus.png",
155
- "title": "12-Hour Precipitation"
156
- },
157
- {
158
- "url": "https://graphical.weather.gov/images/conus/QPF24_conus.png",
159
- "title": "24-Hour Precipitation"
160
- },
161
- {
162
- "url": "https://graphical.weather.gov/images/conus/QPF48_conus.png",
163
- "title": "48-Hour Precipitation"
164
- },
165
-
166
- # Snow Forecasts
167
- {
168
- "url": "https://graphical.weather.gov/images/conus/Snow1_conus.png",
169
- "title": "Snowfall Amount"
170
- },
171
- {
172
- "url": "https://www.wpc.ncep.noaa.gov/pwpf/24hr_pwpf_fill.gif",
173
- "title": "24hr Snow Probability"
174
- },
175
- {
176
- "url": "https://www.wpc.ncep.noaa.gov/pwpf/48hr_pwpf_fill.gif",
177
- "title": "48hr Snow Probability"
178
- },
179
- {
180
- "url": "https://www.wpc.ncep.noaa.gov/pwpf/72hr_pwpf_fill.gif",
181
- "title": "72hr Snow Probability"
182
- },
183
-
184
- # Additional Forecasts
185
- {
186
- "url": "https://graphical.weather.gov/images/conus/Wx1_conus.png",
187
- "title": "Weather Type"
188
- },
189
- {
190
- "url": "https://graphical.weather.gov/images/conus/Wx2_conus.png",
191
- "title": "Weather Type (Day 2)"
192
- },
193
- {
194
- "url": "https://www.wpc.ncep.noaa.gov/noaa/noaa.gif",
195
- "title": "Surface Analysis"
196
- }
197
  ]
198
 
199
- for product in products:
200
  try:
201
- response = requests.get(product["url"], timeout=10)
202
  if response.status_code == 200:
203
- img = Image.open(io.BytesIO(response.content))
204
-
205
- if img.mode != 'RGB':
206
- img = img.convert('RGB')
207
-
208
- # Crop to region
209
  img = crop_to_region(img, lat, lon)
210
 
211
- # Add title and timestamp
212
  draw = ImageDraw.Draw(img)
213
- try:
214
- font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", 20)
215
- except:
216
- font = ImageFont.load_default()
217
 
218
- text = f"{product['title']}\n{timestamp}"
219
-
220
- # Draw text with outline
221
- x, y = 10, 10
222
- for dx, dy in [(-1,-1), (-1,1), (1,-1), (1,1)]:
223
- draw.text((x+dx, y+dy), text, fill='black', font=font)
224
- draw.text((x, y), text, fill='white', font=font)
225
-
226
- # Save processed image
227
- with tempfile.NamedTemporaryFile(delete=False, suffix='.png') as tmp_file:
228
- img.save(tmp_file.name, format='PNG')
229
- gallery_data.append(tmp_file.name)
230
- print(f"Successfully saved {product['title']}")
231
  except Exception as e:
232
- print(f"Error processing {product['title']}: {str(e)}")
233
  continue
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
234
 
235
- return gallery_data
 
236
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
237
  except Exception as e:
238
- print(f"Error getting forecast products: {str(e)}")
239
- return []
240
 
241
- [Previous crop_to_region, get_snow_forecast, get_map, and make_peak_click_handler functions remain the same]
 
 
 
 
242
 
243
  def update_weather(lat, lon):
244
  """Update weather information based on coordinates."""
@@ -247,31 +218,31 @@ def update_weather(lat, lon):
247
  lat = float(lat)
248
  lon = float(lon)
249
  if not (-90 <= lat <= 90 and -180 <= lon <= 180):
250
- return "Invalid coordinates", [], [], [], get_map(45.5, -111.0)
251
 
252
  # Get text forecast
253
- forecast_text = get_snow_forecast(lat, lon)
254
 
255
- # Get satellite animations and frames
256
- satellite_animations, satellite_frames = get_satellite_animation(lat, lon)
257
 
258
  # Get forecast products
259
- forecast_products = get_forecast_products(lat, lon)
260
 
261
  # Combine all images for gallery
262
- gallery_data = satellite_frames + forecast_products
263
 
264
  # Get map
265
  map_html = get_map(lat, lon)
266
 
267
- return forecast_text, gallery_data, satellite_animations, [], map_html
268
 
269
  except Exception as e:
270
- return f"Error: {str(e)}", [], [], [], get_map(45.5, -111.0)
271
 
272
  # Create Gradio interface
273
- with gr.Blocks(title="Montana Mountain Weather - Satellite & Forecast") as demo:
274
- gr.Markdown("# Montana Mountain Weather - Satellite & Forecast")
275
 
276
  with gr.Row():
277
  with gr.Column(scale=1):
@@ -288,10 +259,9 @@ with gr.Blocks(title="Montana Mountain Weather - Satellite & Forecast") as demo:
288
  maximum=180
289
  )
290
 
291
- # Quick access buttons for Montana peaks
292
  gr.Markdown("### Quick Access - Montana Peaks")
293
  peak_buttons = []
294
- for peak_name in MONTANA_PEAKS.keys():
295
  peak_buttons.append(gr.Button(f"📍 {peak_name}"))
296
 
297
  submit_btn = gr.Button("Get Weather", variant="primary")
@@ -307,20 +277,18 @@ with gr.Blocks(title="Montana Mountain Weather - Satellite & Forecast") as demo:
307
  placeholder="Select a location to see the forecast..."
308
  )
309
  with gr.Column(scale=2):
310
- satellite_gallery = gr.Gallery(
311
- label="Satellite Animations",
312
  show_label=True,
313
- columns=3,
314
- height=300,
315
- object_fit="contain"
316
  )
317
 
318
  with gr.Row():
319
  forecast_gallery = gr.Gallery(
320
- label="Forecast Products",
321
  show_label=True,
322
  columns=4,
323
- height=800,
324
  object_fit="contain"
325
  )
326
 
@@ -331,8 +299,7 @@ with gr.Blocks(title="Montana Mountain Weather - Satellite & Forecast") as demo:
331
  outputs=[
332
  forecast_output,
333
  forecast_gallery,
334
- satellite_gallery,
335
- satellite_frames,
336
  map_display
337
  ]
338
  )
@@ -349,8 +316,7 @@ with gr.Blocks(title="Montana Mountain Weather - Satellite & Forecast") as demo:
349
  outputs=[
350
  forecast_output,
351
  forecast_gallery,
352
- satellite_gallery,
353
- satellite_frames,
354
  map_display
355
  ]
356
  )
@@ -359,29 +325,27 @@ with gr.Blocks(title="Montana Mountain Weather - Satellite & Forecast") as demo:
359
  ## Instructions
360
  1. Use the quick access buttons to check specific Montana peaks
361
  2. Or enter coordinates manually / click on the map
362
- 3. Click "Get Weather" to see the forecast and satellite data
363
 
364
  **Montana Peaks Included:**
365
  - Lone Peak (Big Sky): 45°16′41″N 111°27′01″W
366
  - Sacajawea Peak: 45°53′45″N 110°58′7″W
367
  - Pioneer Mountain: 45°13′55″N 111°27′2″W
368
 
369
- **Satellite Products:**
370
- - Visible Cloud Cover
371
- - Infrared Clean
372
- - True Color
373
- - Water Vapor (Multiple Levels)
 
374
 
375
  **Forecast Products:**
376
- - Temperature (Max/Min, Days 1-2)
377
- - Precipitation (6/12/24/48 Hour)
378
- - Snow Probability and Amount
379
- - Weather Type and Surface Analysis
380
-
381
- The satellite animations show the last 6 hours with 10-minute intervals.
382
- All images are automatically cropped to focus on the selected location.
383
 
384
- **Note**: This app uses NOAA and GOES-West satellite data.
385
  Mountain weather can change rapidly - always check multiple sources for safety.
386
  """)
387
 
 
9
 
10
  # Montana Mountain Peaks coordinates
11
  MONTANA_PEAKS = {
12
+ "Lone Peak (Big Sky)": (45.27806, -111.45028),
13
+ "Sacajawea Peak": (45.89583, -110.96861),
14
+ "Pioneer Mountain": (45.231835, -111.450505)
15
  }
16
 
17
+ def get_radar_frames(lat, lon, hours_back=6):
18
+ """Get historical radar frames and create animation."""
19
+ frames = []
20
+ frame_files = []
21
+ current_time = datetime.utcnow()
22
+
23
+ # Get historical radar frames at 5-minute intervals
24
+ for hour in range(hours_back, -1, -1):
25
+ time = current_time - timedelta(hours=hour)
26
+ timestamp = time.strftime("%Y%m%d_%H%M")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
 
28
+ # Different radar products available
29
+ url_base = "https://radar.weather.gov/ridge/standard/CONUS"
30
  products = [
31
+ (f"{url_base}_{timestamp}.gif", "Standard Radar"),
32
+ (f"{url_base}_BR_{timestamp}.gif", "Base Reflectivity"),
33
+ (f"{url_base}_CV_{timestamp}.gif", "Composite Reflectivity"),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34
  ]
35
 
36
+ for url, title in products:
37
  try:
38
+ response = requests.get(url, timeout=5)
39
  if response.status_code == 200:
40
+ img = Image.open(io.BytesIO(response.content)).convert('RGB')
 
 
 
 
 
41
  img = crop_to_region(img, lat, lon)
42
 
43
+ # Add timestamp
44
  draw = ImageDraw.Draw(img)
45
+ text = f"{title}\n{time.strftime('%Y-%m-%d %H:%M UTC')}"
46
+ draw_text_with_outline(draw, text, (10, 10))
 
 
47
 
48
+ # Save both individual frame and add to animation
49
+ with tempfile.NamedTemporaryFile(delete=False, suffix='.png') as tmp:
50
+ img.save(tmp.name)
51
+ frame_files.append(tmp.name)
52
+ frames.append(img)
53
+ break
 
 
 
 
 
 
 
54
  except Exception as e:
 
55
  continue
56
+
57
+ if frames:
58
+ # Create animation
59
+ with tempfile.NamedTemporaryFile(delete=False, suffix='.gif') as tmp:
60
+ frames[0].save(
61
+ tmp.name,
62
+ save_all=True,
63
+ append_images=frames[1:],
64
+ duration=200,
65
+ loop=0
66
+ )
67
+ return tmp.name, frame_files
68
+
69
+ return None, []
70
+
71
+ def crop_to_region(img, lat, lon, zoom=1.5):
72
+ """Crop image to focus on selected region."""
73
+ img_width, img_height = img.size
74
+
75
+ # CONUS bounds (approximate)
76
+ lat_min, lat_max = 25.0, 50.0
77
+ lon_min, lon_max = -125.0, -65.0
78
+
79
+ # Convert coordinates to image space
80
+ x = (lon - lon_min) / (lon_max - lon_min) * img_width
81
+ y = (lat_max - lat) / (lat_max - lat_min) * img_height
82
+
83
+ # Calculate crop box
84
+ crop_width = img_width / zoom
85
+ crop_height = img_height / zoom
86
+
87
+ # Center crop box on point
88
+ x1 = max(0, x - crop_width/2)
89
+ y1 = max(0, y - crop_height/2)
90
+ x2 = min(img_width, x + crop_width/2)
91
+ y2 = min(img_height, y + crop_height/2)
92
+
93
+ # Ensure we don't crop outside bounds
94
+ if x1 < 0: x2 -= x1; x1 = 0
95
+ if y1 < 0: y2 -= y1; y1 = 0
96
+ if x2 > img_width: x1 -= (x2 - img_width); x2 = img_width
97
+ if y2 > img_height: y1 -= (y2 - img_height); y2 = img_height
98
+
99
+ # Crop and resize
100
+ cropped = img.crop((x1, y1, x2, y2))
101
+ return cropped.resize((img_width, img_height), Image.Resampling.LANCZOS)
102
+
103
+ def draw_text_with_outline(draw, text, pos, font_size=20):
104
+ """Draw text with outline for visibility."""
105
+ try:
106
+ font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", font_size)
107
+ except:
108
+ font = ImageFont.load_default()
109
+
110
+ x, y = pos
111
+ # Draw outline
112
+ for dx, dy in [(-1,-1), (-1,1), (1,-1), (1,1)]:
113
+ draw.text((x+dx, y+dy), text, fill='black', font=font)
114
+ # Draw text
115
+ draw.text((x, y), text, fill='white', font=font)
116
+
117
+ def get_forecast_products(lat, lon):
118
+ """Get various forecast products."""
119
+ gallery_data = []
120
+ timestamp = datetime.utcnow().strftime('%Y-%m-%d %H:%M UTC')
121
+
122
+ products = [
123
+ # Temperature
124
+ ("MaxT1_conus.png", "Maximum Temperature"),
125
+ ("MinT1_conus.png", "Minimum Temperature"),
126
+ # Precipitation
127
+ ("QPF06_conus.png", "6-Hour Precipitation"),
128
+ ("QPF12_conus.png", "12-Hour Precipitation"),
129
+ ("QPF24_conus.png", "24-Hour Precipitation"),
130
+ # Snow
131
+ ("Snow1_conus.png", "Snowfall Amount"),
132
+ ("Snow2_conus.png", "Snowfall Day 2"),
133
+ # Weather Type
134
+ ("Wx1_conus.png", "Weather Type"),
135
+ ]
136
+
137
+ base_url = "https://graphical.weather.gov/images/conus"
138
+
139
+ for filename, title in products:
140
+ try:
141
+ url = f"{base_url}/{filename}"
142
+ response = requests.get(url, timeout=10)
143
+ if response.status_code == 200:
144
+ img = Image.open(io.BytesIO(response.content)).convert('RGB')
145
+ img = crop_to_region(img, lat, lon)
146
+
147
+ # Add title
148
+ draw = ImageDraw.Draw(img)
149
+ text = f"{title}\n{timestamp}"
150
+ draw_text_with_outline(draw, text, (10, 10))
151
+
152
+ # Save processed image
153
+ with tempfile.NamedTemporaryFile(delete=False, suffix='.png') as tmp:
154
+ img.save(tmp.name)
155
+ gallery_data.append(tmp.name)
156
+ except Exception as e:
157
+ continue
158
+
159
+ return gallery_data
160
+
161
+ def get_map(lat, lon):
162
+ """Create a map centered on the given coordinates with markers."""
163
+ m = folium.Map(location=[lat, lon], zoom_start=9)
164
+
165
+ # Add all Montana peaks
166
+ for peak_name, coords in MONTANA_PEAKS.items():
167
+ folium.Marker(
168
+ coords,
169
+ popup=f"{peak_name}<br>Lat: {coords[0]:.4f}, Lon: {coords[1]:.4f}",
170
+ tooltip=peak_name
171
+ ).add_to(m)
172
+
173
+ # Add current location marker if not a peak
174
+ if (lat, lon) not in MONTANA_PEAKS.values():
175
+ folium.Marker([lat, lon], popup=f"Selected Location<br>Lat: {lat:.4f}, Lon: {lon:.4f}").add_to(m)
176
+
177
+ m.add_child(folium.ClickForLatLng())
178
+ return m._repr_html_()
179
+
180
+ def get_noaa_forecast(lat, lon):
181
+ """Get NOAA text forecast."""
182
+ try:
183
+ # Get forecast URL
184
+ points_url = f"https://api.weather.gov/points/{lat},{lon}"
185
+ response = requests.get(points_url, timeout=10)
186
+ forecast_url = response.json()['properties']['forecast']
187
 
188
+ # Get forecast data
189
+ forecast = requests.get(forecast_url, timeout=10).json()
190
 
191
+ # Format text forecast
192
+ text = "Weather Forecast:\n\n"
193
+ for period in forecast['properties']['periods']:
194
+ text += f"{period['name']}:\n"
195
+ text += f"Temperature: {period['temperature']}°{period['temperatureUnit']}\n"
196
+ text += f"Wind: {period['windSpeed']} {period['windDirection']}\n"
197
+ text += f"{period['detailedForecast']}\n\n"
198
+
199
+ # Highlight snow events
200
+ if any(word in period['detailedForecast'].lower() for word in
201
+ ['snow', 'flurries', 'wintry mix', 'blizzard']):
202
+ text += "⚠️ SNOW EVENT PREDICTED ⚠️\n\n"
203
+
204
+ return text
205
  except Exception as e:
206
+ return f"Error getting forecast: {str(e)}"
 
207
 
208
+ def make_peak_click_handler(peak_name):
209
+ """Creates a click handler for a specific peak."""
210
+ def handler():
211
+ return MONTANA_PEAKS[peak_name]
212
+ return handler
213
 
214
  def update_weather(lat, lon):
215
  """Update weather information based on coordinates."""
 
218
  lat = float(lat)
219
  lon = float(lon)
220
  if not (-90 <= lat <= 90 and -180 <= lon <= 180):
221
+ return "Invalid coordinates", [], None, get_map(45.5, -111.0)
222
 
223
  # Get text forecast
224
+ forecast_text = get_noaa_forecast(lat, lon)
225
 
226
+ # Get radar animation and frames
227
+ radar_animation, frames = get_radar_frames(lat, lon)
228
 
229
  # Get forecast products
230
+ forecast_frames = get_forecast_products(lat, lon)
231
 
232
  # Combine all images for gallery
233
+ gallery_data = frames + forecast_frames
234
 
235
  # Get map
236
  map_html = get_map(lat, lon)
237
 
238
+ return forecast_text, gallery_data, radar_animation, map_html
239
 
240
  except Exception as e:
241
+ return f"Error: {str(e)}", [], None, get_map(45.5, -111.0)
242
 
243
  # Create Gradio interface
244
+ with gr.Blocks(title="Montana Mountain Weather") as demo:
245
+ gr.Markdown("# Montana Mountain Weather")
246
 
247
  with gr.Row():
248
  with gr.Column(scale=1):
 
259
  maximum=180
260
  )
261
 
 
262
  gr.Markdown("### Quick Access - Montana Peaks")
263
  peak_buttons = []
264
+ for peak_name in MONTANA_PEAKS:
265
  peak_buttons.append(gr.Button(f"📍 {peak_name}"))
266
 
267
  submit_btn = gr.Button("Get Weather", variant="primary")
 
277
  placeholder="Select a location to see the forecast..."
278
  )
279
  with gr.Column(scale=2):
280
+ radar_animation = gr.Image(
281
+ label="Radar Animation",
282
  show_label=True,
283
+ type="filepath"
 
 
284
  )
285
 
286
  with gr.Row():
287
  forecast_gallery = gr.Gallery(
288
+ label="Weather Products",
289
  show_label=True,
290
  columns=4,
291
+ height=600,
292
  object_fit="contain"
293
  )
294
 
 
299
  outputs=[
300
  forecast_output,
301
  forecast_gallery,
302
+ radar_animation,
 
303
  map_display
304
  ]
305
  )
 
316
  outputs=[
317
  forecast_output,
318
  forecast_gallery,
319
+ radar_animation,
 
320
  map_display
321
  ]
322
  )
 
325
  ## Instructions
326
  1. Use the quick access buttons to check specific Montana peaks
327
  2. Or enter coordinates manually / click on the map
328
+ 3. Click "Get Weather" to see the forecast and weather products
329
 
330
  **Montana Peaks Included:**
331
  - Lone Peak (Big Sky): 45°16′41″N 111°27′01″W
332
  - Sacajawea Peak: 45°53′45″N 110°58′7″W
333
  - Pioneer Mountain: 45°13′55″N 111°27′2″W
334
 
335
+ **Radar Products:**
336
+ - Standard Radar
337
+ - Base Reflectivity
338
+ - Composite Reflectivity
339
+ All images are cropped to focus on selected location.
340
+ Radar animation shows the last 6 hours of data.
341
 
342
  **Forecast Products:**
343
+ - Temperature (Max/Min)
344
+ - Precipitation (6/12/24 Hour)
345
+ - Snowfall Amount
346
+ - Weather Type
 
 
 
347
 
348
+ **Note**: This app uses NOAA weather data.
349
  Mountain weather can change rapidly - always check multiple sources for safety.
350
  """)
351