""" Router for geocoded case data and grid aggregated data """ from fastapi import APIRouter, HTTPException from pydantic import BaseModel from typing import List, Optional import logging from functools import lru_cache import pandas as pd from pathlib import Path logger = logging.getLogger("cbpoa.geocoded") router = APIRouter(prefix="/api/geocoded", tags=["geocoded"]) PROJECT_ROOT = Path(__file__).parent.parent.parent DATA_DIR = PROJECT_ROOT / "outputs" @lru_cache(maxsize=1) def _load_csv(path: Path) -> pd.DataFrame: return pd.read_csv(path) class GridCaseData(BaseModel): """Grid case data for visualization""" grid_id: int latitude: float longitude: float total_cases: int outpatient_cases: int inpatient_cases: int case_density: float risk_index: float risk_level: str class GridCaseResponse(BaseModel): grids: List[GridCaseData] total_count: int total_cases: int class GeocodedCaseData(BaseModel): """Individual geocoded case""" case_id: str case_type: str latitude: float longitude: float district: str street: Optional[str] geocode_method: str confidence: float class GeocodedResponse(BaseModel): cases: List[GeocodedCaseData] total_count: int @router.get("/grid", response_model=GridCaseResponse, summary="Get aggregated grid case data") async def get_grid_cases(): """ Get 100x100m grid aggregated case data for high-resolution visualization. Returns grid cells with case counts, density, and risk indices. """ grid_file = DATA_DIR / "grid_risk_summary.csv" if not grid_file.exists(): raise HTTPException(status_code=404, detail="Grid data not found") try: df = _load_csv(grid_file) grids = [] for _, row in df.iterrows(): grids.append(GridCaseData( grid_id=int(row['grid_id']), latitude=float(row['center_y']), longitude=float(row['center_x']), total_cases=int(row['total_cases']), outpatient_cases=int(row['outpatient_cases']), inpatient_cases=int(row['inpatient_cases']), case_density=float(row['cases_per_km2']), risk_index=float(row['risk_index']), risk_level=str(row['risk_level']) )) total_cases = int(df['total_cases'].sum()) return GridCaseResponse( grids=grids, total_count=len(grids), total_cases=total_cases ) except Exception as e: logger.exception("Error loading grid case data") raise HTTPException(status_code=500, detail="Internal server error") @router.get("/geocoded", response_model=GeocodedResponse, summary="Get geocoded case data") async def get_geocoded_cases( limit: int = 1000, district: Optional[str] = None, ): """ Get individual geocoded case data. Args: limit: Maximum number of cases to return (for performance) district: Filter by district name """ cases_file = DATA_DIR / "geocoded_all_cases.csv" if not cases_file.exists(): raise HTTPException(status_code=404, detail="Geocoded data not found") try: df = _load_csv(cases_file) # Drop rows with missing coordinates df = df.dropna(subset=['latitude', 'longitude']) # Fix swapped lat/lon (Wuhan: lat ~29.9-31.4, lon ~113.7-115.1) swapped = df['latitude'] > 50 # longitude values are >113 df.loc[swapped, ['latitude', 'longitude']] = df.loc[swapped, ['longitude', 'latitude']].values # Filter by district if specified if district: df = df[df['district'] == district] # Limit for performance df = df.head(limit) cases = [] for row in df.to_dict('records'): street_val = row.get('street') if pd.isna(street_val): street_val = None district_val = row.get('district', '') if pd.isna(district_val): district_val = '未知' cases.append(GeocodedCaseData( case_id=str(row['case_id']), case_type=str(row['case_type']), latitude=float(row['latitude']), longitude=float(row['longitude']), district=str(district_val), street=street_val, geocode_method=str(row.get('geocode_method', 'unknown')), confidence=float(row.get('confidence', 0) or 0) if not pd.isna(row.get('confidence')) else 0.0 )) return GeocodedResponse( cases=cases, total_count=len(cases) ) except Exception as e: logger.exception("Error loading geocoded case data") raise HTTPException(status_code=500, detail="Internal server error") @router.get("/geocoded/count", summary="Get geocoded case count") async def get_geocoded_count(): """Get total count of geocoded cases.""" cases_file = DATA_DIR / "geocoded_all_cases.csv" if not cases_file.exists(): raise HTTPException(status_code=404, detail="Geocoded data not found") try: df = _load_csv(cases_file) street_matched = len(df[df['geocode_method'] == 'street']) district_fallback = len(df[df['geocode_method'] == 'district']) return { "total": len(df), "street_matched": street_matched, "district_fallback": district_fallback, "match_rate": round(street_matched / len(df) * 100, 1) } except Exception as e: logger.exception("Error counting geocoded cases") raise HTTPException(status_code=500, detail="Internal server error")