map-store.ts
1 /** 2 * Store centralizado para la gestión del estado del mapa 3 */ 4 import { writable, derived } from 'svelte/store'; 5 import { browser } from '$app/environment'; 6 import type { Place, MapMarker, OfflineRegion, Route, MapViewport } from '../models'; 7 8 // Now using interfaces imported from models.ts 9 10 export interface MapState { 11 // Estado visual del mapa 12 viewport: MapViewport; 13 14 // Modo offline 15 offline: boolean; 16 17 // Marcadores 18 markers: MapMarker[]; 19 selectedMarkerId: string | null; 20 21 // Ruta actual 22 currentRoute: Route | null; 23 24 // Rutas guardadas 25 savedRoutes: Route[]; 26 27 // Lugares guardados 28 savedPlaces: Place[]; 29 30 // Búsquedas recientes 31 recentSearches: { 32 query: string; 33 lat: number; 34 lng: number; 35 timestamp: number; 36 }[]; 37 38 // Regiones disponibles offline 39 availableRegions: OfflineRegion[]; 40 41 // UI state 42 showInfoPanel: boolean; 43 showSearch: boolean; 44 showRouting: boolean; 45 } 46 47 // Estado inicial 48 const initialState: MapState = { 49 viewport: { 50 center: { lat: 40.416775, lng: -3.703790 }, // Madrid 51 zoom: 13 52 }, 53 offline: true, // Por defecto en modo offline 54 markers: [], 55 selectedMarkerId: null, 56 currentRoute: null, 57 savedRoutes: [], 58 savedPlaces: [], 59 recentSearches: [], 60 availableRegions: [ 61 { 62 name: 'madrid-centro', 63 bounds: { 64 north: 40.438, 65 east: -3.670, 66 south: 40.400, 67 west: -3.725 68 }, 69 minZoom: 13, 70 maxZoom: 16, 71 tileCount: 120, 72 lastUpdated: Date.now() 73 } 74 ], 75 showInfoPanel: false, 76 showSearch: true, 77 showRouting: false 78 }; 79 80 // Cargar estado guardado 81 function loadSavedState(): MapState { 82 if (!browser) { 83 return initialState; 84 } 85 86 try { 87 const savedState = localStorage.getItem('mapState'); 88 89 if (savedState) { 90 return { 91 ...initialState, 92 ...JSON.parse(savedState) 93 }; 94 } 95 } catch (error) { 96 console.error('[mapStore] Error al cargar estado:', error); 97 } 98 99 return initialState; 100 } 101 102 // Crear store 103 function createMapStore() { 104 const state = loadSavedState(); 105 const { subscribe, update, set } = writable<MapState>(state); 106 107 // Guardar estado en localStorage cuando cambia 108 if (browser) { 109 subscribe(state => { 110 try { 111 localStorage.setItem('mapState', JSON.stringify({ 112 // Sólo guardamos lo necesario para persistir 113 offline: state.offline, 114 savedRoutes: state.savedRoutes, 115 savedPlaces: state.savedPlaces, 116 recentSearches: state.recentSearches 117 })); 118 } catch (error) { 119 console.error('[mapStore] Error al guardar estado:', error); 120 } 121 }); 122 } 123 124 return { 125 subscribe, 126 127 // Actualizar viewport 128 setViewport: (viewport: MapViewport) => { 129 update(state => ({ 130 ...state, 131 viewport: { 132 ...state.viewport, 133 ...viewport 134 } 135 })); 136 }, 137 138 // Actualizar modo offline 139 setOfflineMode: (offline: boolean) => { 140 update(state => ({ 141 ...state, 142 offline 143 })); 144 }, 145 146 // Añadir marcador 147 addMarker: (marker: Omit<MapMarker, 'id'>) => { 148 const id = `marker-${Date.now()}-${Math.floor(Math.random() * 10000)}`; 149 150 update(state => ({ 151 ...state, 152 markers: [ 153 ...state.markers, 154 { ...marker, id } 155 ] 156 })); 157 158 return id; 159 }, 160 161 // Eliminar marcador 162 removeMarker: (id: string) => { 163 update(state => ({ 164 ...state, 165 markers: state.markers.filter(m => m.id !== id) 166 })); 167 }, 168 169 // Limpiar todos los marcadores 170 clearMarkers: () => { 171 update(state => ({ 172 ...state, 173 markers: [], 174 selectedMarkerId: null 175 })); 176 }, 177 178 // Seleccionar marcador 179 selectMarker: (id: string | null) => { 180 update(state => ({ 181 ...state, 182 selectedMarkerId: id 183 })); 184 }, 185 186 // Establecer ruta actual 187 setCurrentRoute: (route: Route | null) => { 188 update(state => ({ 189 ...state, 190 currentRoute: route 191 })); 192 }, 193 194 // Guardar ruta 195 saveRoute: (route: Omit<Route, 'id' | 'createdAt'>) => { 196 const id = `route-${Date.now()}-${Math.floor(Math.random() * 10000)}`; 197 const newRoute: Route = { 198 ...route, 199 id, 200 createdAt: Date.now() 201 }; 202 203 update(state => ({ 204 ...state, 205 savedRoutes: [...state.savedRoutes, newRoute] 206 })); 207 208 return id; 209 }, 210 211 // Eliminar ruta guardada 212 removeSavedRoute: (id: string) => { 213 update(state => ({ 214 ...state, 215 savedRoutes: state.savedRoutes.filter(r => r.id !== id) 216 })); 217 }, 218 219 // Guardar lugar 220 savePlace: (place: Place) => { 221 update(state => ({ 222 ...state, 223 savedPlaces: [...state.savedPlaces, place] 224 })); 225 }, 226 227 // Eliminar lugar guardado 228 removeSavedPlace: (id: string) => { 229 update(state => ({ 230 ...state, 231 savedPlaces: state.savedPlaces.filter(p => p.id !== id) 232 })); 233 }, 234 235 // Añadir búsqueda reciente 236 addRecentSearch: (query: string, lat: number, lng: number) => { 237 update(state => { 238 // Filtrar duplicados y limitar a 5 239 const newSearches = [ 240 { query, lat, lng, timestamp: Date.now() }, 241 ...state.recentSearches 242 .filter(s => s.query !== query) 243 .slice(0, 4) 244 ]; 245 246 return { 247 ...state, 248 recentSearches: newSearches 249 }; 250 }); 251 }, 252 253 // Limpiar búsquedas recientes 254 clearRecentSearches: () => { 255 update(state => ({ 256 ...state, 257 recentSearches: [] 258 })); 259 }, 260 261 // Actualizar regiones disponibles 262 setAvailableRegions: (regions: OfflineRegion[]) => { 263 update(state => ({ 264 ...state, 265 availableRegions: regions 266 })); 267 }, 268 269 // Mostrar/ocultar panel de información 270 toggleInfoPanel: () => { 271 update(state => ({ 272 ...state, 273 showInfoPanel: !state.showInfoPanel 274 })); 275 }, 276 277 // Mostrar/ocultar búsqueda 278 toggleSearch: () => { 279 update(state => ({ 280 ...state, 281 showSearch: !state.showSearch 282 })); 283 }, 284 285 // Mostrar/ocultar routing 286 toggleRouting: () => { 287 update(state => ({ 288 ...state, 289 showRouting: !state.showRouting 290 })); 291 }, 292 293 // Restaurar configuración por defecto 294 reset: () => { 295 set(initialState); 296 } 297 }; 298 } 299 300 // Exportar store 301 export const mapStore = createMapStore(); 302 303 // Store derivado con cálculos útiles 304 export const mapMetrics = derived(mapStore, $state => { 305 // Calcular número total de marcadores 306 const markerCount = $state.markers.length; 307 308 // Calcular número de lugares guardados 309 const savedPlacesCount = $state.savedPlaces.length; 310 311 // Calcular número de rutas guardadas 312 const savedRoutesCount = $state.savedRoutes.length; 313 314 // Calcular tamaño total de tiles descargados (estimación) 315 const totalTileCount = $state.availableRegions.reduce( 316 (sum, region) => sum + region.tileCount, 317 0 318 ); 319 320 // Estimar espacio en disco (aproximado: ~20KB por tile) 321 const estimatedStorageMB = Math.round(totalTileCount * 20 / 1024); 322 323 return { 324 markerCount, 325 savedPlacesCount, 326 savedRoutesCount, 327 totalTileCount, 328 estimatedStorageMB, 329 isOffline: $state.offline 330 }; 331 });