vincewin commited on
Commit
f68b7ff
·
verified ·
1 Parent(s): 31976e1

fix event overlay stale bounds; basin ring must contain trigger gauge

Browse files
Files changed (2) hide show
  1. frontend/app.js +6 -2
  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) { evtOverlay.setUrl(url); }
2790
- else {
 
 
 
 
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. None on
133
- any failure (the UI falls back to the domain rectangle)."""
 
 
 
 
 
 
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(out_dir)
 
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: