Files
CA/backend/routers/alerts.py

201 lines
6.8 KiB
Python
Raw Normal View History

"""
Router for CBPOA alert management endpoints
Generates alerts from high-risk grids in GeoJSON files
"""
from fastapi import APIRouter, HTTPException, Query
from datetime import datetime
from typing import List
import json
from config import DATA_DIR, ALERT_P1_RISK, ALERT_P2_RISK, WUHAN_BOUNDS, LAT_STEP, LON_STEP, MAX_ALERTS
from models import Alert, AlertResponse
from utils.date_helpers import get_latest_date, validate_date_format
from utils.risk import risk_value_to_level
router = APIRouter(prefix="/api/alerts", tags=["alerts"])
def lat_lon_to_grid_id(lat: float, lon: float) -> str:
"""Convert lat/lon to 100m grid cell ID in r{row}_c{col} format."""
row = int((lat - WUHAN_BOUNDS["min_lat"]) / LAT_STEP)
col = int((lon - WUHAN_BOUNDS["min_lon"]) / LON_STEP)
return f"r{row}_c{col}"
def grid_id_to_center(grid_id: str) -> tuple[float, float]:
"""Convert r{row}_c{col} grid ID back to center lat/lon."""
parts = grid_id.split("_")
row = int(parts[0][1:])
col = int(parts[1][1:])
lat = WUHAN_BOUNDS["min_lat"] + (row + 0.5) * LAT_STEP
lon = WUHAN_BOUNDS["min_lon"] + (col + 0.5) * LON_STEP
return lat, lon
def generate_alerts_for_date(date: str) -> List[Alert]:
"""Generate alerts for high-risk grids on a specific date.
Phase 1: iterate features, aggregate max risk per 100m grid cell.
Phase 2: build Alert objects from aggregated grid cells.
Phase 3: sort by (priority, -risk_value), cap at MAX_ALERTS.
"""
filepath = DATA_DIR / f"risk_{date}.geojson"
if not filepath.exists():
raise HTTPException(status_code=404, detail=f"No data found for date {date}")
with open(filepath, 'r', encoding='utf-8') as f:
geojson = json.load(f)
# Phase 1: aggregate by 100m grid cell, taking max risk per cell
grid_cells: dict[str, dict] = {}
for feature in geojson.get("features", []):
props = feature.get("properties", {})
risk_1d = props.get("risk_1d", 0)
risk_3d = props.get("risk_3d", 0)
risk_7d = props.get("risk_7d", 0)
if risk_1d < ALERT_P2_RISK and risk_3d < ALERT_P2_RISK:
continue
lat = props.get("lat", 0)
lon = props.get("lon", 0)
grid_id = lat_lon_to_grid_id(lat, lon)
max_risk = max(risk_1d, risk_3d, risk_7d)
existing = grid_cells.get(grid_id)
if existing is None or max_risk > existing["max_risk"]:
grid_cells[grid_id] = {
"risk_1d": risk_1d,
"risk_3d": risk_3d,
"risk_7d": risk_7d,
"max_risk": max_risk,
}
# Phase 2: build Alert objects from aggregated grid cells
alerts = []
for grid_id, data in grid_cells.items():
risk_1d = data["risk_1d"]
risk_3d = data["risk_3d"]
risk_7d = data["risk_7d"]
max_risk = data["max_risk"]
if risk_1d >= ALERT_P1_RISK or risk_3d >= ALERT_P1_RISK:
priority = "P1"
reason = f"高风险区域1天风险 {risk_1d:.2f}, 3天风险 {risk_3d:.2f}"
else:
priority = "P2"
reason = f"中高风险区域1天风险 {risk_1d:.2f}, 3天风险 {risk_3d:.2f}"
lat, lon = grid_id_to_center(grid_id)
risk_level = risk_value_to_level(max_risk)
alerts.append(
Alert(
alert_id=f"alert_{date}_{grid_id}",
grid_id=grid_id,
region="武汉市",
street=f"Grid {grid_id}",
latitude=lat,
longitude=lon,
risk_value=max_risk,
risk_level=risk_level,
priority=priority,
reason=reason,
timestamp=datetime.now().isoformat(),
forecast_time=f"{date}T00:00:00"
)
)
# Phase 3: sort by priority then descending risk, cap at MAX_ALERTS
alerts.sort(key=lambda x: (0 if x.priority == "P1" else 1, -x.risk_value))
return alerts[:MAX_ALERTS]
@router.get("", response_model=AlertResponse)
async def list_alerts(date: str | None = None, priority: str | None = None, min_risk: float | None = None):
if date is not None and not validate_date_format(date):
raise HTTPException(status_code=400, detail="Invalid date format. Use YYYYMMDD")
if date is None:
date = get_latest_date()
alerts = generate_alerts_for_date(date)
if priority:
alerts = [a for a in alerts if a.priority == priority]
if min_risk is not None:
alerts = [a for a in alerts if a.risk_value >= min_risk]
return AlertResponse(
alerts=alerts,
total=len(alerts),
timestamp=datetime.now().isoformat()
)
@router.get("/{alert_id}", response_model=Alert)
async def get_alert(alert_id: str, date: str | None = None):
if date is not None and not validate_date_format(date):
raise HTTPException(status_code=400, detail="Invalid date format. Use YYYYMMDD")
if date is None:
date = get_latest_date()
alerts = generate_alerts_for_date(date)
for alert in alerts:
if alert.alert_id == alert_id:
return alert
raise HTTPException(status_code=404, detail=f"Alert {alert_id} not found")
@router.get("/priority/p1", response_model=AlertResponse)
async def get_p1_alerts(date: str | None = None):
if date is not None and not validate_date_format(date):
raise HTTPException(status_code=400, detail="Invalid date format. Use YYYYMMDD")
if date is None:
date = get_latest_date()
alerts = generate_alerts_for_date(date)
p1_alerts = [a for a in alerts if a.priority == "P1"]
return AlertResponse(
alerts=p1_alerts,
total=len(p1_alerts),
timestamp=datetime.now().isoformat()
)
@router.get("/priority/p2", response_model=AlertResponse)
async def get_p2_alerts(date: str | None = None):
if date is not None and not validate_date_format(date):
raise HTTPException(status_code=400, detail="Invalid date format. Use YYYYMMDD")
if date is None:
date = get_latest_date()
alerts = generate_alerts_for_date(date)
p2_alerts = [a for a in alerts if a.priority == "P2"]
return AlertResponse(
alerts=p2_alerts,
total=len(p2_alerts),
timestamp=datetime.now().isoformat()
)
@router.get("/grid/{grid_id}", response_model=AlertResponse)
async def get_grid_alerts(grid_id: str, date: str | None = None):
if date is not None and not validate_date_format(date):
raise HTTPException(status_code=400, detail="Invalid date format. Use YYYYMMDD")
if date is None:
date = get_latest_date()
alerts = generate_alerts_for_date(date)
grid_alerts = [a for a in alerts if a.grid_id == grid_id]
return AlertResponse(
alerts=grid_alerts,
total=len(grid_alerts),
timestamp=datetime.now().isoformat()
)