""" Archivo: geometry.py Fecha de modificación: 03/06/2026 Autor: Equipo AgroVisión Descripción: Funciones geométricas **puras** (Fase 1): distancia de muestreo terrestre (GSD) que relaciona la resolución con la geometría de vuelo del dron, densidad de siembra por hectárea y área geodésica de un polígono lon/lat. El área geodésica sirve de respaldo cuando no hay PostGIS disponible (en producción se usa `ST_Area(geom::geography)`). Acciones Principales: - Calcula GSD, densidad por hectárea y área geodésica. Estructura Interna: - `gsd_cm_per_px`: cm/píxel a partir de sensor/altura/focal/ancho. - `density_per_ha`: plantas por hectárea a partir de conteo y área. - `geodesic_area_m2`: área (m²) de un anillo [lon, lat] sobre la esfera. Entradas / Dependencias: - `math` (biblioteca estándar). Salidas / Efectos: - Ninguno; funciones puras. Ejemplo de Integración: from backend.core.geometry import gsd_cm_per_px, density_per_ha, geodesic_area_m2 """ from __future__ import annotations import math _EARTH_RADIUS_M: float = 6_378_137.0 # radio ecuatorial WGS84 SQUARE_METERS_PER_HECTARE: int = 10_000 def gsd_cm_per_px( sensor_w_mm: float, flight_h_m: float, focal_mm: float, img_w_px: float ) -> float: """ Calcula la distancia de muestreo terrestre (GSD) en centímetros por píxel. Args: sensor_w_mm (float): Ancho del sensor en milímetros. flight_h_m (float): Altura de vuelo en metros. focal_mm (float): Distancia focal en milímetros. img_w_px (float): Ancho de la imagen en píxeles. Returns: float: GSD en cm/px; 0.0 si focal o ancho son 0. """ denom = focal_mm * img_w_px if denom == 0: return 0.0 return (sensor_w_mm * flight_h_m) / denom * 100 def density_per_ha(count: int, area_m2: float) -> float: """ Calcula la densidad de plantas por hectárea. Args: count (int): Número de plantas detectadas. area_m2 (float): Área de la parcela en metros cuadrados. Returns: float: Plantas por hectárea; 0.0 si el área es 0. """ if area_m2 <= 0: return 0.0 return count / (area_m2 / SQUARE_METERS_PER_HECTARE) def geodesic_area_m2(coords: list[list[float]]) -> float: """ Calcula el área geodésica (m²) de un anillo de coordenadas [lon, lat]. Usa la fórmula estándar de área de un polígono sobre la esfera (la misma de la librería de geometría de Google Maps). El resultado es el valor absoluto, por lo que no depende del sentido de giro. Args: coords (list[list[float]]): Vértices [lon, lat] en grados (anillo cerrado o no). Returns: float: Área en metros cuadrados (>= 0). """ if len(coords) < 3: return 0.0 total = 0.0 n = len(coords) for i in range(n): lon1, lat1 = coords[i] lon2, lat2 = coords[(i + 1) % n] total += math.radians(lon2 - lon1) * ( 2 + math.sin(math.radians(lat1)) + math.sin(math.radians(lat2)) ) return abs(total * _EARTH_RADIUS_M * _EARTH_RADIUS_M / 2.0)