Flood data
Flood resilience is the number-one demo moment for water boards, councils, and highways operators. Stratumly ships two complementary Environment Agency (EA) datasets, both rendered as feature layers on the 2D map.
Two distinct things
| Layer | What it is | Updates | Geometry | Source |
|---|---|---|---|---|
EA Flood Risk Zones (flood-zones) | Static planning polygons. Zone 2 = 1-in-1000-yr fluvial / 1-in-200-yr tidal risk; Zone 3 = 1-in-100-yr fluvial. Used for planning permission decisions. | Yearly-ish, by EA hand. | Polygon. Tens of thousands nationally. | Public download from data.gov.uk. |
EA Flood Warnings (live) (flood-warnings) | Live operational alerts. "This area is flooding right now." Severity from Alert (possible) → Warning (expected) → Severe Warning (danger to life). | Multiple times per hour during events. | Polygon. ~10–500 active across UK. | Public REST API (no auth). |
The pair tells a complete story: zones say "what might flood", warnings say "what is flooding now". The intersection (a Zone-3 polygon that currently has an active Severe Warning) is the AI-driven asset-risk pitch in one map view.
Flood zones: static polygons
By default the demo seed includes 60 synthetic 5-km polygons per organisation placed at known UK flood-prone city centroids.
Real-data toggle. Drop the EA's flood-zone shapefile into your seed-data directory and re-seed. A real-flood-zones provisioner deletes the synthetic polygons per org and inserts a sample of real ones (default 200 per org). Both zipped shapefile (BNG, auto-reprojected via proj4j) and zipped GeoJSON (already WGS84) work.
Why per-org duplication: each org sees the same national data. A shared global layer would be cleaner architecturally, but the rest of the app (MVT tiles, layer panel, ACLs, public-share tokens) is org-scoped, and ~10–500 polygons × N orgs is a few MB. Trade-off is fine.
Flood warnings: live API
Every org has a system-managed flood-warnings feature class that's auto-populated and refreshed.
Architecture
EA flood-monitoring API
│ (HTTP poll every 15 min)
▼
FloodWarningsService.scheduledRefresh()
│
├─ GET /id/floods?_limit=500 ─→ list of active warnings
├─ GET /id/floodAreas/{id}/polygon ─→ per-area polygon (cached)
│
▼
For each org:
DELETE FROM features WHERE feature_class_id = <flood-warnings>
INSERT one row per warning (geometry = polygon, properties = severity, message, …)
│
▼
Martin MVT tiles serve the new rows on the next browser tile request
Why this shape
- Background poll, not on-demand proxy. Avoids EA API latency on every map page load. 15-min staleness is fine for the use case (warnings rarely change every minute, except during events when 15 min still beats the operational reality of council response times).
- Wipe-and-reinsert rather than ON CONFLICT upsert: at fewer than 500 rows national, the DELETE+INSERT cost is negligible, and we get correct behaviour on warning expiry without an explicit "no longer in force" branch.
- Polygon cache in memory. Flood-area polygons rarely change. Cached by
floodAreaID, no expiry. Worst-case stale polygon survives until next process restart, which is acceptable. - Severity preserved as both string and integer.
severityis the human label ("Severe Flood Warning") andseverity_level(1–4) is the EA's published numeric code. Frontend can colour by integer; admin reports can read the label.
Severity levels
EA's published codes:
| Level | Name | Meaning |
|---|---|---|
| 1 | Severe Flood Warning | Danger to life. |
| 2 | Flood Warning | Flooding is expected, immediate action required. |
| 3 | Flood Alert | Flooding is possible, be prepared. |
| 4 | Warning No Longer In Force | Recently de-escalated; kept on the API for ~24h. |
How updates happen
| Trigger | When | Mechanism |
|---|---|---|
| First boot | API container starts | Scheduled at 60s after Spring is up so it doesn't race migrations. |
| Routine | Every 15 min thereafter | Scheduled fixed-delay refresh. |
| Demo seed | Once after orgs are provisioned | Demo seeder calls the refresh service directly so demo data is fresh-at-seed-time. |
| Manual force | On demand | POST /api/flood-warnings/refresh (OWNER / ADMIN only). |
The seed container disables its scheduler so it doesn't race the regular API container.
Disabling
If a customer doesn't want the live layer (e.g. EA API blocked at their network egress, or non-UK org):
- Set
FLOOD_WARNINGS_ENABLED=falseon the API container; scheduled refresh stops, the layer stays empty. - Or set the layer's visibility off in the layer panel (UI-only, the data still lands in the database).
Failure modes
- EA API timeout / 5xx. Refresh logs a warning and bails. Previous data stays visible. Next 15-min tick retries.
- Polygon fetch fails for one area. That warning is skipped; the others still land. Logged per area.
- No active warnings. All flood-warnings rows are deleted. Map layer becomes empty (correct, since there genuinely is nothing to warn about right now).
Data residency note
EA flood data is Crown copyright under the Open Government Licence. Free to use commercially with attribution. Stratumly attributes on the map's basemap-attribution strip when the layer is visible.
Per-severity colour ramp
Polygons render in colours that match EA's published severity legend:
| Level | Colour | Hex |
|---|---|---|
| 1, Severe Flood Warning | Dark red | #a80000 |
| 2, Flood Warning | Red | #d13438 |
| 3, Flood Alert | Orange | #ca5010 |
| 4, Warning No Longer In Force | Grey | #909090 |
The flood-warnings feature class style carries a generic colorByProperty block:
{
"field": "severity_level",
"match": {"1": "#a80000", "2": "#d13438", "3": "#ca5010", "4": "#909090"},
"default": "#d13438"
}
The map page reads the block and emits a MapLibre match expression keyed on the property. The same pattern applies to fill colour AND outline colour so each warning is visually unified. Other layers can opt in to the same pattern just by adding the block, no per-layer code path.
On our roadmap
- Region filter per org. Every org sees every UK warning today. A water board in Cornwall doesn't need warnings in Yorkshire. Filtering by org region (org → bbox → spatial intersect) is a future chunk.
- Push, not poll. EA doesn't offer webhooks; if they ever do, we'd swap the cron for a push handler.