|
| 1 | +# Model Forecast Viewer |
| 2 | + |
| 3 | +[](https://github.com/ShianMike/ModelForecast/commits/main) |
| 4 | +[](https://github.com/ShianMike/ModelForecast/stargazers) |
| 5 | +[](https://github.com/ShianMike/ModelForecast/forks) |
| 6 | +[](https://www.python.org/) |
| 7 | +[](https://react.dev/) |
| 8 | +[](https://model-forecast-omtebvnjea-uc.a.run.app) |
| 9 | +[](#license) |
| 10 | + |
| 11 | +A full-stack weather model forecast viewer that fetches real-time gridded forecast data from NOAA NOMADS, decodes GRIB2 with a pure-Python decoder, and renders interactive map overlays with wind arrows, color-coded parameters, and animation controls — inspired by Pivotal Weather and Aguacero. |
| 12 | + |
| 13 | +**Live site:** <https://model-forecast-omtebvnjea-uc.a.run.app/ModelForecast/> |
| 14 | + |
| 15 | +--- |
| 16 | + |
| 17 | +## Features |
| 18 | + |
| 19 | +### Interactive Forecast Map |
| 20 | +- **Leaflet-based map** with CartoDB dark/light basemap tiles |
| 21 | +- **Gap-free canvas overlay** rendering gridded forecast data with bilinear edge interpolation |
| 22 | +- **Wind arrows** — U/V component vectors drawn on the map when wind parameters are selected |
| 23 | +- **Opacity slider** — adjustable overlay transparency (0–100%, default 50%) |
| 24 | +- **Color bar legend** — dynamic per-parameter color scale with labeled tick marks |
| 25 | +- **Region presets** — quick-select CONUS, Northeast, Southeast, Central, West, and more |
| 26 | + |
| 27 | +### NOAA NOMADS Integration |
| 28 | +- Direct GRIB2 filter downloads from NOMADS — no API keys, no rate limits |
| 29 | +- **Per-model variable/level overrides** — handles model-specific GRIB variable names (e.g., MSLMA for HRRR/RAP pressure, different cloud cover levels for NAM) |
| 30 | +- **Automatic latest-run detection** — finds the most recent model cycle with data available |
| 31 | +- **Geographic subsetting** — downloads only the bounding box needed, not the full grid |
| 32 | +- **30-minute response cache** with 10-minute latest-run cache |
| 33 | + |
| 34 | +### Pure-Python GRIB2 Decoder |
| 35 | +- No C libraries required — works on Windows, Linux, macOS without compiled dependencies |
| 36 | +- **Grid templates:** 3.0 (regular lat/lon) and 3.30 (Lambert Conformal Conic with bilinear regridding) |
| 37 | +- **Packing templates:** 5.0 (simple packing) and 5.40 (JPEG2000) |
| 38 | +- **Sign-magnitude encoding** — correctly decodes GRIB2 scale factors (not two's complement) |
| 39 | + |
| 40 | +### Supported Models |
| 41 | + |
| 42 | +| Model | Resolution | Max Forecast | Cycles | Source | |
| 43 | +|-------|-----------|-------------|--------|--------| |
| 44 | +| **GFS** | 0.25° (~28 km) | 384 hours | 00/06/12/18Z | NOMADS `filter_gfs_0p25.pl` | |
| 45 | +| **HRRR** | 3 km | 48 hours | Every hour | NOMADS `filter_hrrr_2d.pl` | |
| 46 | +| **NAM** | 12 km | 84 hours | 00/06/12/18Z | NOMADS `filter_nam.pl` | |
| 47 | +| **RAP** | 13 km | 51 hours | Every hour | NOMADS `filter_rap.pl` | |
| 48 | + |
| 49 | +### Forecast Parameters (18) |
| 50 | + |
| 51 | +| Category | Parameters | |
| 52 | +|----------|-----------| |
| 53 | +| **Surface** | Temperature (2m), Dewpoint (2m), Relative Humidity (2m), Surface Pressure (MSLP) | |
| 54 | +| **Wind** | Wind Speed (10m), Wind Gusts (10m) | |
| 55 | +| **Precipitation** | Accumulated Precipitation, Snowfall | |
| 56 | +| **Radiation** | Shortwave Radiation, Cloud Cover | |
| 57 | +| **Convective** | CAPE, Convective Inhibition (CIN) | |
| 58 | +| **Visibility** | Surface Visibility | |
| 59 | +| **Upper-Air** | 500 hPa Geopotential Height, 850 hPa Temperature, 250/500/850 hPa Wind Speed | |
| 60 | + |
| 61 | +### Animation Controls |
| 62 | +- Play/pause/step through forecast hours |
| 63 | +- Adjustable playback speed |
| 64 | +- Timeline slider with forecast hour labels |
| 65 | +- Frame caching for smooth playback |
| 66 | + |
| 67 | +### Theme Support |
| 68 | +- Dark theme (default) and light theme toggle |
| 69 | +- Consistent styling across all components |
| 70 | + |
| 71 | +--- |
| 72 | + |
| 73 | +## Project Structure |
| 74 | + |
| 75 | +``` |
| 76 | +├── app.py # Flask entry point (CORS, Talisman, rate limits, SPA catch-all) |
| 77 | +├── gunicorn.conf.py # Gunicorn WSGI config (reads PORT from env) |
| 78 | +├── requirements.txt # Python dependencies |
| 79 | +├── Dockerfile # Multi-stage build (Node + Python) |
| 80 | +├── .github/ |
| 81 | +│ └── workflows/ |
| 82 | +│ └── deploy.yml # GitHub Actions → Cloud Run CI/CD |
| 83 | +├── forecast/ # Core data package |
| 84 | +│ ├── nomads.py # NOMADS GRIB filter client (4 models, 18 variables) |
| 85 | +│ ├── grib2.py # Pure-Python GRIB2 decoder (lat/lon + Lambert grids) |
| 86 | +│ ├── parameters.py # Parameter definitions, color scales, categories |
| 87 | +│ └── open_meteo.py # Open-Meteo client (legacy, unused for grid forecasts) |
| 88 | +├── routes/ # Flask API blueprints |
| 89 | +│ ├── __init__.py # Blueprint registry |
| 90 | +│ ├── forecast_routes.py # /api/forecast, /api/color-scale |
| 91 | +│ ├── meta.py # /api/health, /api/models, /api/parameters |
| 92 | +│ └── helpers.py # NaN-safe JSON serializer |
| 93 | +└── frontend/ # React 18 + Vite 6 |
| 94 | + ├── src/ |
| 95 | + │ ├── App.jsx # Main app, state management, frame cache |
| 96 | + │ ├── api.js # API client |
| 97 | + │ └── components/ |
| 98 | + │ ├── Sidebar.jsx # Model/parameter/region selection |
| 99 | + │ ├── Header.jsx # Top bar, theme toggle, opacity slider |
| 100 | + │ ├── ForecastMap.jsx # Leaflet map container |
| 101 | + │ ├── CanvasOverlay.jsx # Canvas grid renderer + wind arrows |
| 102 | + │ ├── ColorBar.jsx # Color scale legend |
| 103 | + │ ├── AnimationControls.jsx # Play/pause/step/speed controls |
| 104 | + │ └── ParameterPicker.jsx # Grouped parameter selector |
| 105 | + ├── public/ |
| 106 | + │ └── manifest.json # PWA manifest |
| 107 | + ├── package.json |
| 108 | + └── vite.config.js |
| 109 | +``` |
| 110 | + |
| 111 | +--- |
| 112 | + |
| 113 | +## Quick Start |
| 114 | + |
| 115 | +### Backend (Python) |
| 116 | + |
| 117 | +```bash |
| 118 | +pip install -r requirements.txt |
| 119 | +python app.py |
| 120 | +# → http://localhost:5001 |
| 121 | +``` |
| 122 | + |
| 123 | +### Frontend (React) |
| 124 | + |
| 125 | +```bash |
| 126 | +cd frontend |
| 127 | +npm install |
| 128 | +npm run dev |
| 129 | +# → http://localhost:3002 |
| 130 | +``` |
| 131 | + |
| 132 | +The Vite dev server proxies `/api` requests to the backend at `localhost:5001`. |
| 133 | + |
| 134 | +--- |
| 135 | + |
| 136 | +## Deployment |
| 137 | + |
| 138 | +| Component | Platform | Region | URL | |
| 139 | +|-----------|----------|--------|-----| |
| 140 | +| **Full-stack** | Google Cloud Run | us-central1 | `https://model-forecast-omtebvnjea-uc.a.run.app` | |
| 141 | + |
| 142 | +**Cloud Run configuration:** 512 MiB memory, 1 vCPU, max 3 instances, 300 s timeout, 2 workers × 4 threads. |
| 143 | + |
| 144 | +### CI/CD Pipeline |
| 145 | + |
| 146 | +Every push to `main` triggers a GitHub Actions workflow that: |
| 147 | + |
| 148 | +1. Authenticates to GCP via Workload Identity Federation (keyless) |
| 149 | +2. Builds a multi-stage Docker image (Node 20 frontend build + Python 3.12 runtime) |
| 150 | +3. Pushes to Artifact Registry (`us-central1-docker.pkg.dev`) |
| 151 | +4. Deploys to Cloud Run |
| 152 | + |
| 153 | +No service account keys are stored — authentication uses OIDC tokens scoped to the repository. |
| 154 | + |
| 155 | +--- |
| 156 | + |
| 157 | +## API Endpoints |
| 158 | + |
| 159 | +| Method | Endpoint | Description | |
| 160 | +|--------|----------|-------------| |
| 161 | +| `GET` | `/api/health` | Health check | |
| 162 | +| `GET` | `/api/models` | List available models with metadata (resolution, max hour, step) | |
| 163 | +| `GET` | `/api/parameters?model={m}` | Get supported parameters for a model, grouped by category | |
| 164 | +| `GET` | `/api/forecast?model={m}&variable={v}&fhour={h}&lat_min=...` | Gridded forecast data (lats, lons, values, wind U/V) | |
| 165 | +| `GET` | `/api/color-scale?cmap={name}` | Color scale stops for a parameter's colormap | |
| 166 | + |
| 167 | +### `GET /api/forecast` — Query Parameters |
| 168 | + |
| 169 | +| Parameter | Required | Example | Description | |
| 170 | +|-----------|----------|---------|-------------| |
| 171 | +| `model` | Yes | `gfs` | Model name (gfs, hrrr, nam, rap) | |
| 172 | +| `variable` | Yes | `temperature_2m` | Parameter key | |
| 173 | +| `fhour` | Yes | `6` | Forecast hour | |
| 174 | +| `lat_min` | No | `24.0` | Bounding box south edge (default: CONUS) | |
| 175 | +| `lat_max` | No | `50.0` | Bounding box north edge | |
| 176 | +| `lon_min` | No | `-125.0` | Bounding box west edge | |
| 177 | +| `lon_max` | No | `-66.0` | Bounding box east edge | |
| 178 | + |
| 179 | +--- |
| 180 | + |
| 181 | +## Security |
| 182 | + |
| 183 | +- **HTTPS:** forced in production via Flask-Talisman (HSTS 2-year preload) |
| 184 | +- **Content Security Policy:** restrictive CSP with nonce-based script-src |
| 185 | +- **CORS:** locked to production origins only (localhost allowed in development) |
| 186 | +- **Rate limiting:** Flask-Limiter — 200 req/min global, 30 req/sec burst |
| 187 | +- **Security headers:** X-Content-Type-Options, X-Frame-Options DENY, COOP, CORP, Permissions-Policy, Referrer-Policy |
| 188 | +- **Input validation:** path-traversal blocking, 16 MB request-size limit |
| 189 | +- **Workload Identity Federation:** keyless GCP auth for CI/CD — no service account keys in secrets |
| 190 | + |
| 191 | +--- |
| 192 | + |
| 193 | +## Dependencies |
| 194 | + |
| 195 | +### Python |
| 196 | + |
| 197 | +| Package | Purpose | |
| 198 | +|---------|---------| |
| 199 | +| Flask 3.1 | Web framework | |
| 200 | +| flask-cors 4.0 | CORS headers | |
| 201 | +| flask-limiter 3.5 | Rate limiting | |
| 202 | +| flask-talisman 1.1 | Security headers | |
| 203 | +| gunicorn 22.0 | WSGI server | |
| 204 | +| requests 2.31 | NOMADS HTTP client | |
| 205 | +| numpy 1.26 | Array operations | |
| 206 | +| Pillow 10.0 | Image generation for color scales | |
| 207 | + |
| 208 | +### Frontend |
| 209 | + |
| 210 | +| Package | Purpose | |
| 211 | +|---------|---------| |
| 212 | +| React 18.3 | UI framework | |
| 213 | +| Vite 6 | Build tooling | |
| 214 | +| Leaflet + React-Leaflet | Interactive maps | |
| 215 | +| D3 7.9 | Data visualization | |
| 216 | +| Recharts 3.7 | Charts | |
| 217 | +| Lucide React | SVG iconography | |
| 218 | + |
| 219 | +--- |
| 220 | + |
| 221 | +## Related Projects |
| 222 | + |
| 223 | +- **[Sounding Analysis](https://github.com/ShianMike/SoundingAnalysis)** — Upper-air sounding analysis platform with Skew-T, hodograph, 50+ parameters, risk scanner, and radar overlays |
| 224 | + |
| 225 | +--- |
| 226 | + |
| 227 | +## License |
| 228 | + |
| 229 | +This project is for educational and research purposes. |
0 commit comments