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:
FlotillasGPS Developer
2026-01-21 08:18:00 +00:00
commit 51d78bacf4
248 changed files with 50171 additions and 0 deletions

View 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