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"
PT: {pt} | MT: {mt} | CT: {ct} | ET: {et}
" # 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"  💨{round(wind_val+abs(var))}mph" if show_wind else "" # Solid White Background, Deep Navy Border, High-Contrast Red & Blue label_html = f"""
{pt['name'].upper()}: {hi}° / {lo}° {wind}
""" 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("
🌍 Aqua Forecast
", 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("
📅 7-Day Forecast
") 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()