Spaces:
Running
Running
| import gradio as gr | |
| import pandas as pd | |
| import requests | |
| import urllib.parse | |
| from datetime import datetime | |
| import pytz | |
| import time | |
| import folium | |
| from folium.features import DivIcon | |
| import re | |
| # 1. LOAD DATASET | |
| try: | |
| drift_df = pd.read_csv("weather_drift.csv") | |
| drift_df['Date'] = drift_df['Date'].astype(str) | |
| except Exception: | |
| drift_df = None | |
| def extract_drift(target_date): | |
| if drift_df is not None: | |
| match = drift_df[drift_df['Date'] == target_date] | |
| if not match.empty: | |
| return float(match.iloc[0]['Drift_Fahrenheit']) | |
| return 0.0 | |
| # 2. TIMEZONE ENGINE (Deep Navy, No Black) | |
| def get_timezone_string(): | |
| fmt = "%H:%M" | |
| pt = datetime.now(pytz.timezone('America/Los_Angeles')).strftime(fmt) | |
| mt = datetime.now(pytz.timezone('America/Denver')).strftime(fmt) | |
| ct = datetime.now(pytz.timezone('America/Chicago')).strftime(fmt) | |
| et = datetime.now(pytz.timezone('America/New_York')).strftime(fmt) | |
| return f"<div style='text-align: right; font-size: 14px; font-weight: 800; color: #1e3a8a; font-family: Arial, sans-serif;'>PT: {pt} | MT: {mt} | CT: {ct} | ET: {et}</div>" | |
| # 3. DUAL GEOCODING (ZIP + CITY) | |
| def geocode_query(query): | |
| query = str(query).strip() | |
| if re.match(r'^\d{5}$', query): | |
| try: | |
| resp = requests.get(f"https://api.zippopotam.us/us/{query}", timeout=3).json() | |
| if "places" in resp: | |
| return float(resp["places"][0]["latitude"]), float(resp["places"][0]["longitude"]), resp["places"][0]["place name"] | |
| except: pass | |
| try: | |
| safe_query = urllib.parse.quote(query.split(',')[0].strip()) | |
| resp = requests.get(f"https://geocoding-api.open-meteo.com/v1/search?name={safe_query}&count=1&format=json", timeout=3).json() | |
| if resp.get("results"): | |
| return resp["results"][0]["latitude"], resp["results"][0]["longitude"], resp["results"][0]["name"] | |
| except: pass | |
| return None, None, None | |
| def get_real_city_name(lat, lon): | |
| try: | |
| resp = requests.get(f"https://api.bigdatacloud.net/data/reverse-geocode-client?latitude={lat}&longitude={lon}&localityLanguage=en", timeout=2).json() | |
| return resp.get('city') or resp.get('locality') if resp.get('countryCode') else None | |
| except: return None | |
| # 4. WEATHER MAP ENGINE (100% OPAQUE, HIGH CONTRAST) | |
| def generate_folium_map(lat, lon, date_str, base_max, base_min, wind_val, show_wind=False, loc_name="Target"): | |
| if lat is None: return folium.Map(location=[39.8, -98.5], zoom_start=4, tiles="CartoDB positron")._repr_html_() | |
| m = folium.Map(location=[lat, lon], zoom_start=8, tiles="CartoDB positron") | |
| folium.raster_layers.TileLayer( | |
| tiles='https://mesonet.agron.iastate.edu/cache/tile.py/1.0.0/nexrad-n0q-900913/{z}/{x}/{y}.png', | |
| attr='NEXRAD', overlay=True, opacity=0.4 | |
| ).add_to(m) | |
| valid_points = [{"lat": lat, "lon": lon, "name": loc_name}] | |
| offsets = [(0.4, 0), (-0.4, 0), (0, 0.5), (0, -0.5)] | |
| for dlat, dlon in offsets: | |
| plat, plon = lat + dlat, lon + dlon | |
| real_name = get_real_city_name(plat, plon) | |
| if real_name and not any(p['name'] == real_name for p in valid_points): | |
| valid_points.append({"lat": plat, "lon": plon, "name": real_name}) | |
| drift = extract_drift(date_str) | |
| for pt in valid_points: | |
| var = (pt['lat']-lat)*2 - (pt['lon']-lon)*1.5 | |
| hi, lo = round(base_max+drift+var), round(base_min+drift+var) | |
| wind = f" <span style='color:#475569; font-size:12px;'>💨{round(wind_val+abs(var))}mph</span>" if show_wind else "" | |
| # Solid White Background, Deep Navy Border, High-Contrast Red & Blue | |
| label_html = f""" | |
| <div style="background-color: #ffffff; border: 2px solid #1e3a8a; border-radius: 6px; padding: 6px 12px; text-align: center; font-family: Arial, sans-serif; box-shadow: 0 4px 8px rgba(30,58,138,0.2); white-space: nowrap;"> | |
| <span style='font-size:14px; font-weight:900; color:#1e3a8a;'>{pt['name'].upper()}:</span> | |
| <span style='font-size:16px; font-weight:900;'> | |
| <span style='color:#cc0000;'>{hi}°</span> | |
| <span style='color:#94a3b8;'>/</span> | |
| <span style='color:#0055cc;'>{lo}°</span> | |
| </span>{wind} | |
| </div>""" | |
| folium.Marker([pt['lat'], pt['lon']], icon=DivIcon(icon_size=(220,40), icon_anchor=(110,20), html=label_html)).add_to(m) | |
| return m._repr_html_() | |
| # 5. CORE ENGINE (RENAMED TO "ACTUAL FORECAST") | |
| def process_search(query): | |
| lat, lon, name = geocode_query(query) | |
| if not lat: yield "", pd.DataFrame(), get_timezone_string(); return | |
| resp = requests.get(f"https://api.open-meteo.com/v1/forecast?latitude={lat}&longitude={lon}&daily=temperature_2m_max,temperature_2m_min,windspeed_10m_max&temperature_unit=fahrenheit&timezone=auto").json() | |
| dates, maxs, mins, winds = resp["daily"]["time"], resp["daily"]["temperature_2m_max"], resp["daily"]["temperature_2m_min"], resp["daily"]["windspeed_10m_max"] | |
| results = [] | |
| for i in range(min(len(dates), 7)): | |
| d = extract_drift(dates[i]) | |
| results.append({ | |
| "Date": dates[i], | |
| "Aqua Forecast": f"{round(maxs[i]+d)}°F / {round(mins[i]+d)}°F", | |
| "Actual Forecast": f"{round(maxs[i])}°F / {round(mins[i])}°F", | |
| "Drift Correction": f"{d}°F" | |
| }) | |
| df = pd.DataFrame(results) | |
| yield generate_folium_map(lat, lon, dates[0], maxs[0], mins[0], winds[0], False, name), df, get_timezone_string() | |
| time.sleep(4) | |
| yield generate_folium_map(lat, lon, dates[0], maxs[0], mins[0], winds[0], True, name), df, get_timezone_string() | |
| def on_load(request: gr.Request): | |
| default = "88220" | |
| for m, d, t in process_search(default): yield default, m, d, t | |
| # 6. PROFESSIONAL UI CSS | |
| custom_css = """ | |
| body, .gradio-container { background-color: #f1f5f9 !important; color: #1e3a8a !important; font-family: Arial, sans-serif !important; } | |
| #title_row { border-bottom: 2px solid #cbd5e1; padding-bottom: 15px; margin-bottom: 20px; align-items: center; } | |
| #title_text { font-size: 26px !important; font-weight: 900 !important; color: #1e3a8a !important; } | |
| /* Thick, prominent search bar */ | |
| #search_box input { border: 2px solid #1e3a8a !important; border-radius: 6px !important; font-weight: 800 !important; color: #1e3a8a !important; background: #ffffff !important; font-size: 16px !important; padding: 10px !important; } | |
| /* Clean Map Border */ | |
| #main_map { border: 2px solid #1e3a8a !important; border-radius: 8px; overflow: hidden; height: 500px; box-shadow: 0 4px 12px rgba(30,58,138,0.1); } | |
| #main_map iframe { width: 100%; height: 500px; border: none; } | |
| #forecast_title { font-size: 20px !important; font-weight: 900 !important; color: #1e3a8a !important; margin-top: 25px; margin-bottom: 10px; } | |
| /* High Contrast Data Table */ | |
| table { border: 2px solid #1e3a8a !important; background: #ffffff !important; border-radius: 6px !important; overflow: hidden; } | |
| th { background: #e2e8f0 !important; color: #1e3a8a !important; border-bottom: 2px solid #1e3a8a !important; font-weight: 900 !important; text-transform: uppercase; font-size: 14px !important; padding: 12px !important; } | |
| td { border-bottom: 1px solid #cbd5e1 !important; font-weight: 800 !important; color: #1e3a8a !important; padding: 12px !important; } | |
| """ | |
| with gr.Blocks(css=custom_css) as demo: | |
| with gr.Row(elem_id="title_row"): | |
| gr.HTML("<div id='title_text'>🌍 Aqua Forecast</div>", scale=2) | |
| loc_input = gr.Textbox(placeholder="Enter ZIP or City...", show_label=False, elem_id="search_box", scale=2) | |
| tz_display = gr.HTML(scale=2) | |
| spatial_map = gr.HTML(elem_id="main_map") | |
| gr.HTML("<div id='forecast_title'>📅 7-Day Forecast</div>") | |
| main_table = gr.Dataframe(headers=["Date", "Aqua Forecast", "Actual Forecast", "Drift Correction"], interactive=False) | |
| demo.load(on_load, None, [loc_input, spatial_map, main_table, tz_display]) | |
| loc_input.submit(process_search, loc_input, [spatial_map, main_table, tz_display]) | |
| demo.launch() | |