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()