fix(leaflet): ensure GIF loops + render via Folium or Leaflet; disable SSR so JS runs; fix(raw-grib): force Herbie download and copy into exports
Browse files- app.py +93 -57
- requirements.txt +1 -0
app.py
CHANGED
|
@@ -467,6 +467,42 @@ def add_radar_image_layer(fig: go.Figure, lat2d: np.ndarray, lon2d: np.ndarray,
|
|
| 467 |
print(f"Image layer error: {e}")
|
| 468 |
return False
|
| 469 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 470 |
def export_radar_grib(forecast_hour: int, min_dbz: float):
|
| 471 |
"""Export the HRRR radar (REFC) field to a GRIB2 file with values below min_dbz set to missing.
|
| 472 |
|
|
@@ -494,24 +530,14 @@ def export_radar_grib(forecast_hour: int, min_dbz: float):
|
|
| 494 |
thr = float(min_dbz) if min_dbz is not None else 1.0
|
| 495 |
z = np.where(z >= thr, z.astype(float), np.nan)
|
| 496 |
|
| 497 |
-
# Determine source GRIB path
|
| 498 |
src = None
|
| 499 |
-
if isinstance(info, dict):
|
| 500 |
-
src = info
|
| 501 |
if not src:
|
| 502 |
-
|
| 503 |
-
try:
|
| 504 |
-
src = ds.encoding.get('source', None)
|
| 505 |
-
except Exception:
|
| 506 |
-
pass
|
| 507 |
if not src:
|
| 508 |
-
|
| 509 |
-
enc = getattr(ds[vn], 'encoding', {})
|
| 510 |
-
src = enc.get('source', None)
|
| 511 |
-
if src:
|
| 512 |
-
break
|
| 513 |
-
if not src:
|
| 514 |
-
return None, "Could not locate the source GRIB file path."
|
| 515 |
|
| 516 |
import os
|
| 517 |
from eccodes import codes_grib_new_from_file, codes_get, codes_set, codes_set_values, codes_write, codes_release
|
|
@@ -588,18 +614,19 @@ def download_raw_grib(forecast_hour: int):
|
|
| 588 |
try:
|
| 589 |
if not HERBIE_AVAILABLE:
|
| 590 |
return None, "Herbie is not available"
|
| 591 |
-
|
| 592 |
-
|
| 593 |
-
|
| 594 |
-
|
| 595 |
-
|
| 596 |
-
|
| 597 |
-
|
| 598 |
-
|
| 599 |
-
|
| 600 |
-
|
| 601 |
-
|
| 602 |
-
|
|
|
|
| 603 |
# Fallback: attempt direct Herbie path
|
| 604 |
current_time = datetime.utcnow().replace(minute=0, second=0, microsecond=0)
|
| 605 |
for hours_back in [2, 3, 6, 12, 18]:
|
|
@@ -615,16 +642,7 @@ def download_raw_grib(forecast_hour: int):
|
|
| 615 |
local = files[0]
|
| 616 |
if not local and hasattr(H, 'fpath'):
|
| 617 |
local = H.fpath
|
| 618 |
-
|
| 619 |
-
try:
|
| 620 |
-
import shutil
|
| 621 |
-
os.makedirs('exports', exist_ok=True)
|
| 622 |
-
base = os.path.basename(str(local))
|
| 623 |
-
dest = os.path.join('exports', f"raw_{base}")
|
| 624 |
-
shutil.copy2(local, dest)
|
| 625 |
-
return dest, None
|
| 626 |
-
except Exception as e:
|
| 627 |
-
return None, f"Copy error: {e}"
|
| 628 |
except Exception:
|
| 629 |
continue
|
| 630 |
return None, "Unable to locate/download raw GRIB file"
|
|
@@ -713,25 +731,42 @@ def build_leaflet_overlay_html(gif_path: Optional[str], grid: Optional[Dict[str,
|
|
| 713 |
gif_b64 = base64.b64encode(f.read()).decode('ascii')
|
| 714 |
data_url = f"data:image/gif;base64,{gif_b64}"
|
| 715 |
|
| 716 |
-
|
| 717 |
-
|
| 718 |
-
|
| 719 |
-
|
| 720 |
-
|
| 721 |
-
|
| 722 |
-
|
| 723 |
-
|
| 724 |
-
|
| 725 |
-
|
| 726 |
-
|
| 727 |
-
|
| 728 |
-
|
| 729 |
-
|
| 730 |
-
|
| 731 |
-
|
| 732 |
-
|
| 733 |
-
|
| 734 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 735 |
except Exception as e:
|
| 736 |
return f"<div style='padding:8px;color:#900'>Leaflet overlay error: {str(e)}</div>"
|
| 737 |
|
|
@@ -1163,4 +1198,5 @@ with gr.Blocks(title="HRRR Weather + Radar") as app:
|
|
| 1163 |
)
|
| 1164 |
|
| 1165 |
if __name__ == "__main__":
|
| 1166 |
-
|
|
|
|
|
|
| 467 |
print(f"Image layer error: {e}")
|
| 468 |
return False
|
| 469 |
|
| 470 |
+
def _locate_or_download_grib(forecast_hour: int):
|
| 471 |
+
"""Return local GRIB2 path for HRRR REFC at fxx, downloading if needed."""
|
| 472 |
+
if not HERBIE_AVAILABLE:
|
| 473 |
+
return None, "Herbie is not available"
|
| 474 |
+
try:
|
| 475 |
+
current_time = datetime.utcnow().replace(minute=0, second=0, microsecond=0)
|
| 476 |
+
for hours_back in [2, 3, 6, 12, 18]:
|
| 477 |
+
try:
|
| 478 |
+
target_time = current_time - timedelta(hours=hours_back)
|
| 479 |
+
date_str = target_time.strftime('%Y-%m-%d %H:00')
|
| 480 |
+
H = Herbie(date_str, model='hrrr', product='sfc', fxx=int(forecast_hour))
|
| 481 |
+
# Ensure local file
|
| 482 |
+
local = None
|
| 483 |
+
try:
|
| 484 |
+
local = H.get_localFilePath()
|
| 485 |
+
except Exception:
|
| 486 |
+
local = None
|
| 487 |
+
if not local:
|
| 488 |
+
files = None
|
| 489 |
+
try:
|
| 490 |
+
files = H.download()
|
| 491 |
+
except Exception:
|
| 492 |
+
files = None
|
| 493 |
+
if isinstance(files, (list, tuple)) and files:
|
| 494 |
+
local = files[0]
|
| 495 |
+
if not local and hasattr(H, 'fpath'):
|
| 496 |
+
local = H.fpath
|
| 497 |
+
if local:
|
| 498 |
+
return local, None
|
| 499 |
+
except Exception as e:
|
| 500 |
+
print(f"locate/download attempt failed: {e}")
|
| 501 |
+
continue
|
| 502 |
+
return None, "Unable to locate/download GRIB file"
|
| 503 |
+
except Exception as e:
|
| 504 |
+
return None, f"Locate/download error: {e}"
|
| 505 |
+
|
| 506 |
def export_radar_grib(forecast_hour: int, min_dbz: float):
|
| 507 |
"""Export the HRRR radar (REFC) field to a GRIB2 file with values below min_dbz set to missing.
|
| 508 |
|
|
|
|
| 530 |
thr = float(min_dbz) if min_dbz is not None else 1.0
|
| 531 |
z = np.where(z >= thr, z.astype(float), np.nan)
|
| 532 |
|
| 533 |
+
# Determine or download source GRIB path
|
| 534 |
src = None
|
| 535 |
+
if isinstance(info, dict) and info.get('file') and os.path.exists(info['file']):
|
| 536 |
+
src = info['file']
|
| 537 |
if not src:
|
| 538 |
+
src, err = _locate_or_download_grib(int(forecast_hour))
|
|
|
|
|
|
|
|
|
|
|
|
|
| 539 |
if not src:
|
| 540 |
+
return None, err or "Could not obtain source GRIB file"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 541 |
|
| 542 |
import os
|
| 543 |
from eccodes import codes_grib_new_from_file, codes_get, codes_set, codes_set_values, codes_write, codes_release
|
|
|
|
| 614 |
try:
|
| 615 |
if not HERBIE_AVAILABLE:
|
| 616 |
return None, "Herbie is not available"
|
| 617 |
+
# Try immediate locate/download via Herbie
|
| 618 |
+
src_file, err = _locate_or_download_grib(int(forecast_hour))
|
| 619 |
+
if not src_file:
|
| 620 |
+
return None, err
|
| 621 |
+
try:
|
| 622 |
+
import shutil
|
| 623 |
+
os.makedirs('exports', exist_ok=True)
|
| 624 |
+
base = os.path.basename(str(src_file))
|
| 625 |
+
dest = os.path.join('exports', f"raw_{base}")
|
| 626 |
+
shutil.copy2(src_file, dest)
|
| 627 |
+
return dest, None
|
| 628 |
+
except Exception as e:
|
| 629 |
+
return None, f"Copy error: {e}"
|
| 630 |
# Fallback: attempt direct Herbie path
|
| 631 |
current_time = datetime.utcnow().replace(minute=0, second=0, microsecond=0)
|
| 632 |
for hours_back in [2, 3, 6, 12, 18]:
|
|
|
|
| 642 |
local = files[0]
|
| 643 |
if not local and hasattr(H, 'fpath'):
|
| 644 |
local = H.fpath
|
| 645 |
+
# Fallback handled above
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 646 |
except Exception:
|
| 647 |
continue
|
| 648 |
return None, "Unable to locate/download raw GRIB file"
|
|
|
|
| 731 |
gif_b64 = base64.b64encode(f.read()).decode('ascii')
|
| 732 |
data_url = f"data:image/gif;base64,{gif_b64}"
|
| 733 |
|
| 734 |
+
# Prefer Folium (self-contained HTML) and fallback to raw Leaflet
|
| 735 |
+
try:
|
| 736 |
+
import folium
|
| 737 |
+
m = folium.Map(location=[c_lat, c_lon], zoom_start=5, control_scale=True)
|
| 738 |
+
folium.TileLayer('OpenStreetMap').add_to(m)
|
| 739 |
+
folium.raster_layers.ImageOverlay(
|
| 740 |
+
image=data_url,
|
| 741 |
+
bounds=[[min_lat, min_lon], [max_lat, max_lon]],
|
| 742 |
+
opacity=0.95,
|
| 743 |
+
interactive=False,
|
| 744 |
+
cross_origin=False,
|
| 745 |
+
zindex=2,
|
| 746 |
+
).add_to(m)
|
| 747 |
+
folium.LayerControl().add_to(m)
|
| 748 |
+
html = m.get_root().render()
|
| 749 |
+
return html
|
| 750 |
+
except Exception as fe:
|
| 751 |
+
html = f"""
|
| 752 |
+
<link rel=\"stylesheet\" href=\"https://unpkg.com/leaflet@1.9.4/dist/leaflet.css\"/>
|
| 753 |
+
<div id=\"leaflet-map\" style=\"height:500px; border-radius:8px; overflow:hidden;\"></div>
|
| 754 |
+
<script src=\"https://unpkg.com/leaflet@1.9.4/dist/leaflet.js\"></script>
|
| 755 |
+
<script>
|
| 756 |
+
(function() {{
|
| 757 |
+
var map = L.map('leaflet-map', {{center: [{c_lat:.5f}, {c_lon:.5f}], zoom: 5, zoomControl: true}});
|
| 758 |
+
L.tileLayer('https://{{s}}.tile.openstreetmap.org/{{z}}/{{x}}/{{y}}.png', {{
|
| 759 |
+
maxZoom: 18,
|
| 760 |
+
attribution: '© OpenStreetMap contributors'
|
| 761 |
+
}}).addTo(map);
|
| 762 |
+
|
| 763 |
+
var bounds = [[{min_lat:.6f}, {min_lon:.6f}], [{max_lat:.6f}, {max_lon:.6f}]];
|
| 764 |
+
var overlay = L.imageOverlay('{data_url}', bounds, {{opacity: 0.95, interactive: false}}).addTo(map);
|
| 765 |
+
map.fitBounds(bounds);
|
| 766 |
+
}})();
|
| 767 |
+
</script>
|
| 768 |
+
"""
|
| 769 |
+
return html
|
| 770 |
except Exception as e:
|
| 771 |
return f"<div style='padding:8px;color:#900'>Leaflet overlay error: {str(e)}</div>"
|
| 772 |
|
|
|
|
| 1198 |
)
|
| 1199 |
|
| 1200 |
if __name__ == "__main__":
|
| 1201 |
+
# Disable SSR to allow custom JS (Leaflet/Folium) to run in gr.HTML blocks
|
| 1202 |
+
app.launch(server_name="0.0.0.0", server_port=7860, ssr_mode=False)
|
requirements.txt
CHANGED
|
@@ -8,6 +8,7 @@ metpy>=1.5.0
|
|
| 8 |
cartopy>=0.22.0
|
| 9 |
matplotlib>=3.7.0
|
| 10 |
imageio>=2.16.0
|
|
|
|
| 11 |
requests>=2.31.0
|
| 12 |
aiohttp>=3.8.0
|
| 13 |
fsspec>=2023.1.0
|
|
|
|
| 8 |
cartopy>=0.22.0
|
| 9 |
matplotlib>=3.7.0
|
| 10 |
imageio>=2.16.0
|
| 11 |
+
folium>=0.14.0
|
| 12 |
requests>=2.31.0
|
| 13 |
aiohttp>=3.8.0
|
| 14 |
fsspec>=2023.1.0
|