from fastapi import APIRouter, HTTPException, BackgroundTasks from typing import List, Optional from pydantic import BaseModel from app.services.coverage_service import ( coverage_service, CoverageSettings, SiteParams, CoveragePoint ) router = APIRouter() class CoverageRequest(BaseModel): """Request body for coverage calculation""" sites: List[SiteParams] settings: CoverageSettings = CoverageSettings() class CoverageResponse(BaseModel): """Coverage calculation response""" points: List[CoveragePoint] count: int settings: CoverageSettings stats: dict @router.post("/calculate") async def calculate_coverage(request: CoverageRequest) -> CoverageResponse: """ Calculate RF coverage for one or more sites Returns grid of RSRP values with terrain and building effects """ if not request.sites: raise HTTPException(400, "At least one site required") if len(request.sites) > 10: raise HTTPException(400, "Maximum 10 sites per request") # Validate settings if request.settings.radius > 50000: raise HTTPException(400, "Maximum radius 50km") if request.settings.resolution < 50: raise HTTPException(400, "Minimum resolution 50m") # Calculate if len(request.sites) == 1: points = await coverage_service.calculate_coverage( request.sites[0], request.settings ) else: points = await coverage_service.calculate_multi_site_coverage( request.sites, request.settings ) # Calculate stats rsrp_values = [p.rsrp for p in points] los_count = sum(1 for p in points if p.has_los) stats = { "min_rsrp": min(rsrp_values) if rsrp_values else 0, "max_rsrp": max(rsrp_values) if rsrp_values else 0, "avg_rsrp": sum(rsrp_values) / len(rsrp_values) if rsrp_values else 0, "los_percentage": (los_count / len(points) * 100) if points else 0, "points_with_buildings": sum(1 for p in points if p.building_loss > 0), "points_with_terrain_loss": sum(1 for p in points if p.terrain_loss > 0), } return CoverageResponse( points=points, count=len(points), settings=request.settings, stats=stats ) @router.get("/buildings") async def get_buildings( min_lat: float, min_lon: float, max_lat: float, max_lon: float ): """ Get buildings in bounding box (for debugging/visualization) """ from app.services.buildings_service import buildings_service # Limit bbox size if (max_lat - min_lat) > 0.1 or (max_lon - min_lon) > 0.1: raise HTTPException(400, "Bbox too large (max 0.1 degrees)") buildings = await buildings_service.fetch_buildings( min_lat, min_lon, max_lat, max_lon ) return { "count": len(buildings), "buildings": [b.model_dump() for b in buildings] }