spanofzero's picture
Update app.py
b09ffd7 verified
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" &nbsp;<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()