FlotillasGPS - Sistema completo de monitoreo de flotillas GPS
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.
This commit is contained in:
351
backend/app/services/geocerca_service.py
Normal file
351
backend/app/services/geocerca_service.py
Normal file
@@ -0,0 +1,351 @@
|
||||
"""
|
||||
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
|
||||
Reference in New Issue
Block a user