from __future__ import annotations import asyncio from typing import Any from app.config import settings from app.models.schemas import CoverageRequest, GeoAntenna from app.services.geodesy import grid_in_circle, line_distance_m from app.services.link_calculator import LinkCalculator class CoverageService: def __init__(self, link_calculator: LinkCalculator) -> None: self.link_calculator = link_calculator async def calculate_coverage(self, request: CoverageRequest) -> dict[str, Any]: grid_points = grid_in_circle( request.origin.lat, request.origin.lng, request.radius_m, request.resolution_m, ) if len(grid_points) > settings.max_grid_points: raise ValueError( f"Grid has {len(grid_points)} points, exceeds max_grid_points={settings.max_grid_points}" ) semaphore = asyncio.Semaphore(settings.coverage_concurrency) features: list[dict[str, Any]] = [] async def evaluate_point(lat: float, lng: float) -> dict[str, Any]: async with semaphore: to_point = GeoAntenna(lat=lat, lng=lng, antenna_height_m=request.origin.antenna_height_m) result = await self.link_calculator.calculate_link( from_pt=request.origin, to_pt=to_point, frequency_mhz=request.frequency_mhz, fresnel_clearance=request.fresnel_clearance, tx_power_dbm=request.tx_power_dbm, rx_sensitivity_dbm=request.rx_sensitivity_dbm, sample_step_m=request.sample_step_m, ) covered = result.los and result.fresnel_clear and result.link_budget.status != "poor" distance = line_distance_m(request.origin.lat, request.origin.lng, lat, lng) return { "type": "Feature", "geometry": { "type": "Point", "coordinates": [round(lng, 6), round(lat, 6)], }, "properties": { "covered": covered, "los": result.los, "fresnel_clear": result.fresnel_clear, "margin_db": result.link_budget.margin_db, "status": result.link_budget.status, "distance_m": round(distance, 1), }, } tasks = [evaluate_point(lat, lng) for lat, lng in grid_points] features = await asyncio.gather(*tasks) return { "type": "FeatureCollection", "features": features, }