Spaces:
Sleeping
Sleeping
| ### 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=( | |
| "<b>%{customdata[0]}</b><br>" | |
| "Altitude: %{customdata[1]} m<br>" | |
| "Distance: %{customdata[2]} km<br>" | |
| "<b>❄️ Snow: %{customdata[3]} cm</b><br>" | |
| "Weather: %{customdata[4]}<br>" | |
| "🌡 Temp: %{customdata[5]}<extra></extra>" | |
| ), | |
| ) | |
| ) | |
| # 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() | |