nakas commited on
Commit
51d599e
·
verified ·
1 Parent(s): c2660c5

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +143 -221
app.py CHANGED
@@ -6,105 +6,133 @@ import tempfile
6
  import os
7
  from PIL import Image, ImageDraw, ImageFont
8
  import io
 
 
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
-
22
- # Adjust current time to allow for NOAA delay and round down to the nearest 10 minutes.
23
- now = datetime.utcnow() - timedelta(minutes=20)
24
- now = now - timedelta(minutes=now.minute % 10, seconds=now.second, microseconds=now.microsecond)
25
-
26
- total_frames = (hours_back * 60) // 10 + 1 # one frame every 10 minutes
27
- for i in range(total_frames):
28
- time_point = now - timedelta(minutes=i * 10)
29
- # Updated NOAA radar image URL using jetstream/ridge_download
30
- urls = [
31
- f"https://radar.weather.gov/jetstream/ridge_download/N0R/KMSX_{time_point.strftime('%Y%m%d_%H%M')}_N0R.gif", # Base reflectivity
32
- f"https://radar.weather.gov/jetstream/ridge_download/NCR/KMSX_{time_point.strftime('%Y%m%d_%H%M')}_NCR.gif", # Composite reflectivity
33
- f"https://radar.weather.gov/jetstream/ridge_download/N0S/KMSX_{time_point.strftime('%Y%m%d_%H%M')}_N0S.gif", # Storm relative motion
34
- ]
35
-
36
- for url in urls:
37
- try:
38
- print(f"Trying URL: {url}")
39
- headers = {'User-Agent': 'Mozilla/5.0 (compatible; YourApp/1.0; +http://yourdomain.com)'}
40
- response = requests.get(url, timeout=5, headers=headers)
41
- if response.status_code == 200:
42
- print(f"Successfully downloaded: {url}")
43
- img = Image.open(io.BytesIO(response.content)).convert('RGB')
44
- img = crop_to_region(img, lat, lon)
45
-
46
- # Add timestamp overlay
47
- draw = ImageDraw.Draw(img)
48
- if "N0R" in url:
49
- radar_type = "Base"
50
- elif "NCR" in url:
51
- radar_type = "Composite"
52
- else:
53
- radar_type = "Storm"
54
- text = f"{radar_type} Radar\n{time_point.strftime('%Y-%m-%d %H:%M UTC')}"
55
- draw_text_with_outline(draw, text, (10, 10))
56
-
57
- # Save individual frame and add to animation list
58
- with tempfile.NamedTemporaryFile(delete=False, suffix='.png') as tmp:
59
- img.save(tmp.name)
60
- frame_files.append(tmp.name)
61
- frames.append(img)
62
- break # Use the first available image for this time_point
63
- except Exception as e:
64
- print(f"Error with URL {url}: {str(e)}")
65
- continue
66
-
67
- if frames:
68
- print(f"Creating animation with {len(frames)} frames")
69
- # Create animated gif from frames
70
- with tempfile.NamedTemporaryFile(delete=False, suffix='.gif') as tmp:
71
- frames[0].save(
72
- tmp.name,
73
- save_all=True,
74
- append_images=frames[1:],
75
- duration=200, # 0.2 seconds per frame
76
- loop=0
77
- )
78
- print(f"Animation saved to {tmp.name}")
79
- return tmp.name, frame_files
80
- else:
81
- print("No frames collected for animation")
82
-
83
- return None, []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
84
 
85
  def crop_to_region(img, lat, lon, zoom=1.5):
86
  """Crop image to focus on selected region."""
87
  img_width, img_height = img.size
88
-
89
- # CONUS bounds (approximate)
90
  lat_min, lat_max = 25.0, 50.0
91
  lon_min, lon_max = -125.0, -65.0
92
-
93
- # Convert coordinates to image space
94
  x = (lon - lon_min) / (lon_max - lon_min) * img_width
95
  y = (lat_max - lat) / (lat_max - lat_min) * img_height
96
-
97
- # Calculate crop box dimensions
98
  crop_width = img_width / zoom
99
  crop_height = img_height / zoom
100
-
101
- # Center crop box on the point
102
  x1 = max(0, x - crop_width / 2)
103
  y1 = max(0, y - crop_height / 2)
104
  x2 = min(img_width, x + crop_width / 2)
105
  y2 = min(img_height, y + crop_height / 2)
106
-
107
- # Adjust if crop box goes out of bounds
108
  if x1 < 0:
109
  x2 -= x1
110
  x1 = 0
@@ -117,18 +145,15 @@ def crop_to_region(img, lat, lon, zoom=1.5):
117
  if y2 > img_height:
118
  y1 -= (y2 - img_height)
119
  y2 = img_height
120
-
121
- # Crop and resize back to full image dimensions
122
  cropped = img.crop((x1, y1, x2, y2))
123
  return cropped.resize((img_width, img_height), Image.Resampling.LANCZOS)
124
 
125
  def draw_text_with_outline(draw, text, pos, font_size=20, center=False):
126
- """Draw text with outline for better visibility."""
127
  try:
128
  font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", font_size)
129
  except:
130
  font = ImageFont.load_default()
131
-
132
  x, y = pos
133
  if center:
134
  bbox = draw.textbbox((0, 0), text, font=font)
@@ -136,132 +161,51 @@ def draw_text_with_outline(draw, text, pos, font_size=20, center=False):
136
  text_height = bbox[3] - bbox[1]
137
  x = x - text_width // 2
138
  y = y - text_height // 2
139
-
140
  for dx, dy in [(-1, -1), (-1, 1), (1, -1), (1, 1)]:
141
  draw.text((x + dx, y + dy), text, fill='black', font=font)
142
  draw.text((x, y), text, fill='white', font=font)
143
 
144
- def get_forecast_products(lat, lon):
145
- """Get various forecast products."""
146
- gallery_data = []
147
- timestamp = datetime.utcnow().strftime('%Y-%m-%d %H:%M UTC')
148
-
149
- products = [
150
- ("MaxT1_conus.png", "Maximum Temperature"),
151
- ("MinT1_conus.png", "Minimum Temperature"),
152
- ("QPF06_conus.png", "6-Hour Precipitation"),
153
- ("QPF12_conus.png", "12-Hour Precipitation"),
154
- ("QPF24_conus.png", "24-Hour Precipitation"),
155
- ("Snow1_conus.png", "Snowfall Amount"),
156
- ("Snow2_conus.png", "Snowfall Day 2"),
157
- ("Wx1_conus.png", "Weather Type"),
158
- ]
159
-
160
- base_url = "https://graphical.weather.gov/images/conus"
161
-
162
- for filename, title in products:
163
- try:
164
- url = f"{base_url}/{filename}"
165
- response = requests.get(url, timeout=10)
166
- if response.status_code == 200:
167
- img = Image.open(io.BytesIO(response.content)).convert('RGB')
168
- img = crop_to_region(img, lat, lon)
169
-
170
- draw = ImageDraw.Draw(img)
171
- text = f"{title}\n{timestamp}"
172
- draw_text_with_outline(draw, text, (10, 10))
173
-
174
- with tempfile.NamedTemporaryFile(delete=False, suffix='.png') as tmp:
175
- img.save(tmp.name)
176
- gallery_data.append(tmp.name)
177
- except Exception as e:
178
- continue
179
-
180
- return gallery_data
181
-
182
  def get_map(lat, lon):
183
- """Create a map centered on the given coordinates with markers."""
184
  m = folium.Map(location=[lat, lon], zoom_start=9)
185
-
186
  for peak_name, coords in MONTANA_PEAKS.items():
187
  folium.Marker(
188
  coords,
189
  popup=f"{peak_name}<br>Lat: {coords[0]:.4f}, Lon: {coords[1]:.4f}",
190
  tooltip=peak_name
191
  ).add_to(m)
192
-
193
  if (lat, lon) not in MONTANA_PEAKS.values():
194
  folium.Marker([lat, lon], popup=f"Selected Location<br>Lat: {lat:.4f}, Lon: {lon:.4f}").add_to(m)
195
-
196
  m.add_child(folium.ClickForLatLng())
197
  return m._repr_html_()
198
 
199
- def get_noaa_forecast(lat, lon):
200
- """Get NOAA text forecast."""
201
- try:
202
- points_url = f"https://api.weather.gov/points/{lat},{lon}"
203
- response = requests.get(points_url, timeout=10)
204
- forecast_url = response.json()['properties']['forecast']
205
-
206
- forecast = requests.get(forecast_url, timeout=10).json()
207
-
208
- text = "Weather Forecast:\n\n"
209
- for period in forecast['properties']['periods']:
210
- text += f"{period['name']}:\n"
211
- text += f"Temperature: {period['temperature']}°{period['temperatureUnit']}\n"
212
- text += f"Wind: {period['windSpeed']} {period['windDirection']}\n"
213
- text += f"{period['detailedForecast']}\n\n"
214
- if any(word in period['detailedForecast'].lower() for word in
215
- ['snow', 'flurries', 'wintry mix', 'blizzard']):
216
- text += "⚠️ SNOW EVENT PREDICTED ⚠️\n\n"
217
-
218
- return text
219
- except Exception as e:
220
- return f"Error getting forecast: {str(e)}"
221
-
222
- def make_peak_click_handler(peak_name):
223
- """Creates a click handler for a specific peak."""
224
- def handler():
225
- lat, lon = MONTANA_PEAKS[peak_name]
226
- return lat, lon
227
- return handler
228
-
229
- def update_weather(lat, lon):
230
- """Update weather information based on coordinates."""
231
  try:
232
  lat = float(lat)
233
  lon = float(lon)
234
  if not (-90 <= lat <= 90 and -180 <= lon <= 180):
235
- return "Invalid coordinates", [], None, get_map(45.5, -111.0)
236
-
237
  forecast_text = get_noaa_forecast(lat, lon)
238
- radar_animation, frames = get_radar_frames(lat, lon)
 
 
239
  forecast_frames = get_forecast_products(lat, lon)
240
- gallery_data = frames + forecast_frames
241
  map_html = get_map(lat, lon)
242
-
243
- return forecast_text, gallery_data, radar_animation, map_html
244
-
245
  except Exception as e:
246
- return f"Error: {str(e)}", [], None, get_map(45.5, -111.0)
247
 
248
  with gr.Blocks(title="Montana Mountain Weather") as demo:
249
  gr.Markdown("# Montana Mountain Weather")
250
 
251
  with gr.Row():
252
  with gr.Column(scale=1):
253
- lat_input = gr.Number(
254
- label="Latitude",
255
- value=45.5,
256
- minimum=-90,
257
- maximum=90
258
- )
259
- lon_input = gr.Number(
260
- label="Longitude",
261
- value=-111.0,
262
- minimum=-180,
263
- maximum=180
264
- )
265
 
266
  gr.Markdown("### Quick Access - Montana Peaks")
267
  peak_buttons = []
@@ -269,77 +213,56 @@ with gr.Blocks(title="Montana Mountain Weather") as demo:
269
  peak_buttons.append(gr.Button(f"📍 {peak_name}"))
270
 
271
  submit_btn = gr.Button("Get Weather", variant="primary")
272
-
273
  with gr.Column(scale=2):
274
  map_display = gr.HTML(get_map(45.5, -111.0))
275
 
276
  with gr.Row():
277
  with gr.Column(scale=1):
278
- forecast_output = gr.Textbox(
279
- label="Weather Forecast",
280
- lines=12,
281
- placeholder="Select a location to see the forecast..."
282
- )
283
  with gr.Column(scale=2):
284
- radar_animation = gr.Image(
285
- label="Radar Animation",
286
- show_label=True,
287
- type="filepath"
288
- )
289
 
290
  with gr.Row():
291
- forecast_gallery = gr.Gallery(
292
- label="Weather Products",
293
- show_label=True,
294
- columns=4,
295
- height=600,
296
- object_fit="contain"
297
- )
298
 
299
  submit_btn.click(
300
  fn=update_weather,
301
- inputs=[lat_input, lon_input],
302
- outputs=[
303
- forecast_output,
304
- forecast_gallery,
305
- radar_animation,
306
- map_display
307
- ]
308
  )
309
 
310
  for i, peak_name in enumerate(MONTANA_PEAKS.keys()):
311
  peak_buttons[i].click(
312
- fn=make_peak_click_handler(peak_name),
313
  inputs=[],
314
  outputs=[lat_input, lon_input]
315
  ).then(
316
  fn=update_weather,
317
- inputs=[lat_input, lon_input],
318
- outputs=[
319
- forecast_output,
320
- forecast_gallery,
321
- radar_animation,
322
- map_display
323
- ]
324
  )
325
 
326
  gr.Markdown("""
327
  ## Instructions
328
- 1. Use the quick access buttons to check specific Montana peaks
329
- 2. Or enter coordinates manually / click on the map
330
- 3. Click "Get Weather" to see the forecast and weather products
 
331
 
332
  **Montana Peaks Included:**
333
  - Lone Peak (Big Sky): 45°16′41″N 111°27′01″W
334
  - Sacajawea Peak: 45°53′45″N 110°58′7″W
335
  - Pioneer Mountain: 45°13′55″N 111°27′2″W
336
 
337
- **Radar Products:**
338
- - Base Reflectivity
339
- - Composite Reflectivity
340
- - Storm Relative Motion
341
- All images are cropped to focus on the selected location.
342
- Radar animation shows the last 6 hours of data.
343
 
344
  **Forecast Products:**
345
  - Temperature (Max/Min)
@@ -347,8 +270,7 @@ with gr.Blocks(title="Montana Mountain Weather") as demo:
347
  - Snowfall Amount
348
  - Weather Type
349
 
350
- **Note**: This app uses NOAA weather data.
351
- Mountain weather can change rapidly - always check multiple sources for safety.
352
  """)
353
 
354
  demo.queue().launch()
 
6
  import os
7
  from PIL import Image, ImageDraw, ImageFont
8
  import io
9
+ import boto3
10
+ import botocore
11
 
12
+ # Montana Mountain Peaks coordinates (for map and quick access buttons)
13
  MONTANA_PEAKS = {
14
  "Lone Peak (Big Sky)": (45.27806, -111.45028),
15
  "Sacajawea Peak": (45.89583, -110.96861),
16
  "Pioneer Mountain": (45.231835, -111.450505)
17
  }
18
 
19
+ def get_nexrad_file(station, product="N0S", hours_back=6):
20
+ """
21
+ Connects to the NOAA NEXRAD Level-II S3 bucket (noaa-nexrad-level2) and
22
+ retrieves the latest file (within the past `hours_back` hours) for the given
23
+ station and product code. The files are stored under the path:
24
+ {station}/{YYYY}/{MM}/{DD}/
25
+ and have filenames like:
26
+ {station}_{YYYYMMDD}_{HHMM}_{product}.gz
27
+ Returns a tuple of (local_file_path, s3_key) or (None, "") if no file was found.
28
+ """
29
+ s3 = boto3.client('s3')
30
+ bucket = "noaa-nexrad-level2"
31
+ now = datetime.utcnow() - timedelta(minutes=20) # allow for delay
32
+ start_time = now - timedelta(hours=hours_back)
33
+ files = []
34
+ # Check each hour in the time window
35
+ for i in range(hours_back + 1):
36
+ dt = start_time + timedelta(hours=i)
37
+ prefix = f"{station}/{dt.strftime('%Y')}/{dt.strftime('%m')}/{dt.strftime('%d')}/"
38
+ try:
39
+ resp = s3.list_objects_v2(Bucket=bucket, Prefix=prefix)
40
+ except botocore.exceptions.ClientError as e:
41
+ continue
42
+ if "Contents" in resp:
43
+ for obj in resp["Contents"]:
44
+ key = obj["Key"]
45
+ # Look for the desired product code in the filename.
46
+ # Typical filename: KMSX_20250221_1320_N0S.gz (or similar)
47
+ if f"_{product}." in key:
48
+ try:
49
+ parts = key.split("_")
50
+ if len(parts) >= 3:
51
+ # Combine the date and time parts
52
+ timestamp_str = parts[1] # YYYYMMDD
53
+ time_str = parts[2] # HHMM (might include additional info if not split by underscore)
54
+ # Remove any suffix from time_str (e.g. if it ends with extra letters)
55
+ time_str = ''.join(filter(str.isdigit, time_str))
56
+ file_dt = datetime.strptime(timestamp_str + time_str, "%Y%m%d%H%M")
57
+ if start_time <= file_dt <= now:
58
+ files.append((file_dt, key))
59
+ except Exception as e:
60
+ continue
61
+ if not files:
62
+ return None, ""
63
+ # Sort descending by file timestamp and choose the latest file
64
+ files.sort(key=lambda x: x[0], reverse=True)
65
+ latest_file_key = files[0][1]
66
+ # Download the file to a temporary location
67
+ tmp_file = tempfile.NamedTemporaryFile(delete=False, suffix=".gz")
68
+ s3.download_file(bucket, latest_file_key, tmp_file.name)
69
+ return tmp_file.name, latest_file_key
70
+
71
+ def get_noaa_forecast(lat, lon):
72
+ """Get NOAA text forecast using the points API."""
73
+ try:
74
+ points_url = f"https://api.weather.gov/points/{lat},{lon}"
75
+ response = requests.get(points_url, timeout=10)
76
+ forecast_url = response.json()['properties']['forecast']
77
+ forecast = requests.get(forecast_url, timeout=10).json()
78
+ text = "Weather Forecast:\n\n"
79
+ for period in forecast['properties']['periods']:
80
+ text += f"{period['name']}:\n"
81
+ text += f"Temperature: {period['temperature']}°{period['temperatureUnit']}\n"
82
+ text += f"Wind: {period['windSpeed']} {period['windDirection']}\n"
83
+ text += f"{period['detailedForecast']}\n\n"
84
+ if any(word in period['detailedForecast'].lower() for word in
85
+ ['snow', 'flurries', 'wintry mix', 'blizzard']):
86
+ text += "⚠️ SNOW EVENT PREDICTED ⚠️\n\n"
87
+ return text
88
+ except Exception as e:
89
+ return f"Error getting forecast: {str(e)}"
90
+
91
+ def get_forecast_products(lat, lon):
92
+ """Download and process various forecast product images."""
93
+ gallery_data = []
94
+ timestamp = datetime.utcnow().strftime('%Y-%m-%d %H:%M UTC')
95
+ products = [
96
+ ("MaxT1_conus.png", "Maximum Temperature"),
97
+ ("MinT1_conus.png", "Minimum Temperature"),
98
+ ("QPF06_conus.png", "6-Hour Precipitation"),
99
+ ("QPF12_conus.png", "12-Hour Precipitation"),
100
+ ("QPF24_conus.png", "24-Hour Precipitation"),
101
+ ("Snow1_conus.png", "Snowfall Amount"),
102
+ ("Snow2_conus.png", "Snowfall Day 2"),
103
+ ("Wx1_conus.png", "Weather Type"),
104
+ ]
105
+ base_url = "https://graphical.weather.gov/images/conus"
106
+ for filename, title in products:
107
+ try:
108
+ url = f"{base_url}/{filename}"
109
+ response = requests.get(url, timeout=10)
110
+ if response.status_code == 200:
111
+ img = Image.open(io.BytesIO(response.content)).convert('RGB')
112
+ img = crop_to_region(img, lat, lon)
113
+ draw = ImageDraw.Draw(img)
114
+ text = f"{title}\n{timestamp}"
115
+ draw_text_with_outline(draw, text, (10, 10))
116
+ with tempfile.NamedTemporaryFile(delete=False, suffix='.png') as tmp:
117
+ img.save(tmp.name)
118
+ gallery_data.append(tmp.name)
119
+ except Exception as e:
120
+ continue
121
+ return gallery_data
122
 
123
  def crop_to_region(img, lat, lon, zoom=1.5):
124
  """Crop image to focus on selected region."""
125
  img_width, img_height = img.size
 
 
126
  lat_min, lat_max = 25.0, 50.0
127
  lon_min, lon_max = -125.0, -65.0
 
 
128
  x = (lon - lon_min) / (lon_max - lon_min) * img_width
129
  y = (lat_max - lat) / (lat_max - lat_min) * img_height
 
 
130
  crop_width = img_width / zoom
131
  crop_height = img_height / zoom
 
 
132
  x1 = max(0, x - crop_width / 2)
133
  y1 = max(0, y - crop_height / 2)
134
  x2 = min(img_width, x + crop_width / 2)
135
  y2 = min(img_height, y + crop_height / 2)
 
 
136
  if x1 < 0:
137
  x2 -= x1
138
  x1 = 0
 
145
  if y2 > img_height:
146
  y1 -= (y2 - img_height)
147
  y2 = img_height
 
 
148
  cropped = img.crop((x1, y1, x2, y2))
149
  return cropped.resize((img_width, img_height), Image.Resampling.LANCZOS)
150
 
151
  def draw_text_with_outline(draw, text, pos, font_size=20, center=False):
152
+ """Draw text with an outline for better visibility."""
153
  try:
154
  font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", font_size)
155
  except:
156
  font = ImageFont.load_default()
 
157
  x, y = pos
158
  if center:
159
  bbox = draw.textbbox((0, 0), text, font=font)
 
161
  text_height = bbox[3] - bbox[1]
162
  x = x - text_width // 2
163
  y = y - text_height // 2
 
164
  for dx, dy in [(-1, -1), (-1, 1), (1, -1), (1, 1)]:
165
  draw.text((x + dx, y + dy), text, fill='black', font=font)
166
  draw.text((x, y), text, fill='white', font=font)
167
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
168
  def get_map(lat, lon):
169
+ """Create a folium map centered on the coordinates with markers for Montana peaks."""
170
  m = folium.Map(location=[lat, lon], zoom_start=9)
 
171
  for peak_name, coords in MONTANA_PEAKS.items():
172
  folium.Marker(
173
  coords,
174
  popup=f"{peak_name}<br>Lat: {coords[0]:.4f}, Lon: {coords[1]:.4f}",
175
  tooltip=peak_name
176
  ).add_to(m)
 
177
  if (lat, lon) not in MONTANA_PEAKS.values():
178
  folium.Marker([lat, lon], popup=f"Selected Location<br>Lat: {lat:.4f}, Lon: {lon:.4f}").add_to(m)
 
179
  m.add_child(folium.ClickForLatLng())
180
  return m._repr_html_()
181
 
182
+ def update_weather(lat, lon, station, product):
183
+ """Update weather info and retrieve raw radar data from AWS."""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
184
  try:
185
  lat = float(lat)
186
  lon = float(lon)
187
  if not (-90 <= lat <= 90 and -180 <= lon <= 180):
188
+ return "Invalid coordinates", [], "No radar data", get_map(45.5, -111.0), ""
 
189
  forecast_text = get_noaa_forecast(lat, lon)
190
+ # Retrieve raw radar data file from AWS (returns local file path and S3 key)
191
+ radar_file_path, radar_key = get_nexrad_file(station, product, hours_back=6)
192
+ # Get forecast product images
193
  forecast_frames = get_forecast_products(lat, lon)
194
+ gallery_data = forecast_frames
195
  map_html = get_map(lat, lon)
196
+ return forecast_text, gallery_data, radar_file_path, map_html, radar_key
 
 
197
  except Exception as e:
198
+ return f"Error: {str(e)}", [], "No radar data", get_map(45.5, -111.0), ""
199
 
200
  with gr.Blocks(title="Montana Mountain Weather") as demo:
201
  gr.Markdown("# Montana Mountain Weather")
202
 
203
  with gr.Row():
204
  with gr.Column(scale=1):
205
+ lat_input = gr.Number(label="Latitude", value=45.5, minimum=-90, maximum=90)
206
+ lon_input = gr.Number(label="Longitude", value=-111.0, minimum=-180, maximum=180)
207
+ station_input = gr.Textbox(label="Radar Station ID", value="KMSX")
208
+ product_input = gr.Textbox(label="Radar Product Code", value="N0S")
 
 
 
 
 
 
 
 
209
 
210
  gr.Markdown("### Quick Access - Montana Peaks")
211
  peak_buttons = []
 
213
  peak_buttons.append(gr.Button(f"📍 {peak_name}"))
214
 
215
  submit_btn = gr.Button("Get Weather", variant="primary")
 
216
  with gr.Column(scale=2):
217
  map_display = gr.HTML(get_map(45.5, -111.0))
218
 
219
  with gr.Row():
220
  with gr.Column(scale=1):
221
+ forecast_output = gr.Textbox(label="Weather Forecast", lines=12,
222
+ placeholder="Select a location to see the forecast...")
 
 
 
223
  with gr.Column(scale=2):
224
+ radar_data_output = gr.Textbox(label="Raw Radar Data File Path",
225
+ placeholder="Radar data file path will appear here")
 
 
 
226
 
227
  with gr.Row():
228
+ forecast_gallery = gr.Gallery(label="Forecast Products", show_label=True,
229
+ columns=4, height=600, object_fit="contain")
230
+
231
+ radar_key_output = gr.Textbox(label="S3 Key for Radar Data",
232
+ placeholder="S3 key will appear here")
 
 
233
 
234
  submit_btn.click(
235
  fn=update_weather,
236
+ inputs=[lat_input, lon_input, station_input, product_input],
237
+ outputs=[forecast_output, forecast_gallery, radar_data_output, map_display, radar_key_output]
 
 
 
 
 
238
  )
239
 
240
  for i, peak_name in enumerate(MONTANA_PEAKS.keys()):
241
  peak_buttons[i].click(
242
+ fn=lambda name=peak_name: MONTANA_PEAKS[name],
243
  inputs=[],
244
  outputs=[lat_input, lon_input]
245
  ).then(
246
  fn=update_weather,
247
+ inputs=[lat_input, lon_input, station_input, product_input],
248
+ outputs=[forecast_output, forecast_gallery, radar_data_output, map_display, radar_key_output]
 
 
 
 
 
249
  )
250
 
251
  gr.Markdown("""
252
  ## Instructions
253
+ 1. Use the quick access buttons to check specific Montana peaks.
254
+ 2. Or enter coordinates manually (or click on the map).
255
+ 3. Enter the Radar Station ID (e.g., KMSX) and Radar Product Code (e.g., N0S).
256
+ 4. Click "Get Weather" to see the forecast, forecast product images, and to download the latest raw radar data file from NOAA’s AWS bucket.
257
 
258
  **Montana Peaks Included:**
259
  - Lone Peak (Big Sky): 45°16′41″N 111°27′01″W
260
  - Sacajawea Peak: 45°53′45″N 110°58′7″W
261
  - Pioneer Mountain: 45°13′55″N 111°27′2″W
262
 
263
+ **Radar Data:**
264
+ - This app now retrieves raw NEXRAD Level‑II data (e.g. the “N0S” product) from NOAA’s AWS S3 bucket.
265
+ - You can process this raw file with external tools (like Py‑ART) to generate images.
 
 
 
266
 
267
  **Forecast Products:**
268
  - Temperature (Max/Min)
 
270
  - Snowfall Amount
271
  - Weather Type
272
 
273
+ **Note:** NOAA’s raw radar data is available via AWS and covers nearly all U.S. radars. For global coverage, you may need to explore additional sources.
 
274
  """)
275
 
276
  demo.queue().launch()