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.
266 lines
6.9 KiB
TypeScript
266 lines
6.9 KiB
TypeScript
import { create } from 'zustand'
|
|
import { persist, createJSONStorage } from 'zustand/middleware'
|
|
import {
|
|
Coordenadas,
|
|
MapaEstado,
|
|
Geocerca,
|
|
POI,
|
|
VehiculoEstado,
|
|
VehiculoMovimiento,
|
|
VehiculoTipo,
|
|
} from '@/types'
|
|
|
|
interface MapaStoreState {
|
|
// Map state
|
|
centro: Coordenadas
|
|
zoom: number
|
|
bounds: { north: number; south: number; east: number; west: number } | null
|
|
|
|
// Selection
|
|
vehiculoSeleccionado: string | null
|
|
geocercaSeleccionada: string | null
|
|
poiSeleccionado: string | null
|
|
|
|
// Filters
|
|
filtros: {
|
|
estados: VehiculoEstado[]
|
|
movimientos: VehiculoMovimiento[]
|
|
tipos: VehiculoTipo[]
|
|
grupos: string[]
|
|
}
|
|
|
|
// Layers visibility
|
|
capas: {
|
|
vehiculos: boolean
|
|
geocercas: boolean
|
|
pois: boolean
|
|
trafico: boolean
|
|
rutas: boolean
|
|
labels: boolean
|
|
}
|
|
|
|
// Drawing
|
|
herramienta: 'seleccionar' | 'medir' | 'dibujar_circulo' | 'dibujar_poligono' | null
|
|
dibujando: boolean
|
|
puntosDibujo: Coordenadas[]
|
|
|
|
// Data (cached for map display)
|
|
geocercas: Geocerca[]
|
|
pois: POI[]
|
|
|
|
// Map style
|
|
estilo: 'dark' | 'light' | 'satellite'
|
|
|
|
// Following vehicle
|
|
siguiendoVehiculo: string | null
|
|
|
|
// Actions
|
|
setCentro: (centro: Coordenadas) => void
|
|
setZoom: (zoom: number) => void
|
|
setBounds: (bounds: { north: number; south: number; east: number; west: number } | null) => void
|
|
setView: (centro: Coordenadas, zoom: number) => void
|
|
fitBounds: (bounds: { north: number; south: number; east: number; west: number }) => void
|
|
|
|
setVehiculoSeleccionado: (id: string | null) => void
|
|
setGeocercaSeleccionada: (id: string | null) => void
|
|
setPoiSeleccionado: (id: string | null) => void
|
|
|
|
setFiltros: (filtros: Partial<MapaStoreState['filtros']>) => void
|
|
toggleFiltro: <K extends keyof MapaStoreState['filtros']>(
|
|
key: K,
|
|
value: MapaStoreState['filtros'][K][number]
|
|
) => void
|
|
resetFiltros: () => void
|
|
|
|
setCapa: (capa: keyof MapaStoreState['capas'], visible: boolean) => void
|
|
toggleCapa: (capa: keyof MapaStoreState['capas']) => void
|
|
|
|
setHerramienta: (herramienta: MapaStoreState['herramienta']) => void
|
|
startDibujo: () => void
|
|
addPuntoDibujo: (punto: Coordenadas) => void
|
|
removePuntoDibujo: (index: number) => void
|
|
finishDibujo: () => void
|
|
cancelDibujo: () => void
|
|
|
|
setGeocercas: (geocercas: Geocerca[]) => void
|
|
setPois: (pois: POI[]) => void
|
|
|
|
setEstilo: (estilo: MapaStoreState['estilo']) => void
|
|
|
|
setSiguiendoVehiculo: (id: string | null) => void
|
|
|
|
// Utils
|
|
centrarEnVehiculo: (lat: number, lng: number) => void
|
|
centrarEnGeocerca: (geocerca: Geocerca) => void
|
|
}
|
|
|
|
const defaultFiltros = {
|
|
estados: [] as VehiculoEstado[],
|
|
movimientos: [] as VehiculoMovimiento[],
|
|
tipos: [] as VehiculoTipo[],
|
|
grupos: [] as string[],
|
|
}
|
|
|
|
const defaultCapas = {
|
|
vehiculos: true,
|
|
geocercas: true,
|
|
pois: true,
|
|
trafico: false,
|
|
rutas: false,
|
|
labels: true,
|
|
}
|
|
|
|
// Default center (Mexico City)
|
|
const defaultCentro: Coordenadas = { lat: 19.4326, lng: -99.1332 }
|
|
|
|
export const useMapaStore = create<MapaStoreState>()(
|
|
persist(
|
|
(set, get) => ({
|
|
centro: defaultCentro,
|
|
zoom: 12,
|
|
bounds: null,
|
|
vehiculoSeleccionado: null,
|
|
geocercaSeleccionada: null,
|
|
poiSeleccionado: null,
|
|
filtros: defaultFiltros,
|
|
capas: defaultCapas,
|
|
herramienta: null,
|
|
dibujando: false,
|
|
puntosDibujo: [],
|
|
geocercas: [],
|
|
pois: [],
|
|
estilo: 'dark',
|
|
siguiendoVehiculo: null,
|
|
|
|
setCentro: (centro) => set({ centro }),
|
|
setZoom: (zoom) => set({ zoom }),
|
|
setBounds: (bounds) => set({ bounds }),
|
|
setView: (centro, zoom) => set({ centro, zoom }),
|
|
fitBounds: (bounds) => set({ bounds }),
|
|
|
|
setVehiculoSeleccionado: (id) =>
|
|
set({
|
|
vehiculoSeleccionado: id,
|
|
geocercaSeleccionada: null,
|
|
poiSeleccionado: null,
|
|
}),
|
|
|
|
setGeocercaSeleccionada: (id) =>
|
|
set({
|
|
geocercaSeleccionada: id,
|
|
vehiculoSeleccionado: null,
|
|
poiSeleccionado: null,
|
|
}),
|
|
|
|
setPoiSeleccionado: (id) =>
|
|
set({
|
|
poiSeleccionado: id,
|
|
vehiculoSeleccionado: null,
|
|
geocercaSeleccionada: null,
|
|
}),
|
|
|
|
setFiltros: (filtros) =>
|
|
set((state) => ({
|
|
filtros: { ...state.filtros, ...filtros },
|
|
})),
|
|
|
|
toggleFiltro: (key, value) =>
|
|
set((state) => {
|
|
const current = state.filtros[key] as unknown[]
|
|
const newValues = current.includes(value)
|
|
? current.filter((v) => v !== value)
|
|
: [...current, value]
|
|
return {
|
|
filtros: { ...state.filtros, [key]: newValues },
|
|
}
|
|
}),
|
|
|
|
resetFiltros: () => set({ filtros: defaultFiltros }),
|
|
|
|
setCapa: (capa, visible) =>
|
|
set((state) => ({
|
|
capas: { ...state.capas, [capa]: visible },
|
|
})),
|
|
|
|
toggleCapa: (capa) =>
|
|
set((state) => ({
|
|
capas: { ...state.capas, [capa]: !state.capas[capa] },
|
|
})),
|
|
|
|
setHerramienta: (herramienta) =>
|
|
set({
|
|
herramienta,
|
|
dibujando: false,
|
|
puntosDibujo: [],
|
|
}),
|
|
|
|
startDibujo: () => set({ dibujando: true, puntosDibujo: [] }),
|
|
|
|
addPuntoDibujo: (punto) =>
|
|
set((state) => ({
|
|
puntosDibujo: [...state.puntosDibujo, punto],
|
|
})),
|
|
|
|
removePuntoDibujo: (index) =>
|
|
set((state) => ({
|
|
puntosDibujo: state.puntosDibujo.filter((_, i) => i !== index),
|
|
})),
|
|
|
|
finishDibujo: () =>
|
|
set({
|
|
dibujando: false,
|
|
herramienta: null,
|
|
}),
|
|
|
|
cancelDibujo: () =>
|
|
set({
|
|
dibujando: false,
|
|
puntosDibujo: [],
|
|
herramienta: null,
|
|
}),
|
|
|
|
setGeocercas: (geocercas) => set({ geocercas }),
|
|
setPois: (pois) => set({ pois }),
|
|
|
|
setEstilo: (estilo) => set({ estilo }),
|
|
|
|
setSiguiendoVehiculo: (id) => set({ siguiendoVehiculo: id }),
|
|
|
|
centrarEnVehiculo: (lat, lng) =>
|
|
set({
|
|
centro: { lat, lng },
|
|
zoom: 16,
|
|
}),
|
|
|
|
centrarEnGeocerca: (geocerca) => {
|
|
if (geocerca.tipo === 'circulo' && geocerca.centroLat && geocerca.centroLng) {
|
|
set({
|
|
centro: { lat: geocerca.centroLat, lng: geocerca.centroLng },
|
|
zoom: 14,
|
|
})
|
|
} else if (geocerca.vertices && geocerca.vertices.length > 0) {
|
|
// Calculate center of polygon
|
|
const lats = geocerca.vertices.map((v) => v.lat)
|
|
const lngs = geocerca.vertices.map((v) => v.lng)
|
|
const centerLat = (Math.max(...lats) + Math.min(...lats)) / 2
|
|
const centerLng = (Math.max(...lngs) + Math.min(...lngs)) / 2
|
|
set({
|
|
centro: { lat: centerLat, lng: centerLng },
|
|
zoom: 14,
|
|
})
|
|
}
|
|
},
|
|
}),
|
|
{
|
|
name: 'flotillas-mapa',
|
|
storage: createJSONStorage(() => localStorage),
|
|
partialize: (state) => ({
|
|
centro: state.centro,
|
|
zoom: state.zoom,
|
|
capas: state.capas,
|
|
estilo: state.estilo,
|
|
}),
|
|
}
|
|
)
|
|
)
|