import gradio as gr import pandas as pd import numpy as np import json from geopy import distance from geopy.geocoders import Nominatim import srtm import requests import requests_cache import openmeteo_requests from retry_requests import retry import plotly.graph_objects as go # --- GLOBAL SETUP --- elevation_data = srtm.get_data() with open("weather_icons_custom.json", "r") as f: icons = json.load(f) cache_session = requests_cache.CachedSession(".cache", expire_after=3600) retry_session = retry(cache_session, retries=5, backoff_factor=0.2) openmeteo = openmeteo_requests.Client(session=retry_session) geolocator = Nominatim(user_agent="snow_finder") OVERPASS_URL = "https://maps.mail.ru/osm/tools/overpass/api/interpreter" ICON_URL = "https://raw.githubusercontent.com/basmilius/weather-icons/refs/heads/dev/production/fill/svg/" DEFAULT_LAT, DEFAULT_LON = 49.6116, 6.1319 MAX_PEAKS = 100 # --- UTILS --- def compute_bbox(lat, lon, dist_km): lat_delta = dist_km / 111.0 lon_delta = dist_km / (111.0 * np.cos(np.radians(lat))) south = max(lat - lat_delta, -90) north = min(lat + lat_delta, 90) west = lon - lon_delta east = lon + lon_delta if west < -180: west += 360 if east > 180: east -= 360 return f"{south},{west},{north},{east}" def get_elevation_from_srtm(lat, lon): if lat is None or lon is None: return None if -56 <= lat <= 60: try: alt = elevation_data.get_elevation(lat, lon) if alt is not None and alt > 0: return alt except Exception: pass return None # --- PEAK FETCHING (REFACTORED LOGIC) --- def get_peaks_from_overpass(lat, lon, dist_km, min_altitude_m): bbox = compute_bbox(lat, lon, dist_km) query = f""" [out:json]; ( nwr[natural=peak]({bbox}); nwr[natural=hill]({bbox}); ); out body; """ try: r = requests.get(OVERPASS_URL, params={"data": query}, timeout=30) r.raise_for_status() data = r.json() except Exception as e: print(f"Error fetching peaks: {e}") return pd.DataFrame() rows = [] for e in data.get("elements", []): lat_e, lon_e = e.get("lat"), e.get("lon") if lat_e is None or lon_e is None: continue tags = e.get("tags", {}) alt = None ele = tags.get("ele") if ele and str(ele).replace(".", "").replace("-", "").isnumeric(): alt = float(ele) if alt is None or alt <= 0: alt = get_elevation_from_srtm(lat_e, lon_e) if alt is None or alt < min_altitude_m: continue rows.append({ "name": tags.get("name", "Unnamed Peak/Hill"), "latitude": lat_e, "longitude": lon_e, "altitude": int(round(alt, 0)), }) if not rows: return pd.DataFrame() df = pd.DataFrame(rows) df["distance_m"] = df.apply( lambda r: distance.distance( (r["latitude"], r["longitude"]), (lat, lon) ).m, axis=1 ) df = ( df.sort_values("distance_m") .head(MAX_PEAKS) .reset_index(drop=True) ) return df # --- WEATHER FETCH --- def get_weather_for_peaks_iteratively(df_peaks, min_snow_cm, max_results=20, max_requests=100): if df_peaks.empty: return pd.DataFrame() url = "https://api.open-meteo.com/v1/forecast" results, requests_made = [], 0 for _, row in df_peaks.iterrows(): if len(results) >= max_results or requests_made >= max_requests: break params = { "latitude": str(row["latitude"]), "longitude": str(row["longitude"]), "elevation": str(row["altitude"]), "hourly": "temperature_2m,is_day,weather_code,snow_depth", "forecast_days": "1", "timezone": "auto", } try: responses = openmeteo.weather_api(url, params=params) if not responses: continue hourly = responses[0].Hourly() if hourly is None: continue idx = 0 snow_depth_cm = float(hourly.Variables(3).ValuesAsNumpy()[idx]) * 100 if snow_depth_cm >= min_snow_cm: results.append({ **row.to_dict(), "temp_c": float(hourly.Variables(0).ValuesAsNumpy()[idx]), "is_day": int(hourly.Variables(1).ValuesAsNumpy()[idx]), "weather_code": int(hourly.Variables(2).ValuesAsNumpy()[idx]), "snow_depth_m": snow_depth_cm / 100, "snow_depth_cm": int(round(snow_depth_cm, 0)), }) except Exception as e: print(f"Error fetching weather for {row['name']}: {e}") requests_made += 1 return pd.DataFrame(results) # --- POST-PROCESSING --- def format_weather_data(df): if df.empty: return df def icon_mapper(row): code = str(int(row["weather_code"])) tod = "day" if row["is_day"] == 1 else "night" info = icons.get(code, {}).get(tod, {}) return ( ICON_URL + info.get("icon", ""), info.get("description", "Unknown"), info.get("icon", "") ) df[["weather_icon_url", "weather_desc", "weather_icon_name"]] = df.apply( icon_mapper, axis=1, result_type="expand" ) df["distance_km"] = (df["distance_m"] / 1000).round(1) df["temp_c_str"] = df["temp_c"].round(0).astype(int).astype(str) + "°C" return df def geocode_location(location_text): try: loc = geolocator.geocode(location_text, timeout=10) if loc: return loc.latitude, loc.longitude, f"Found: {loc.address}" return None, None, f"Location '{location_text}' not found." except Exception as e: return None, None, f"Geocoding error: {e}" # --- CORE LOGIC --- def find_snowy_peaks(min_snow_cm, radius_km, min_altitude_m, lat, lon): if lat is None or lon is None: fig = create_empty_map(DEFAULT_LAT, DEFAULT_LON) fig.update_layout(title_text="Enter valid coordinates.") return fig, "Please enter coordinates." if not (-90 <= lat <= 90 and -180 <= lon <= 180): fig = create_empty_map(DEFAULT_LAT, DEFAULT_LON) fig.update_layout(title_text="Invalid coordinates.") return fig, "Coordinates out of range." df_peaks = get_peaks_from_overpass(lat, lon, radius_km, min_altitude_m) if df_peaks.empty: fig = create_map_with_center(lat, lon) fig.update_layout(title_text=f"No peaks found within {radius_km} km.") return fig, f"No peaks found within {radius_km} km." df_weather = get_weather_for_peaks_iteratively(df_peaks, min_snow_cm) if df_weather.empty: fig = create_map_with_center(lat, lon) fig.update_layout(title_text=f"No snowy peaks ≥ {min_snow_cm} cm.") return fig, f"No peaks met the ≥ {min_snow_cm} cm snow requirement." df_final = format_weather_data(df_weather) fig = create_map_with_results(lat, lon, df_final) fig.update_layout(title_text=f"Found {len(df_final)} snowy peaks!") return fig, f"🎉 Showing {len(df_final)} snowy peaks with ≥ {min_snow_cm} cm of snow." # --- MAP HELPERS --- def create_empty_map(lat, lon): fig = go.Figure() fig.update_layout( map=dict(style="open-street-map", center={"lat": lat, "lon": lon}, zoom=8), margin={"r": 0, "t": 40, "l": 0, "b": 0}, height=1024, width=1024, ) return fig def create_map_with_center(lat, lon): fig = go.Figure( go.Scattermap( lat=[lat], lon=[lon], mode="markers", marker=dict(size=24, color="white", opacity=0.8), hoverinfo="skip", ) ) fig.add_trace( go.Scattermap( lat=[lat], lon=[lon], mode="markers", marker=dict(size=12, color="red"), text=["Search Center"], hoverinfo="text", ) ) fig.update_layout( map=dict(style="open-street-map", center={"lat": lat, "lon": lon}, zoom=8), margin={"r": 0, "t": 40, "l": 0, "b": 0}, height=1024, width=1024, ) return fig def create_map_with_results(lat, lon, df_final): fig = go.Figure() fig.add_trace( go.Scattermap( lat=df_final["latitude"], lon=df_final["longitude"], mode="markers", marker=dict(size=24, color="white", opacity=0.8), hoverinfo="skip", ) ) fig.add_trace( go.Scattermap( lat=df_final["latitude"], lon=df_final["longitude"], mode="markers", marker=dict(size=12, color="blue"), customdata=df_final[ ["name", "altitude", "distance_km", "snow_depth_cm", "weather_desc", "temp_c_str"] ], hovertemplate=( "%{customdata[0]}
" "Altitude: %{customdata[1]} m
" "Distance: %{customdata[2]} km
" "❄️ Snow: %{customdata[3]} cm
" "Weather: %{customdata[4]}
" "🌡 Temp: %{customdata[5]}" ), ) ) fig.add_trace( go.Scattermap( lat=[lat], lon=[lon], mode="markers", marker=dict(size=24, color="white", opacity=0.8), hoverinfo="skip", ) ) fig.add_trace( go.Scattermap( lat=[lat], lon=[lon], mode="markers", marker=dict(size=12, color="red"), text=["Search Center"], hoverinfo="text", ) ) fig.update_layout( map=dict(style="open-street-map", center={"lat": lat, "lon": lon}, zoom=9), margin={"r": 0, "t": 40, "l": 0, "b": 0}, height=1024, width=1024, showlegend=False, ) return fig # --- GRADIO UI --- with gr.Blocks(theme=gr.themes.Soft(), title="Snow Finder") as demo: gr.Markdown("# ☃️ Snow Finder for Families") gr.Markdown("Find nearby snowy peaks perfect for sledding and snowmen!") with gr.Row(): with gr.Column(scale=1): location_search = gr.Textbox(label="Search Location") search_location_btn = gr.Button("🔍 Find Location") lat_input = gr.Number(value=DEFAULT_LAT, label="Latitude", precision=4) lon_input = gr.Number(value=DEFAULT_LON, label="Longitude", precision=4) snow_slider = gr.Radio(choices=[1, 2, 3, 4, 5, 6], value=1, label="Min Snow (cm)") radius_slider = gr.Radio(choices=[10, 20, 30, 40, 50, 60], value=30, label="Radius (km)") altitude_slider = gr.Radio(choices=[100, 200, 300, 400, 500, 600, 700, 800, 900, 1000], value=300, label="Min Altitude (m)") search_button = gr.Button("❄️ Find Snow!", variant="primary") status_output = gr.Textbox(lines=4, interactive=False) with gr.Column(scale=2): init_fig = create_map_with_center(DEFAULT_LAT, DEFAULT_LON) init_fig.update_layout(title_text="Luxembourg City – Click 'Find Snow!' to start") map_plot = gr.Plot(init_fig, label="Map") search_location_btn.click( fn=geocode_location, inputs=[location_search], outputs=[lat_input, lon_input, status_output], ) search_button.click( fn=find_snowy_peaks, inputs=[snow_slider, radius_slider, altitude_slider, lat_input, lon_input], outputs=[map_plot, status_output], ) if __name__ == "__main__": demo.launch()