Sistema completo para monitoreo y gestion de flotas de vehiculos con: - Backend FastAPI con PostgreSQL/TimescaleDB - Frontend React con TypeScript y TailwindCSS - App movil React Native con Expo - Soporte para dispositivos GPS, Meshtastic y celulares - Video streaming en vivo con MediaMTX - Geocercas, alertas, viajes y reportes - Autenticacion JWT y WebSockets en tiempo real Documentacion completa y guias de usuario incluidas.
352 lines
10 KiB
Python
352 lines
10 KiB
Python
"""
|
|
Servicio para gestión de geocercas.
|
|
|
|
Proporciona funcionalidades para verificar si un punto está dentro
|
|
de una geocerca y calcular distancias.
|
|
"""
|
|
|
|
import json
|
|
import math
|
|
from typing import List, Optional, Tuple
|
|
|
|
from sqlalchemy import select
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from app.models.geocerca import Geocerca
|
|
|
|
|
|
class GeocercaService:
|
|
"""Servicio para operaciones con geocercas."""
|
|
|
|
def __init__(self, db: AsyncSession):
|
|
"""
|
|
Inicializa el servicio.
|
|
|
|
Args:
|
|
db: Sesión de base de datos async.
|
|
"""
|
|
self.db = db
|
|
|
|
async def verificar_punto_en_geocerca(
|
|
self,
|
|
lat: float,
|
|
lng: float,
|
|
geocerca_id: int,
|
|
) -> Tuple[bool, Optional[float]]:
|
|
"""
|
|
Verifica si un punto está dentro de una geocerca.
|
|
|
|
Args:
|
|
lat: Latitud del punto.
|
|
lng: Longitud del punto.
|
|
geocerca_id: ID de la geocerca.
|
|
|
|
Returns:
|
|
Tupla (está_dentro, distancia_al_borde_metros).
|
|
distancia es None si está dentro, o la distancia al borde si está fuera.
|
|
"""
|
|
result = await self.db.execute(
|
|
select(Geocerca).where(Geocerca.id == geocerca_id)
|
|
)
|
|
geocerca = result.scalar_one_or_none()
|
|
|
|
if not geocerca:
|
|
return False, None
|
|
|
|
if geocerca.tipo == "circular":
|
|
return self._punto_en_circulo(
|
|
lat, lng,
|
|
geocerca.centro_lat, geocerca.centro_lng,
|
|
geocerca.radio_metros
|
|
)
|
|
else:
|
|
coordenadas = json.loads(geocerca.coordenadas_json) if geocerca.coordenadas_json else []
|
|
return self._punto_en_poligono(lat, lng, coordenadas)
|
|
|
|
async def obtener_geocercas_activas_para_vehiculo(
|
|
self,
|
|
vehiculo_id: int,
|
|
) -> List[Geocerca]:
|
|
"""
|
|
Obtiene las geocercas activas aplicables a un vehículo.
|
|
|
|
Args:
|
|
vehiculo_id: ID del vehículo.
|
|
|
|
Returns:
|
|
Lista de geocercas aplicables.
|
|
"""
|
|
# Geocercas sin vehículos asignados (aplican a todos)
|
|
# o con este vehículo asignado
|
|
result = await self.db.execute(
|
|
select(Geocerca)
|
|
.where(Geocerca.activa == True)
|
|
)
|
|
todas_geocercas = result.scalars().all()
|
|
|
|
geocercas_aplicables = []
|
|
for g in todas_geocercas:
|
|
# Si no tiene vehículos asignados, aplica a todos
|
|
if not g.vehiculos_asignados:
|
|
geocercas_aplicables.append(g)
|
|
# Si tiene vehículos asignados, verificar si incluye este
|
|
elif any(v.id == vehiculo_id for v in g.vehiculos_asignados):
|
|
geocercas_aplicables.append(g)
|
|
|
|
return geocercas_aplicables
|
|
|
|
async def verificar_todas_geocercas(
|
|
self,
|
|
lat: float,
|
|
lng: float,
|
|
vehiculo_id: int,
|
|
) -> List[dict]:
|
|
"""
|
|
Verifica un punto contra todas las geocercas aplicables.
|
|
|
|
Args:
|
|
lat: Latitud del punto.
|
|
lng: Longitud del punto.
|
|
vehiculo_id: ID del vehículo.
|
|
|
|
Returns:
|
|
Lista de geocercas con información de si está dentro o fuera.
|
|
"""
|
|
geocercas = await self.obtener_geocercas_activas_para_vehiculo(vehiculo_id)
|
|
resultados = []
|
|
|
|
for g in geocercas:
|
|
if g.tipo == "circular":
|
|
dentro, distancia = self._punto_en_circulo(
|
|
lat, lng,
|
|
g.centro_lat, g.centro_lng,
|
|
g.radio_metros
|
|
)
|
|
else:
|
|
coordenadas = json.loads(g.coordenadas_json) if g.coordenadas_json else []
|
|
dentro, distancia = self._punto_en_poligono(lat, lng, coordenadas)
|
|
|
|
resultados.append({
|
|
"geocerca_id": g.id,
|
|
"geocerca_nombre": g.nombre,
|
|
"dentro": dentro,
|
|
"distancia_metros": distancia,
|
|
"alerta_entrada": g.alerta_entrada,
|
|
"alerta_salida": g.alerta_salida,
|
|
"velocidad_maxima": g.velocidad_maxima,
|
|
})
|
|
|
|
return resultados
|
|
|
|
def _punto_en_circulo(
|
|
self,
|
|
lat: float,
|
|
lng: float,
|
|
centro_lat: float,
|
|
centro_lng: float,
|
|
radio_metros: float,
|
|
) -> Tuple[bool, Optional[float]]:
|
|
"""
|
|
Verifica si un punto está dentro de un círculo.
|
|
|
|
Args:
|
|
lat, lng: Coordenadas del punto.
|
|
centro_lat, centro_lng: Centro del círculo.
|
|
radio_metros: Radio del círculo.
|
|
|
|
Returns:
|
|
(está_dentro, distancia_al_borde).
|
|
"""
|
|
distancia = self._distancia_haversine(lat, lng, centro_lat, centro_lng)
|
|
distancia_metros = distancia * 1000 # km a metros
|
|
|
|
dentro = distancia_metros <= radio_metros
|
|
|
|
if dentro:
|
|
return True, None
|
|
else:
|
|
return False, distancia_metros - radio_metros
|
|
|
|
def _punto_en_poligono(
|
|
self,
|
|
lat: float,
|
|
lng: float,
|
|
coordenadas: List[List[float]],
|
|
) -> Tuple[bool, Optional[float]]:
|
|
"""
|
|
Verifica si un punto está dentro de un polígono.
|
|
|
|
Usa el algoritmo ray casting.
|
|
|
|
Args:
|
|
lat, lng: Coordenadas del punto.
|
|
coordenadas: Lista de coordenadas [[lat, lng], ...].
|
|
|
|
Returns:
|
|
(está_dentro, distancia_al_borde).
|
|
"""
|
|
if not coordenadas or len(coordenadas) < 3:
|
|
return False, None
|
|
|
|
n = len(coordenadas)
|
|
dentro = False
|
|
|
|
j = n - 1
|
|
for i in range(n):
|
|
yi, xi = coordenadas[i][0], coordenadas[i][1]
|
|
yj, xj = coordenadas[j][0], coordenadas[j][1]
|
|
|
|
if ((yi > lat) != (yj > lat)) and (
|
|
lng < (xj - xi) * (lat - yi) / (yj - yi) + xi
|
|
):
|
|
dentro = not dentro
|
|
|
|
j = i
|
|
|
|
if dentro:
|
|
return True, None
|
|
else:
|
|
# Calcular distancia al borde más cercano
|
|
distancia_min = float('inf')
|
|
for i in range(n):
|
|
j = (i + 1) % n
|
|
d = self._distancia_punto_segmento(
|
|
lat, lng,
|
|
coordenadas[i][0], coordenadas[i][1],
|
|
coordenadas[j][0], coordenadas[j][1]
|
|
)
|
|
if d < distancia_min:
|
|
distancia_min = d
|
|
|
|
return False, distancia_min * 1000 # km a metros
|
|
|
|
def _distancia_haversine(
|
|
self,
|
|
lat1: float,
|
|
lng1: float,
|
|
lat2: float,
|
|
lng2: float,
|
|
) -> float:
|
|
"""
|
|
Calcula la distancia entre dos puntos usando Haversine.
|
|
|
|
Args:
|
|
lat1, lng1: Primer punto.
|
|
lat2, lng2: Segundo punto.
|
|
|
|
Returns:
|
|
Distancia en kilómetros.
|
|
"""
|
|
R = 6371 # Radio de la Tierra en km
|
|
|
|
lat1_rad = math.radians(lat1)
|
|
lat2_rad = math.radians(lat2)
|
|
dlat = math.radians(lat2 - lat1)
|
|
dlng = math.radians(lng2 - lng1)
|
|
|
|
a = (
|
|
math.sin(dlat / 2) ** 2
|
|
+ math.cos(lat1_rad) * math.cos(lat2_rad) * math.sin(dlng / 2) ** 2
|
|
)
|
|
c = 2 * math.asin(math.sqrt(a))
|
|
|
|
return R * c
|
|
|
|
def _distancia_punto_segmento(
|
|
self,
|
|
px: float,
|
|
py: float,
|
|
x1: float,
|
|
y1: float,
|
|
x2: float,
|
|
y2: float,
|
|
) -> float:
|
|
"""
|
|
Calcula la distancia de un punto a un segmento de línea.
|
|
|
|
Args:
|
|
px, py: Punto.
|
|
x1, y1, x2, y2: Extremos del segmento.
|
|
|
|
Returns:
|
|
Distancia en kilómetros.
|
|
"""
|
|
# Longitud del segmento al cuadrado
|
|
l2 = (x2 - x1) ** 2 + (y2 - y1) ** 2
|
|
|
|
if l2 == 0:
|
|
# El segmento es un punto
|
|
return self._distancia_haversine(px, py, x1, y1)
|
|
|
|
# Proyección del punto sobre la línea
|
|
t = max(0, min(1, ((px - x1) * (x2 - x1) + (py - y1) * (y2 - y1)) / l2))
|
|
|
|
# Punto más cercano en el segmento
|
|
proj_x = x1 + t * (x2 - x1)
|
|
proj_y = y1 + t * (y2 - y1)
|
|
|
|
return self._distancia_haversine(px, py, proj_x, proj_y)
|
|
|
|
@staticmethod
|
|
def calcular_area_poligono(coordenadas: List[List[float]]) -> float:
|
|
"""
|
|
Calcula el área de un polígono en metros cuadrados.
|
|
|
|
Args:
|
|
coordenadas: Lista de coordenadas [[lat, lng], ...].
|
|
|
|
Returns:
|
|
Área en metros cuadrados.
|
|
"""
|
|
if len(coordenadas) < 3:
|
|
return 0.0
|
|
|
|
# Usar la fórmula del topógrafo (Shoelace) con conversión a metros
|
|
n = len(coordenadas)
|
|
area = 0.0
|
|
|
|
# Factor de conversión aproximado para grados a metros
|
|
# (varía según la latitud)
|
|
lat_media = sum(c[0] for c in coordenadas) / n
|
|
m_per_deg_lat = 111132.92 - 559.82 * math.cos(2 * math.radians(lat_media))
|
|
m_per_deg_lng = 111412.84 * math.cos(math.radians(lat_media))
|
|
|
|
for i in range(n):
|
|
j = (i + 1) % n
|
|
xi = coordenadas[i][1] * m_per_deg_lng
|
|
yi = coordenadas[i][0] * m_per_deg_lat
|
|
xj = coordenadas[j][1] * m_per_deg_lng
|
|
yj = coordenadas[j][0] * m_per_deg_lat
|
|
|
|
area += xi * yj - xj * yi
|
|
|
|
return abs(area) / 2
|
|
|
|
@staticmethod
|
|
def calcular_perimetro_poligono(coordenadas: List[List[float]]) -> float:
|
|
"""
|
|
Calcula el perímetro de un polígono en metros.
|
|
|
|
Args:
|
|
coordenadas: Lista de coordenadas [[lat, lng], ...].
|
|
|
|
Returns:
|
|
Perímetro en metros.
|
|
"""
|
|
if len(coordenadas) < 2:
|
|
return 0.0
|
|
|
|
servicio = GeocercaService(None) # Solo para usar método estático
|
|
perimetro = 0.0
|
|
n = len(coordenadas)
|
|
|
|
for i in range(n):
|
|
j = (i + 1) % n
|
|
d = servicio._distancia_haversine(
|
|
coordenadas[i][0], coordenadas[i][1],
|
|
coordenadas[j][0], coordenadas[j][1]
|
|
)
|
|
perimetro += d * 1000 # km a metros
|
|
|
|
return perimetro
|