/ src / lib / maps / stores / map-store.ts
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  });