File size: 8,098 Bytes
09f0340
 
ceca998
f19be1c
f83a6ae
 
838cae9
15b52e7
 
cf9fd0b
a5edd38
7950f1f
a5edd38
822b205
 
2499a01
822b205
 
 
 
 
 
 
2499a01
f347b00
7950f1f
f83a6ae
 
 
 
 
 
7950f1f
15b52e7
521a6b0
cf9fd0b
521a6b0
cf9fd0b
 
 
 
521a6b0
 
cf9fd0b
4b783ed
521a6b0
cf9fd0b
521a6b0
 
cf9fd0b
 
 
 
521a6b0
 
 
15b52e7
7950f1f
521a6b0
 
 
ba976b4
521a6b0
15b52e7
b09ffd7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
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()