Spaces:
Running
Running
fix event overlay stale bounds; basin ring must contain trigger gauge
Browse files- frontend/app.js +6 -2
- hf_data/eventsim.py +21 -4
frontend/app.js
CHANGED
|
@@ -2786,8 +2786,12 @@ function showEvtFrame(i) {
|
|
| 2786 |
const man = evtManifest;
|
| 2787 |
const file = i < 0 ? man.maxdepth_png : man.frames[i].png;
|
| 2788 |
const url = `${evtBase}/${man._id}/${file}`;
|
| 2789 |
-
if (evtOverlay) {
|
| 2790 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2791 |
evtOverlay = L.imageOverlay(url, man.bounds,
|
| 2792 |
{ opacity: 0.85, interactive: false }).addTo(evtGroup);
|
| 2793 |
}
|
|
|
|
| 2786 |
const man = evtManifest;
|
| 2787 |
const file = i < 0 ? man.maxdepth_png : man.frames[i].png;
|
| 2788 |
const url = `${evtBase}/${man._id}/${file}`;
|
| 2789 |
+
if (evtOverlay) {
|
| 2790 |
+
evtOverlay.setUrl(url);
|
| 2791 |
+
// bounds belong to the EVENT, not the overlay object — without this,
|
| 2792 |
+
// switching events kept drawing at the previous event's location
|
| 2793 |
+
evtOverlay.setBounds(L.latLngBounds(man.bounds));
|
| 2794 |
+
} else {
|
| 2795 |
evtOverlay = L.imageOverlay(url, man.bounds,
|
| 2796 |
{ opacity: 0.85, interactive: false }).addTo(evtGroup);
|
| 2797 |
}
|
hf_data/eventsim.py
CHANGED
|
@@ -125,12 +125,18 @@ def detect(max_events: int = 1) -> list[dict]:
|
|
| 125 |
return picks
|
| 126 |
|
| 127 |
|
| 128 |
-
def _basin_outline(ef5_dir: str, max_pts: int = 1200):
|
| 129 |
"""True upstream-catchment boundary as [[lat, lon], ...] — EF5 carves the
|
| 130 |
watershed from the gauge and writes nodata outside it in every gridded
|
| 131 |
output, so the valid-data mask of any output grid IS the basin at 3
|
| 132 |
-
arc-sec. Coarsened so the ring stays a few KB in the manifest.
|
| 133 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 134 |
import glob
|
| 135 |
|
| 136 |
import numpy as np
|
|
@@ -156,16 +162,26 @@ def _basin_outline(ef5_dir: str, max_pts: int = 1200):
|
|
| 156 |
ny, nx = m.shape[0] // f, m.shape[1] // f
|
| 157 |
m = m[:ny * f, :nx * f].reshape(ny, f, nx, f).any(axis=(1, 3))
|
| 158 |
tr = rasterio.Affine(tr.a * f, tr.b, tr.c, tr.d, tr.e * f, tr.f)
|
|
|
|
| 159 |
best, best_area = None, 0.0
|
| 160 |
for geom, val in features.shapes(m.astype("uint8"), transform=tr):
|
| 161 |
if val != 1:
|
| 162 |
continue
|
| 163 |
ring = geom["coordinates"][0]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 164 |
area = abs(sum(x1 * y2 - x2 * y1 for (x1, y1), (x2, y2)
|
| 165 |
in zip(ring, ring[1:]))) # shoelace, lon/lat units
|
| 166 |
if area > best_area:
|
| 167 |
best, best_area = ring, area
|
| 168 |
if not best:
|
|
|
|
|
|
|
| 169 |
return None
|
| 170 |
step = max(1, len(best) // max_pts)
|
| 171 |
return [[round(y, 4), round(x, 4)] for x, y in best[::step]]
|
|
@@ -260,7 +276,8 @@ def run_one(gid: str, t0: datetime.datetime | None = None,
|
|
| 260 |
manifest["hydro"] = [hydro[k] for k in sorted(hydro)]
|
| 261 |
manifest["status"] = "active"
|
| 262 |
try: # true watershed ring for the map overlay
|
| 263 |
-
manifest["basin"] = _basin_outline(
|
|
|
|
| 264 |
if manifest["basin"]:
|
| 265 |
log(f"basin outline: {len(manifest['basin'])} vertices")
|
| 266 |
except Exception as e:
|
|
|
|
| 125 |
return picks
|
| 126 |
|
| 127 |
|
| 128 |
+
def _basin_outline(ef5_dir: str, gauge_ll=None, max_pts: int = 1200, log=print):
|
| 129 |
"""True upstream-catchment boundary as [[lat, lon], ...] — EF5 carves the
|
| 130 |
watershed from the gauge and writes nodata outside it in every gridded
|
| 131 |
output, so the valid-data mask of any output grid IS the basin at 3
|
| 132 |
+
arc-sec. Coarsened so the ring stays a few KB in the manifest.
|
| 133 |
+
|
| 134 |
+
The mask can hold several disjoint blobs (and EF5's gauge snap sometimes
|
| 135 |
+
carves a NEIGHBORING catchment for small basins — seen live: rings 11-12
|
| 136 |
+
km from the gauge). So the ring must CONTAIN the trigger gauge (bbox test
|
| 137 |
+
with ~2 km slack; the outlet sits exactly on the ring). If no ring does,
|
| 138 |
+
return None rather than draw a misleading polygon — the UI falls back to
|
| 139 |
+
the pin + domain rectangle. None on any failure, too."""
|
| 140 |
import glob
|
| 141 |
|
| 142 |
import numpy as np
|
|
|
|
| 162 |
ny, nx = m.shape[0] // f, m.shape[1] // f
|
| 163 |
m = m[:ny * f, :nx * f].reshape(ny, f, nx, f).any(axis=(1, 3))
|
| 164 |
tr = rasterio.Affine(tr.a * f, tr.b, tr.c, tr.d, tr.e * f, tr.f)
|
| 165 |
+
PAD = 0.02 # ~2 km containment slack
|
| 166 |
best, best_area = None, 0.0
|
| 167 |
for geom, val in features.shapes(m.astype("uint8"), transform=tr):
|
| 168 |
if val != 1:
|
| 169 |
continue
|
| 170 |
ring = geom["coordinates"][0]
|
| 171 |
+
if gauge_ll is not None:
|
| 172 |
+
glat, glon = gauge_ll
|
| 173 |
+
lons = [p[0] for p in ring]
|
| 174 |
+
lats = [p[1] for p in ring]
|
| 175 |
+
if not (min(lats) - PAD <= glat <= max(lats) + PAD
|
| 176 |
+
and min(lons) - PAD <= glon <= max(lons) + PAD):
|
| 177 |
+
continue
|
| 178 |
area = abs(sum(x1 * y2 - x2 * y1 for (x1, y1), (x2, y2)
|
| 179 |
in zip(ring, ring[1:]))) # shoelace, lon/lat units
|
| 180 |
if area > best_area:
|
| 181 |
best, best_area = ring, area
|
| 182 |
if not best:
|
| 183 |
+
log("basin outline: no catchment ring contains the gauge "
|
| 184 |
+
"(EF5 gauge snap may have carved a neighboring basin) — skipped")
|
| 185 |
return None
|
| 186 |
step = max(1, len(best) // max_pts)
|
| 187 |
return [[round(y, 4), round(x, 4)] for x, y in best[::step]]
|
|
|
|
| 276 |
manifest["hydro"] = [hydro[k] for k in sorted(hydro)]
|
| 277 |
manifest["status"] = "active"
|
| 278 |
try: # true watershed ring for the map overlay
|
| 279 |
+
manifest["basin"] = _basin_outline(
|
| 280 |
+
out_dir, gauge_ll=(g["lat"], g["lon"]), log=log)
|
| 281 |
if manifest["basin"]:
|
| 282 |
log(f"basin outline: {len(manifest['basin'])} vertices")
|
| 283 |
except Exception as e:
|