import gradio as gr import requests import folium from datetime import datetime, timedelta import tempfile import os from PIL import Image import io # Montana Mountain Peaks coordinates MONTANA_PEAKS = { "Lone Peak (Big Sky)": (45.27806, -111.45028), # 45°16′41″N 111°27′01″W "Sacajawea Peak": (45.89583, -110.96861), # 45°53′45″N 110°58′7″W "Pioneer Mountain": (45.231835, -111.450505) # 45°13′55″N 111°27′2″W } def download_image(url): """Download image from URL and return as PIL Image.""" try: response = requests.get(url, timeout=10) response.raise_for_status() image = Image.open(io.BytesIO(response.content)) return image except Exception as e: print(f"Error downloading image from {url}: {str(e)}") return None def get_snow_forecast(lat, lon): """Get snow forecast data from NOAA's snow forecast API.""" try: # Get the forecast URL for the coordinates points_url = f"https://api.weather.gov/points/{lat},{lon}" response = requests.get(points_url, timeout=10) response.raise_for_status() forecast_url = response.json()['properties']['forecast'] # Get the actual forecast with snow data forecast_response = requests.get(forecast_url, timeout=10) forecast_response.raise_for_status() forecast_data = forecast_response.json() # Process forecast periods to extract snow data snow_data = [] for period in forecast_data['properties']['periods']: period_data = { 'time': period['startTime'], 'temperature': period['temperature'], 'snowfall': 0 # Default to no snow } # Check if snow is mentioned in the forecast forecast_text = period['detailedForecast'].lower() if any(word in forecast_text for word in ['snow', 'flurries', 'wintry mix']): import re # Look for snow amounts in the text amount_patterns = [ r'(\d+(?:\.\d+)?)\s*(?:to\s*\d+(?:\.\d+)?)?\s*inches? of snow', r'snow accumulation of (\d+(?:\.\d+)?)\s*(?:to\s*\d+(?:\.\d+)?)?\s*inches', r'accumulation of (\d+(?:\.\d+)?)\s*(?:to\s*\d+(?:\.\d+)?)?\s*inches', ] for pattern in amount_patterns: match = re.search(pattern, forecast_text) if match: try: snow_amount = float(match.group(1)) period_data['snowfall'] = snow_amount / 39.37 # Convert inches to meters break except (ValueError, IndexError): continue snow_data.append(period_data) return snow_data except Exception as e: print(f"Error in snow forecast: {str(e)}") return [] except Exception as e: print(f"Error fetching snow forecast: {str(e)}") return None def calculate_snow_density(temp_c, is_new_snow=True): """Calculate approximate snow density based on temperature. Returns density in kg/m³.""" if is_new_snow: if temp_c <= -20: return 50 # Very cold, light powder elif temp_c <= -10: return 70 # Cold powder elif temp_c <= -5: return 100 # Moderate temperature powder elif temp_c <= 0: return 150 # Near freezing powder else: return 200 # Wet snow else: # For existing snowpack, density would be higher return 350 # Average settled snow density def get_noaa_forecast(lat, lon): """Get NOAA forecast data for given coordinates.""" points_url = f"https://api.weather.gov/points/{lat},{lon}" try: # Get the forecast URL for the coordinates response = requests.get(points_url, timeout=10) response.raise_for_status() forecast_url = response.json()['properties']['forecast'] # Get the actual forecast forecast_response = requests.get(forecast_url, timeout=10) forecast_response.raise_for_status() forecast_data = forecast_response.json() # Get snow forecast data snow_data = get_snow_forecast(lat, lon) # Format the forecast text periods = forecast_data['properties']['periods'] forecast_text = "" for i, period in enumerate(periods[:6]): # Show next 3 days (day and night) forecast_text += f"\n{period['name']}:\n" forecast_text += f"Temperature: {period['temperature']}°{period['temperatureUnit']}\n" forecast_text += f"Wind: {period['windSpeed']} {period['windDirection']}\n" # Convert temperature to Celsius for snow density calculation temp_c = (period['temperature'] - 32) * 5/9 if period['temperatureUnit'] == 'F' else period['temperature'] # Add snow-specific forecasts if available if i < len(snow_data) and snow_data[i]['snowfall'] > 0: snow_density = calculate_snow_density(temp_c) snow_amount = snow_data[i]['snowfall'] # in meters swe = snow_amount * (snow_density / 1000) # Convert density to g/cm³ # Convert to inches for display snow_inches = snow_amount * 39.37 swe_inches = swe * 39.37 forecast_text += f"Predicted Snowfall: {snow_inches:.1f} inches\n" forecast_text += f"Snow Water Equivalent: {swe_inches:.2f} inches\n" forecast_text += f"Estimated Snow Density: {snow_density} kg/m³ " forecast_text += f"({get_snow_quality_description(snow_density)})\n" forecast_text += f"{period['detailedForecast']}\n" return forecast_text.strip() except requests.exceptions.RequestException as e: return f"Error fetching forecast: {str(e)}" def get_snow_quality_description(density): """Return a description of snow quality based on density.""" if density <= 50: return "Ultra-light powder" elif density <= 70: return "Light powder" elif density <= 100: return "Moderate powder" elif density <= 150: return "Packed powder" elif density <= 200: return "Wet snow" else: return "Dense/settled snow" import gradio as gr import requests import folium from datetime import datetime, timedelta import tempfile import os from PIL import Image, ImageDraw, ImageFont import io # Montana Mountain Peaks coordinates MONTANA_PEAKS = { "Lone Peak (Big Sky)": (45.27806, -111.45028), # 45°16′41″N 111°27′01″W "Sacajawea Peak": (45.89583, -110.96861), # 45°53′45″N 110°58′7″W "Pioneer Mountain": (45.231835, -111.450505) # 45°13′55″N 111°27′2″W } def get_radar_and_snow_forecasts(): """Get current radar and forecast loop.""" try: # For the Montana region, get: # 1. Current base reflectivity # 2. Short-term forecast (0-6 hours) # 3. Medium-term forecast (6-12 hours) base_url = "https://radar.weather.gov/ridge/Overlays/Foregrounds" overlay_url = "https://radar.weather.gov/ridge/Overlays" background_url = "https://radar.weather.gov/ridge/Overlays/Backgrounds" # Build image frames frames = [] # Current conditions plus 6 forecast periods (7 total frames) for hour in range(7): try: # Start with the static background background = Image.open(requests.get(f"{background_url}/Topo/Short/KMSX_Topo_Short.jpg", timeout=10).content) background = background.convert('RGB') # Add county overlays counties = Image.open(requests.get(f"{overlay_url}/Counties/Short/KMSX_Counties_Short.gif", timeout=10).content) counties = counties.convert('RGBA') background.paste(counties, (0, 0), counties) # Add highways highways = Image.open(requests.get(f"{overlay_url}/Highways/Short/KMSX_Highways_Short.gif", timeout=10).content) highways = highways.convert('RGBA') background.paste(highways, (0, 0), highways) # Add current radar or forecast overlay if hour == 0: # Current radar radar = Image.open(requests.get(f"{base_url}/Base/KMSX_Base_0.gif", timeout=10).content) else: # Forecast radar (using NDFD data) forecast_time = datetime.now() + timedelta(hours=hour) forecast_str = forecast_time.strftime("%Y%m%d_%H") radar = Image.open(requests.get(f"https://forecast.weather.gov/wrf_images/KMSX/Reflectivity/Reflectivity_{forecast_str}.gif", timeout=10).content) radar = radar.convert('RGBA') background.paste(radar, (0, 0), radar) # Add timestamp draw = ImageDraw.Draw(background) if hour == 0: time_text = "Current Conditions" else: forecast_time = datetime.now() + timedelta(hours=hour) time_text = f"Forecast +{hour}h ({forecast_time.strftime('%I%p')})" try: font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", 20) except: font = ImageFont.load_default() # Draw text with outline x, y = 10, 10 for dx, dy in [(-1,-1), (-1,1), (1,-1), (1,1)]: draw.text((x+dx, y+dy), time_text, fill='black', font=font) draw.text((x, y), time_text, fill='white', font=font) frames.append(background) except Exception as e: print(f"Error processing frame {hour}: {str(e)}") continue if not frames: raise Exception("No frames could be generated") # Save as animated GIF with tempfile.NamedTemporaryFile(suffix='.gif', delete=False) as tmp_file: frames[0].save( tmp_file.name, save_all=True, append_images=frames[1:], duration=1500, # 1.5 seconds per frame loop=0 ) return tmp_file.name except Exception as e: print(f"Error creating forecast display: {str(e)}") # Create error message image img = Image.new('RGB', (800, 600), color='black') draw = ImageDraw.Draw(img) message = ( "Weather radar and forecast temporarily unavailable\n" f"{str(e)}\n" "Please check weather.gov for current conditions\n" f"Last attempt: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}" ) try: font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", 20) except: font = ImageFont.load_default() try: draw.text((400, 300), message, fill='white', anchor="mm", align="center", font=font) except: # Fallback if font fails draw.text((400, 300), message, fill='white', anchor="mm", align="center") with tempfile.NamedTemporaryFile(suffix='.gif', delete=False) as tmp_file: img.save(tmp_file.name, format='GIF') return tmp_file.name def create_map(): """Create a folium map centered on Montana with optional markers.""" # Center on Montana m = folium.Map(location=[45.5, -111.0], zoom_start=7) # Add markers for Montana peaks for peak_name, coords in MONTANA_PEAKS.items(): folium.Marker( coords, popup=f"{peak_name}
Lat: {coords[0]:.4f}, Lon: {coords[1]:.4f}", tooltip=peak_name ).add_to(m) m.add_child(folium.ClickForLatLng()) # Enable click events return m._repr_html_() def update_weather(lat, lon): """Update weather information based on coordinates.""" try: # Validate coordinates lat = float(lat) lon = float(lon) if not (-90 <= lat <= 90 and -180 <= lon <= 180): return "Invalid coordinates. Latitude must be between -90 and 90, longitude between -180 and 180.", None, get_map(lat, lon) # Get forecast forecast = get_noaa_forecast(lat, lon) # Get animated snow forecast animated_forecast = get_radar_and_snow_forecasts() # Create updated map map_html = get_map(lat, lon) return forecast, animated_forecast, map_html except ValueError as e: return f"Error: Invalid coordinate format - {str(e)}", None, get_map(lat, lon) except Exception as e: return f"Error: {str(e)}", None, get_map(lat, lon) def get_map(lat, lon): """Create a map centered on the given coordinates with markers.""" try: # Create map with appropriate zoom level m = folium.Map(location=[lat, lon], zoom_start=9) # Add all Montana peaks for peak_name, coords in MONTANA_PEAKS.items(): folium.Marker( coords, popup=f"{peak_name}
Lat: {coords[0]:.4f}, Lon: {coords[1]:.4f}", tooltip=peak_name ).add_to(m) # Add current location marker if not a peak if (lat, lon) not in MONTANA_PEAKS.values(): folium.Marker([lat, lon], popup=f"Selected Location
Lat: {lat:.4f}, Lon: {lon:.4f}").add_to(m) m.add_child(folium.ClickForLatLng()) # Enable click events return m._repr_html_() except Exception as e: print(f"Error creating map: {str(e)}") return None def make_peak_click_handler(peak_name): """Creates a click handler for a specific peak.""" def handler(): coords = MONTANA_PEAKS[peak_name] return coords[0], coords[1] return handler # Create Gradio interface with gr.Blocks(title="Montana Mountain Weather") as demo: gr.Markdown("# Montana Mountain Weather") with gr.Row(): with gr.Column(scale=1): lat_input = gr.Number( label="Latitude", value=45.5, minimum=-90, maximum=90 ) lon_input = gr.Number( label="Longitude", value=-111.0, minimum=-180, maximum=180 ) # Quick access buttons for Montana peaks gr.Markdown("### Quick Access - Montana Peaks") peak_buttons = [] for peak_name in MONTANA_PEAKS.keys(): peak_buttons.append(gr.Button(f"📍 {peak_name}")) submit_btn = gr.Button("Get Weather", variant="primary") with gr.Column(scale=2): map_display = gr.HTML(get_map(45.5, -111.0)) with gr.Row(): with gr.Column(scale=1): forecast_output = gr.Textbox( label="Forecast", lines=12, placeholder="Select a location or mountain peak to see the forecast..." ) with gr.Column(scale=1): snow_forecast = gr.Image( label="12-Hour Snow Forecast Animation", show_label=True, container=True, type="filepath" ) # Handle submit button click submit_btn.click( fn=update_weather, inputs=[lat_input, lon_input], outputs=[ forecast_output, snow_forecast, map_display ] ) # Handle peak button clicks for i, peak_name in enumerate(MONTANA_PEAKS.keys()): peak_buttons[i].click( fn=make_peak_click_handler(peak_name), inputs=[], outputs=[lat_input, lon_input] ).then( fn=update_weather, inputs=[lat_input, lon_input], outputs=[ forecast_output, snow_forecast, map_display ] ) gr.Markdown(""" ## Instructions 1. Use the quick access buttons to check specific Montana peaks 2. Or enter coordinates manually / click on the map 3. Click "Get Weather" to see the forecast and radar imagery **Montana Peaks Included:** - Lone Peak (Big Sky): 45°16′41″N 111°27′01″W - Sacajawea Peak: 45°53′45″N 110°58′7″W - Pioneer Mountain: 45°13′55″N 111°27′2″W **Weather Display Information:** The display rotates through three views: 1. Local Radar (KMSX - Missoula) - Coverage: Western Montana mountain regions - Updates: Every ~10 minutes 2. National Radar Loop - Shows broader weather patterns - Helps track incoming systems 3. Short Term Forecast - Predicted conditions - Weather warnings and advisories **Radar Color Legend:** - Light green: Light precipitation - Dark green/Yellow: Moderate precipitation - Red: Heavy precipitation - Pink/White: Possible snow/mixed precipitation - Blue: Light snow - Dark blue: Heavy snow **Snow Quality Guide:** - Ultra-light powder: ≤ 50 kg/m³ - Light powder: 51-70 kg/m³ - Moderate powder: 71-100 kg/m³ - Packed powder: 101-150 kg/m³ - Wet snow: 151-200 kg/m³ - Dense/settled snow: > 200 kg/m³ **Note**: This app uses the NOAA Weather API and may have occasional delays or service interruptions. Mountain weather can change rapidly - always check multiple sources for safety. Snow density and quality estimates are approximate and based on temperature only. Local conditions, wind, and other factors can significantly affect actual snow conditions. """) # For Hugging Face Spaces, we need to export the demo demo.queue().launch()