Files
ATLAS/backend/app/api/v1/geocercas.py
FlotillasGPS Developer 51d78bacf4 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.
2026-01-21 08:18:00 +00:00

503 lines
15 KiB
Python

"""
Endpoints para gestión de geocercas.
"""
import json
from typing import List, Optional
from fastapi import APIRouter, Depends, HTTPException, Query, status
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload
from app.core.database import get_db
from app.core.security import get_current_user
from app.models.usuario import Usuario
from app.models.geocerca import Geocerca
from app.models.vehiculo import Vehiculo
from app.schemas.geocerca import (
GeocercaCircularCreate,
GeocercaPoligonoCreate,
GeocercaUpdate,
GeocercaResponse,
GeocercaConVehiculos,
AsignarVehiculosRequest,
VerificarPuntoRequest,
VerificarPuntoResponse,
)
from app.services.geocerca_service import GeocercaService
router = APIRouter(prefix="/geocercas", tags=["Geocercas"])
@router.get("", response_model=List[GeocercaResponse])
async def listar_geocercas(
activa: Optional[bool] = None,
tipo: Optional[str] = None,
categoria: Optional[str] = None,
skip: int = Query(0, ge=0),
limit: int = Query(50, ge=1, le=100),
db: AsyncSession = Depends(get_db),
current_user: Usuario = Depends(get_current_user),
):
"""
Lista todas las geocercas.
Args:
activa: Filtrar por estado.
tipo: Filtrar por tipo (circular/poligono).
categoria: Filtrar por categoría.
skip: Registros a saltar.
limit: Límite de registros.
Returns:
Lista de geocercas.
"""
query = select(Geocerca).order_by(Geocerca.nombre)
if activa is not None:
query = query.where(Geocerca.activa == activa)
if tipo:
query = query.where(Geocerca.tipo == tipo)
if categoria:
query = query.where(Geocerca.categoria == categoria)
query = query.offset(skip).limit(limit)
result = await db.execute(query)
geocercas = result.scalars().all()
return [
GeocercaResponse(
id=g.id,
nombre=g.nombre,
descripcion=g.descripcion,
tipo=g.tipo,
color=g.color,
opacidad=g.opacidad,
color_borde=g.color_borde,
categoria=g.categoria,
centro_lat=g.centro_lat,
centro_lng=g.centro_lng,
radio_metros=g.radio_metros,
coordenadas_json=g.coordenadas_json,
alerta_entrada=g.alerta_entrada,
alerta_salida=g.alerta_salida,
velocidad_maxima=g.velocidad_maxima,
horario_json=g.horario_json,
activa=g.activa,
aplica_todos_vehiculos=g.aplica_todos_vehiculos,
creado_en=g.creado_en,
actualizado_en=g.actualizado_en,
)
for g in geocercas
]
@router.get("/geojson")
async def obtener_geocercas_geojson(
activa: bool = True,
db: AsyncSession = Depends(get_db),
current_user: Usuario = Depends(get_current_user),
):
"""
Obtiene todas las geocercas en formato GeoJSON.
Args:
activa: Solo geocercas activas.
Returns:
FeatureCollection GeoJSON.
"""
query = select(Geocerca)
if activa:
query = query.where(Geocerca.activa == True)
result = await db.execute(query)
geocercas = result.scalars().all()
features = [g.to_geojson() for g in geocercas]
return {
"type": "FeatureCollection",
"features": features,
}
@router.get("/{geocerca_id}", response_model=GeocercaConVehiculos)
async def obtener_geocerca(
geocerca_id: int,
db: AsyncSession = Depends(get_db),
current_user: Usuario = Depends(get_current_user),
):
"""
Obtiene una geocerca por su ID.
Args:
geocerca_id: ID de la geocerca.
Returns:
Geocerca con vehículos asignados.
"""
result = await db.execute(
select(Geocerca)
.options(selectinload(Geocerca.vehiculos_asignados))
.where(Geocerca.id == geocerca_id)
)
geocerca = result.scalar_one_or_none()
if not geocerca:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Geocerca con id {geocerca_id} no encontrada",
)
from app.schemas.vehiculo import VehiculoResumen
return GeocercaConVehiculos(
id=geocerca.id,
nombre=geocerca.nombre,
descripcion=geocerca.descripcion,
tipo=geocerca.tipo,
color=geocerca.color,
opacidad=geocerca.opacidad,
color_borde=geocerca.color_borde,
categoria=geocerca.categoria,
centro_lat=geocerca.centro_lat,
centro_lng=geocerca.centro_lng,
radio_metros=geocerca.radio_metros,
coordenadas_json=geocerca.coordenadas_json,
alerta_entrada=geocerca.alerta_entrada,
alerta_salida=geocerca.alerta_salida,
velocidad_maxima=geocerca.velocidad_maxima,
horario_json=geocerca.horario_json,
activa=geocerca.activa,
aplica_todos_vehiculos=geocerca.aplica_todos_vehiculos,
creado_en=geocerca.creado_en,
actualizado_en=geocerca.actualizado_en,
vehiculos_asignados=[
VehiculoResumen.model_validate(v)
for v in geocerca.vehiculos_asignados
],
)
@router.post("/circular", response_model=GeocercaResponse, status_code=status.HTTP_201_CREATED)
async def crear_geocerca_circular(
geocerca_data: GeocercaCircularCreate,
db: AsyncSession = Depends(get_db),
current_user: Usuario = Depends(get_current_user),
):
"""
Crea una geocerca circular.
Args:
geocerca_data: Datos de la geocerca.
Returns:
Geocerca creada.
"""
geocerca = Geocerca(
nombre=geocerca_data.nombre,
descripcion=geocerca_data.descripcion,
tipo="circular",
centro_lat=geocerca_data.centro_lat,
centro_lng=geocerca_data.centro_lng,
radio_metros=geocerca_data.radio_metros,
color=geocerca_data.color,
opacidad=geocerca_data.opacidad,
color_borde=geocerca_data.color_borde,
categoria=geocerca_data.categoria,
alerta_entrada=geocerca_data.alerta_entrada,
alerta_salida=geocerca_data.alerta_salida,
velocidad_maxima=geocerca_data.velocidad_maxima,
horario_json=geocerca_data.horario_json,
)
db.add(geocerca)
await db.commit()
await db.refresh(geocerca)
# Asignar vehículos si se especificaron
if geocerca_data.vehiculos_ids:
result = await db.execute(
select(Vehiculo).where(Vehiculo.id.in_(geocerca_data.vehiculos_ids))
)
vehiculos = result.scalars().all()
geocerca.vehiculos_asignados = list(vehiculos)
await db.commit()
return GeocercaResponse(
id=geocerca.id,
nombre=geocerca.nombre,
descripcion=geocerca.descripcion,
tipo=geocerca.tipo,
color=geocerca.color,
opacidad=geocerca.opacidad,
color_borde=geocerca.color_borde,
categoria=geocerca.categoria,
centro_lat=geocerca.centro_lat,
centro_lng=geocerca.centro_lng,
radio_metros=geocerca.radio_metros,
coordenadas_json=geocerca.coordenadas_json,
alerta_entrada=geocerca.alerta_entrada,
alerta_salida=geocerca.alerta_salida,
velocidad_maxima=geocerca.velocidad_maxima,
horario_json=geocerca.horario_json,
activa=geocerca.activa,
aplica_todos_vehiculos=geocerca.aplica_todos_vehiculos,
creado_en=geocerca.creado_en,
actualizado_en=geocerca.actualizado_en,
)
@router.post("/poligono", response_model=GeocercaResponse, status_code=status.HTTP_201_CREATED)
async def crear_geocerca_poligono(
geocerca_data: GeocercaPoligonoCreate,
db: AsyncSession = Depends(get_db),
current_user: Usuario = Depends(get_current_user),
):
"""
Crea una geocerca poligonal.
Args:
geocerca_data: Datos de la geocerca.
Returns:
Geocerca creada.
"""
geocerca = Geocerca(
nombre=geocerca_data.nombre,
descripcion=geocerca_data.descripcion,
tipo="poligono",
coordenadas_json=json.dumps(geocerca_data.coordenadas),
color=geocerca_data.color,
opacidad=geocerca_data.opacidad,
color_borde=geocerca_data.color_borde,
categoria=geocerca_data.categoria,
alerta_entrada=geocerca_data.alerta_entrada,
alerta_salida=geocerca_data.alerta_salida,
velocidad_maxima=geocerca_data.velocidad_maxima,
horario_json=geocerca_data.horario_json,
)
db.add(geocerca)
await db.commit()
await db.refresh(geocerca)
# Asignar vehículos si se especificaron
if geocerca_data.vehiculos_ids:
result = await db.execute(
select(Vehiculo).where(Vehiculo.id.in_(geocerca_data.vehiculos_ids))
)
vehiculos = result.scalars().all()
geocerca.vehiculos_asignados = list(vehiculos)
await db.commit()
return GeocercaResponse(
id=geocerca.id,
nombre=geocerca.nombre,
descripcion=geocerca.descripcion,
tipo=geocerca.tipo,
color=geocerca.color,
opacidad=geocerca.opacidad,
color_borde=geocerca.color_borde,
categoria=geocerca.categoria,
centro_lat=geocerca.centro_lat,
centro_lng=geocerca.centro_lng,
radio_metros=geocerca.radio_metros,
coordenadas_json=geocerca.coordenadas_json,
alerta_entrada=geocerca.alerta_entrada,
alerta_salida=geocerca.alerta_salida,
velocidad_maxima=geocerca.velocidad_maxima,
horario_json=geocerca.horario_json,
activa=geocerca.activa,
aplica_todos_vehiculos=geocerca.aplica_todos_vehiculos,
creado_en=geocerca.creado_en,
actualizado_en=geocerca.actualizado_en,
)
@router.put("/{geocerca_id}", response_model=GeocercaResponse)
async def actualizar_geocerca(
geocerca_id: int,
geocerca_data: GeocercaUpdate,
db: AsyncSession = Depends(get_db),
current_user: Usuario = Depends(get_current_user),
):
"""
Actualiza una geocerca.
Args:
geocerca_id: ID de la geocerca.
geocerca_data: Datos a actualizar.
Returns:
Geocerca actualizada.
"""
result = await db.execute(
select(Geocerca).where(Geocerca.id == geocerca_id)
)
geocerca = result.scalar_one_or_none()
if not geocerca:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Geocerca con id {geocerca_id} no encontrada",
)
update_data = geocerca_data.model_dump(exclude_unset=True)
# Manejar coordenadas si es polígono
if "coordenadas" in update_data and update_data["coordenadas"]:
update_data["coordenadas_json"] = json.dumps(update_data.pop("coordenadas"))
for field, value in update_data.items():
if hasattr(geocerca, field):
setattr(geocerca, field, value)
await db.commit()
await db.refresh(geocerca)
return GeocercaResponse(
id=geocerca.id,
nombre=geocerca.nombre,
descripcion=geocerca.descripcion,
tipo=geocerca.tipo,
color=geocerca.color,
opacidad=geocerca.opacidad,
color_borde=geocerca.color_borde,
categoria=geocerca.categoria,
centro_lat=geocerca.centro_lat,
centro_lng=geocerca.centro_lng,
radio_metros=geocerca.radio_metros,
coordenadas_json=geocerca.coordenadas_json,
alerta_entrada=geocerca.alerta_entrada,
alerta_salida=geocerca.alerta_salida,
velocidad_maxima=geocerca.velocidad_maxima,
horario_json=geocerca.horario_json,
activa=geocerca.activa,
aplica_todos_vehiculos=geocerca.aplica_todos_vehiculos,
creado_en=geocerca.creado_en,
actualizado_en=geocerca.actualizado_en,
)
@router.delete("/{geocerca_id}", status_code=status.HTTP_204_NO_CONTENT)
async def eliminar_geocerca(
geocerca_id: int,
db: AsyncSession = Depends(get_db),
current_user: Usuario = Depends(get_current_user),
):
"""
Elimina una geocerca (soft delete - desactiva).
Args:
geocerca_id: ID de la geocerca.
"""
result = await db.execute(
select(Geocerca).where(Geocerca.id == geocerca_id)
)
geocerca = result.scalar_one_or_none()
if not geocerca:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Geocerca con id {geocerca_id} no encontrada",
)
geocerca.activa = False
await db.commit()
@router.post("/{geocerca_id}/vehiculos")
async def asignar_vehiculos(
geocerca_id: int,
request: AsignarVehiculosRequest,
db: AsyncSession = Depends(get_db),
current_user: Usuario = Depends(get_current_user),
):
"""
Asigna vehículos a una geocerca.
Args:
geocerca_id: ID de la geocerca.
request: Lista de IDs de vehículos.
Returns:
Confirmación.
"""
result = await db.execute(
select(Geocerca)
.options(selectinload(Geocerca.vehiculos_asignados))
.where(Geocerca.id == geocerca_id)
)
geocerca = result.scalar_one_or_none()
if not geocerca:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Geocerca con id {geocerca_id} no encontrada",
)
# Obtener vehículos
result = await db.execute(
select(Vehiculo).where(Vehiculo.id.in_(request.vehiculos_ids))
)
vehiculos = result.scalars().all()
if request.reemplazar:
geocerca.vehiculos_asignados = list(vehiculos)
else:
for v in vehiculos:
if v not in geocerca.vehiculos_asignados:
geocerca.vehiculos_asignados.append(v)
await db.commit()
return {
"message": "Vehículos asignados correctamente",
"total_asignados": len(geocerca.vehiculos_asignados),
}
@router.post("/{geocerca_id}/verificar", response_model=VerificarPuntoResponse)
async def verificar_punto(
geocerca_id: int,
request: VerificarPuntoRequest,
db: AsyncSession = Depends(get_db),
current_user: Usuario = Depends(get_current_user),
):
"""
Verifica si un punto está dentro de una geocerca.
Args:
geocerca_id: ID de la geocerca.
request: Coordenadas del punto.
Returns:
Resultado de la verificación.
"""
result = await db.execute(
select(Geocerca).where(Geocerca.id == geocerca_id)
)
geocerca = result.scalar_one_or_none()
if not geocerca:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Geocerca con id {geocerca_id} no encontrada",
)
geocerca_service = GeocercaService(db)
dentro, distancia = await geocerca_service.verificar_punto_en_geocerca(
request.lat, request.lng, geocerca_id
)
return VerificarPuntoResponse(
dentro=dentro,
geocerca_id=geocerca_id,
geocerca_nombre=geocerca.nombre,
distancia_metros=distancia,
)