201 lines
6.8 KiB
Python
201 lines
6.8 KiB
Python
|
|
"""
|
|||
|
|
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()
|
|||
|
|
)
|