### app_fixed.py ### import gradio as gr import pandas as pd import numpy as np import json from datetime import datetime, timezone import geopy 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 # --- UTILS --- def compute_bbox(lat, lon, dist_km): """Compute bounding box more reliably for any location.""" # Convert km to degrees (rough approximation) # At equator: 1 degree ≈ 111 km lat_delta = dist_km / 111.0 lon_delta = dist_km / (111.0 * np.cos(np.radians(lat))) south = lat - lat_delta north = lat + lat_delta west = lon - lon_delta east = lon + lon_delta # Ensure longitude wraps properly if west < -180: west += 360 if east > 180: east -= 360 # Ensure latitude stays in valid range south = max(south, -90) north = min(north, 90) return f"{south},{west},{north},{east}" def get_elevation_from_srtm(lat, lon): """Get elevation from SRTM if within coverage area.""" if lat is None or lon is None: return None # SRTM coverage: 60°N to 56°S if -56 <= lat <= 60: try: alt = elevation_data.get_elevation(lat, lon) if alt is not None and alt > 0: return alt except Exception as ex: print(f"SRTM error for {lat},{lon}: {ex}") return None def get_peaks_from_overpass(lat, lon, dist_km): """Query Overpass API for nearby peaks and hills.""" 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() peaks = {"name": [], "latitude": [], "longitude": [], "altitude": []} skipped = 0 processed = 0 max_peaks = 100 # Limit processing to avoid slowdowns for e in data.get("elements", []): # Stop if we've processed enough peaks if processed >= max_peaks: break lat_e, lon_e = e.get("lat"), e.get("lon") # Skip elements without valid coordinates if lat_e is None or lon_e is None: skipped += 1 continue tags = e.get("tags", {}) alt = None # Strategy 1: Try to get elevation from OSM tag first ele = tags.get("ele") if ele and str(ele).replace(".", "").replace("-", "").isnumeric(): alt = float(ele) # Strategy 2: If no OSM elevation, try SRTM as fallback if alt is None or alt <= 10: alt = get_elevation_from_srtm(lat_e, lon_e) # Skip peaks if both strategies failed to produce valid elevation if alt is None or alt <= 10: skipped += 1 continue peaks["latitude"].append(lat_e) peaks["longitude"].append(lon_e) peaks["name"].append(tags.get("name", "Unnamed Peak/Hill")) peaks["altitude"].append(alt) processed += 1 if skipped > 0: print(f"Skipped {skipped} peaks without complete data (coordinates or elevation)") if processed >= max_peaks: print(f"Reached limit of {max_peaks} peaks processed") if not peaks["latitude"]: return pd.DataFrame() df = pd.DataFrame(peaks) df["altitude"] = df["altitude"].round(0).astype(int) df["distance_m"] = df.apply( lambda r: distance.distance((r["latitude"], r["longitude"]), (lat, lon)).m, axis=1 ) return df # --- WEATHER FETCH (STRING PARAMS VERSION) --- def get_weather_for_peaks_iteratively(df_peaks, min_snow_cm, max_results=20, max_requests=100): """Fetch weather for peaks with all params as strings to avoid iteration errors.""" 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 response = responses[0] hourly = response.Hourly() if hourly is None: continue idx = 0 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 = float(hourly.Variables(3).ValuesAsNumpy()[idx]) snow_depth_cm = snow_depth_m * 100 if snow_depth_cm >= min_snow_cm: results.append({ **row.to_dict(), "temp_c": temp_c, "is_day": is_day, "weather_code": weather_code, "snow_depth_m": snow_depth_m, "snow_depth_cm": int(np.round(snow_depth_cm, 0)) }) except Exception as e: print(f"Error fetching weather for {row['name']} at {row['latitude']},{row['longitude']}: {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, {}) icon_filename = info.get("icon", "") description = info.get("description", "Unknown") return ICON_URL + icon_filename, description, icon_filename 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, 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) 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_peaks = df_peaks.sort_values("distance_m").reset_index(drop=True) 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!") msg = f"🎉 Showing {len(df_final)} snowy peaks with ≥ {min_snow_cm} cm of snow." return fig, msg # --- 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() # Add white halos for peaks 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", ) ) # Add peak markers with weather info in hover (no HTML icons) 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]}" ), ) ) # Add search center with halo 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)") 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, lat_input, lon_input], outputs=[map_plot, status_output], ) if __name__ == "__main__": demo.launch()