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:
143
backend/app/models/geocerca.py
Normal file
143
backend/app/models/geocerca.py
Normal file
@@ -0,0 +1,143 @@
|
||||
"""
|
||||
Modelo de Geocerca para delimitar zonas geográficas.
|
||||
"""
|
||||
|
||||
from sqlalchemy import (
|
||||
Boolean,
|
||||
Float,
|
||||
Index,
|
||||
Integer,
|
||||
String,
|
||||
Text,
|
||||
)
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from app.core.database import Base
|
||||
from app.models.base import TimestampMixin
|
||||
|
||||
|
||||
# Tabla de asociación para geocercas y vehículos
|
||||
from sqlalchemy import Table, Column, ForeignKey
|
||||
|
||||
geocerca_vehiculo = Table(
|
||||
"geocerca_vehiculo",
|
||||
Base.metadata,
|
||||
Column("geocerca_id", Integer, ForeignKey("geocercas.id", ondelete="CASCADE"), primary_key=True),
|
||||
Column("vehiculo_id", Integer, ForeignKey("vehiculos.id", ondelete="CASCADE"), primary_key=True),
|
||||
)
|
||||
|
||||
|
||||
class Geocerca(Base, TimestampMixin):
|
||||
"""
|
||||
Modelo de geocerca (zona geográfica delimitada).
|
||||
|
||||
Soporta dos tipos de geometría:
|
||||
- circular: definida por un punto central y radio
|
||||
- poligono: definida por una lista de coordenadas
|
||||
"""
|
||||
|
||||
__tablename__ = "geocercas"
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
|
||||
|
||||
# Identificación
|
||||
nombre: Mapped[str] = mapped_column(String(100), nullable=False)
|
||||
descripcion: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
|
||||
# Tipo de geometría
|
||||
tipo: Mapped[str] = mapped_column(
|
||||
String(20),
|
||||
default="circular",
|
||||
nullable=False,
|
||||
) # circular, poligono
|
||||
|
||||
# Para geocercas circulares
|
||||
centro_lat: Mapped[float | None] = mapped_column(Float, nullable=True)
|
||||
centro_lng: Mapped[float | None] = mapped_column(Float, nullable=True)
|
||||
radio_metros: Mapped[float | None] = mapped_column(Float, nullable=True)
|
||||
|
||||
# Para geocercas poligonales (JSON array de coordenadas)
|
||||
# Formato: [[lat1, lng1], [lat2, lng2], ...]
|
||||
coordenadas_json: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
|
||||
# Visualización
|
||||
color: Mapped[str] = mapped_column(String(7), default="#3B82F6", nullable=False)
|
||||
opacidad: Mapped[float] = mapped_column(Float, default=0.3, nullable=False)
|
||||
color_borde: Mapped[str] = mapped_column(String(7), default="#1D4ED8", nullable=False)
|
||||
|
||||
# Configuración de alertas
|
||||
alerta_entrada: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
|
||||
alerta_salida: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
|
||||
velocidad_maxima: Mapped[float | None] = mapped_column(Float, nullable=True) # km/h dentro de la geocerca
|
||||
|
||||
# Horario de activación (opcional)
|
||||
# Formato JSON: {"dias": [1,2,3,4,5], "hora_inicio": "08:00", "hora_fin": "18:00"}
|
||||
horario_json: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
|
||||
# Categoría
|
||||
categoria: Mapped[str | None] = mapped_column(String(50), nullable=True) # oficina, cliente, zona_riesgo, etc.
|
||||
|
||||
# Estado
|
||||
activa: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
|
||||
|
||||
# Vehículos asignados (many-to-many)
|
||||
# Si está vacío, aplica a todos los vehículos
|
||||
vehiculos_asignados: Mapped[list["Vehiculo"]] = relationship(
|
||||
"Vehiculo",
|
||||
secondary=geocerca_vehiculo,
|
||||
lazy="selectin",
|
||||
)
|
||||
|
||||
# Índices
|
||||
__table_args__ = (
|
||||
Index("idx_geocercas_activa", "activa"),
|
||||
Index("idx_geocercas_tipo", "tipo"),
|
||||
)
|
||||
|
||||
@property
|
||||
def aplica_todos_vehiculos(self) -> bool:
|
||||
"""Verifica si la geocerca aplica a todos los vehículos."""
|
||||
return len(self.vehiculos_asignados) == 0
|
||||
|
||||
def to_geojson(self) -> dict:
|
||||
"""Convierte la geocerca a formato GeoJSON."""
|
||||
import json
|
||||
|
||||
if self.tipo == "circular":
|
||||
return {
|
||||
"type": "Feature",
|
||||
"geometry": {
|
||||
"type": "Point",
|
||||
"coordinates": [self.centro_lng, self.centro_lat],
|
||||
},
|
||||
"properties": {
|
||||
"id": self.id,
|
||||
"nombre": self.nombre,
|
||||
"tipo": self.tipo,
|
||||
"radio_metros": self.radio_metros,
|
||||
"color": self.color,
|
||||
},
|
||||
}
|
||||
else:
|
||||
coords = json.loads(self.coordenadas_json) if self.coordenadas_json else []
|
||||
# GeoJSON usa [lng, lat], no [lat, lng]
|
||||
coords_geojson = [[c[1], c[0]] for c in coords]
|
||||
# Cerrar el polígono si no está cerrado
|
||||
if coords_geojson and coords_geojson[0] != coords_geojson[-1]:
|
||||
coords_geojson.append(coords_geojson[0])
|
||||
return {
|
||||
"type": "Feature",
|
||||
"geometry": {
|
||||
"type": "Polygon",
|
||||
"coordinates": [coords_geojson],
|
||||
},
|
||||
"properties": {
|
||||
"id": self.id,
|
||||
"nombre": self.nombre,
|
||||
"tipo": self.tipo,
|
||||
"color": self.color,
|
||||
},
|
||||
}
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<Geocerca(id={self.id}, nombre='{self.nombre}', tipo='{self.tipo}')>"
|
||||
Reference in New Issue
Block a user