File size: 3,886 Bytes
6a82282
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""NWS station observations — latest hourly METAR for the nearest NYC airport.

api.weather.gov/stations/{id}/observations/latest.

Five NYC-region ASOS stations cover the city; we pick the nearest.
Most useful field for flood context is hourly precipitation (the
`precipitationLastHour` quantity, mm). The latest observation is
typically <60 min old.
"""
from __future__ import annotations

from dataclasses import dataclass
from math import asin, cos, radians, sin, sqrt

import httpx

DOC_ID = "nws_obs"
CITATION = "NWS station observations API (api.weather.gov/stations)"

USER_AGENT = "Riprap-NYC/0.1 (civic-flood-tool; +https://huggingface.co/spaces/msradam/riprap-nyc)"

# NYC + Hudson Corridor ASOS stations. Picker is haversine-nearest, so adding
# upstate stations enables Albany / Poughkeepsie / Newburgh queries without
# breaking NYC behaviour (NYC stations stay closer for NYC lat/lon).
STATIONS = [
    # NYC region
    ("KNYC", "Central Park, NY",         40.7794, -73.9692),
    ("KLGA", "LaGuardia Airport, NY",    40.7794, -73.8800),
    ("KJFK", "JFK Airport, NY",          40.6413, -73.7781),
    ("KEWR", "Newark Liberty, NJ",       40.6925, -74.1687),
    ("KFRG", "Republic Farmingdale, NY", 40.7288, -73.4134),
    # Hudson Corridor (south → north)
    ("KHPN", "White Plains, NY",         41.0670, -73.7076),
    ("KSWF", "Newburgh-Stewart, NY",     41.5042, -74.1048),
    ("KPOU", "Poughkeepsie, NY",         41.6262, -73.8842),
    ("KALB", "Albany Intl, NY",          42.7475, -73.8025),
]


@dataclass
class Obs:
    station_id: str
    station_name: str
    distance_km: float
    obs_time: str | None
    temp_c: float | None
    precip_last_hour_mm: float | None
    precip_last_3h_mm: float | None
    precip_last_6h_mm: float | None
    error: str | None = None


def _haversine_km(lat1, lon1, lat2, lon2) -> float:
    R = 6371.0
    p1, p2 = radians(lat1), radians(lat2)
    dp = radians(lat2 - lat1); dl = radians(lon2 - lon1)
    a = sin(dp/2)**2 + cos(p1)*cos(p2)*sin(dl/2)**2
    return 2 * R * asin(sqrt(a))


def _val_mm(props, key) -> float | None:
    """NWS returns {value: ..., unitCode: 'wmoUnit:mm'} per quantity. Convert
    to mm; if value is null, return None."""
    q = (props or {}).get(key) or {}
    v = q.get("value")
    if v is None:
        return None
    return round(float(v), 2)


def obs_at(lat: float, lon: float) -> Obs:
    sid, name, slat, slon = min(STATIONS, key=lambda s: _haversine_km(lat, lon, s[2], s[3]))
    dist_km = round(_haversine_km(lat, lon, slat, slon), 1)
    out = Obs(station_id=sid, station_name=name, distance_km=dist_km,
              obs_time=None, temp_c=None,
              precip_last_hour_mm=None, precip_last_3h_mm=None,
              precip_last_6h_mm=None)
    try:
        r = httpx.get(
            f"https://api.weather.gov/stations/{sid}/observations/latest",
            headers={"User-Agent": USER_AGENT, "Accept": "application/geo+json"},
            timeout=8.0,
        )
        r.raise_for_status()
        p = r.json().get("properties", {}) or {}
        out.obs_time = p.get("timestamp")
        out.temp_c = _val_mm(p, "temperature")
        out.precip_last_hour_mm = _val_mm(p, "precipitationLastHour")
        out.precip_last_3h_mm = _val_mm(p, "precipitationLast3Hours")
        out.precip_last_6h_mm = _val_mm(p, "precipitationLast6Hours")
    except Exception as e:
        out.error = str(e)
    return out


def summary_for_point(lat: float, lon: float) -> dict:
    o = obs_at(lat, lon)
    return {
        "station_id": o.station_id,
        "station_name": o.station_name,
        "distance_km": o.distance_km,
        "obs_time": o.obs_time,
        "temp_c": o.temp_c,
        "precip_last_hour_mm": o.precip_last_hour_mm,
        "precip_last_3h_mm": o.precip_last_3h_mm,
        "precip_last_6h_mm": o.precip_last_6h_mm,
        "error": o.error,
    }