/ src / lib / maps / components / OfflineMap.svelte
OfflineMap.svelte
  1  <script lang="ts">
  2    import { onMount, onDestroy } from 'svelte';
  3    import { mapStore } from '../stores/map-store';
  4    import { browser } from '$app/environment';
  5    import type { MapViewport } from '../models';
  6    
  7    // Props
  8    export let height = '400px';
  9    export let mapId = 'map-container';
 10    export let initialCenter: { lat: number; lng: number } = { lat: 40.416775, lng: -3.703790 }; // Madrid por defecto
 11    export let initialZoom = 13;
 12    export let markers: any[] = [];
 13    export let clickable = true;
 14    
 15    // Variables locales
 16    let mapElement: HTMLElement;
 17    let map: any;
 18    let mapMarkers: any[] = [];
 19    let L: any;
 20    let unsubscribeMapStore: () => void;
 21    
 22    // Inicializar el mapa al montar el componente
 23    onMount(async () => {
 24      // Asegurar que Leaflet esté cargado y solo ejecutar en el navegador
 25      if (browser) {
 26        try {
 27          // Cargar CSS de Leaflet manualmente
 28          const linkElement = document.createElement('link');
 29          linkElement.rel = 'stylesheet';
 30          linkElement.href = 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.css';
 31          document.head.appendChild(linkElement);
 32          
 33          // Importar dinámicamente para evitar problemas de SSR
 34          L = await import('leaflet');
 35          console.log('Leaflet importado correctamente');
 36          
 37          // Esperar un momento para asegurar que el DOM está listo y el CSS esté cargado
 38          setTimeout(() => {
 39            if (mapElement) {
 40              initMap();
 41              console.log('Mapa inicializado con ID:', mapId);
 42              
 43              // Establecer valores iniciales en el store
 44              mapStore.setViewport({
 45                center: initialCenter,
 46                zoom: initialZoom
 47              });
 48              
 49              // Forzar un redimensionamiento para asegurar que el mapa se renderiza correctamente
 50              setTimeout(() => {
 51                if (map) {
 52                  map.invalidateSize();
 53                  console.log('Mapa redimensionado');
 54                }
 55              }, 200);
 56            } else {
 57              console.error('Elemento del mapa no encontrado:', mapId);
 58            }
 59          }, 100);
 60        } catch (error) {
 61          console.error('Error al cargar Leaflet:', error);
 62        }
 63      } else {
 64        console.log('OfflineMap: No estamos en el navegador, omitiendo inicialización del mapa');
 65      }
 66      
 67      // Limpieza al desmontar
 68      return () => {
 69        if (map) {
 70          map.remove();
 71        }
 72        // Cancelar suscripción para evitar memory leaks
 73        if (unsubscribeMapStore) {
 74          unsubscribeMapStore();
 75        }
 76      };
 77    });
 78    
 79    // Inicializar el mapa
 80    function initMap() {
 81      try {
 82        console.log('Inicializando mapa con ID:', mapId);
 83        console.log('Elemento del mapa:', mapElement);
 84        
 85        // Verificar que el elemento existe
 86        if (!mapElement) {
 87          console.error('Elemento del mapa no encontrado');
 88          return;
 89        }
 90        
 91        // Validar las coordenadas
 92        if (!initialCenter || typeof initialCenter.lat !== 'number' || typeof initialCenter.lng !== 'number') {
 93          console.error('Coordenadas iniciales inválidas:', initialCenter);
 94          initialCenter = { lat: 40.416775, lng: -3.703790 }; // Valor predeterminado
 95        }
 96        
 97        // Crear instancia de mapa con verificación
 98        console.log('Creando mapa en coordenadas:', initialCenter);
 99        map = L.map(mapId, {
100          zoomControl: true,
101          attributionControl: true
102        }).setView([initialCenter.lat, initialCenter.lng], initialZoom);
103        
104        console.log('Mapa creado correctamente');
105        
106        // Configuración de iconos - método explícito para evitar problemas
107        delete L.Icon.Default.prototype._getIconUrl;
108        
109        // Verificar si las imágenes existen en ambas rutas posibles
110        const iconPaths = {
111          iconRetinaUrl: ['/static/images/marker-icon.png'],
112          iconUrl: ['/static/images/marker-icon.png'],
113          shadowUrl: ['/static/images/marker-shadow.png']
114        };
115        
116        // Configuración manual de iconos con rutas absolutas
117        L.Icon.Default.mergeOptions({
118          iconRetinaUrl: iconPaths.iconRetinaUrl[0],
119          iconUrl: iconPaths.iconUrl[0],
120          shadowUrl: iconPaths.shadowUrl[0]
121        });
122        
123        console.log('Iconos configurados correctamente');
124      
125        // Capa base de OpenStreetMap online
126        const offlineTileLayer = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
127          attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
128          minZoom: 5,
129          maxZoom: 18,
130          errorTileUrl: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkqAcAAIUAgUW0RjgAAAAASUVORK5CYII='
131        });
132        
133        // Capa online (como fallback)
134        const onlineTileLayer = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
135          attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
136          minZoom: 5,
137          maxZoom: 19
138        });
139        
140        console.log('Layers configurados. Modo offline actual:', $mapStore.offline);
141        
142        // Agregar capa inicial basada en el modo actual
143        if ($mapStore.offline) {
144          console.log('Agregando capa offline');
145          offlineTileLayer.addTo(map);
146        } else {
147          console.log('Agregando capa online');
148          onlineTileLayer.addTo(map);
149        }
150        
151        // Detectar si debemos usar modo offline de acuerdo al store
152        unsubscribeMapStore = mapStore.subscribe(state => {
153          console.log('Cambio de modo offline a:', state.offline);
154          try {
155            if (state.offline) {
156              console.log('Activando capa offline');
157              offlineTileLayer.addTo(map);
158              if (map.hasLayer(onlineTileLayer)) {
159                console.log('Removiendo capa online');
160                map.removeLayer(onlineTileLayer);
161              }
162            } else {
163              console.log('Activando capa online');
164              onlineTileLayer.addTo(map);
165              if (map.hasLayer(offlineTileLayer)) {
166                console.log('Removiendo capa offline');
167                map.removeLayer(offlineTileLayer);
168              }
169            }
170          } catch (error) {
171            console.error('Error al cambiar capas del mapa:', error);
172          }
173        });
174        
175        // Manejar click en el mapa si está habilitado
176        if (clickable) {
177          map.on('click', (e: any) => {
178            const { lat, lng } = e.latlng;
179            addMarker({ lat, lng, title: `Punto (${lat.toFixed(4)}, ${lng.toFixed(4)})` });
180          });
181        }
182        
183        // Añadir marcadores iniciales
184        if (markers && markers.length) {
185          markers.forEach(addMarker);
186        }
187      } catch (error) {
188        console.error('Error al inicializar el mapa:', error);
189      }
190    }
191    
192    // Función para añadir un marcador
193    export function addMarker(markerData: any) {
194      if (!map) return;
195      
196      const { lat, lng, title, popup } = markerData;
197      
198      // Crear un marcador con opciones explícitas para asegurar compatibilidad
199      const markerOptions = {
200        title: title || '',
201        alt: title || 'Marcador',
202        keyboard: true,
203        riseOnHover: true
204      };
205      
206      const marker = L.marker([lat, lng], markerOptions).addTo(map);
207      
208      if (title || popup) {
209        marker.bindPopup(popup || title);
210      }
211      
212      mapMarkers.push(marker);
213      
214      // Actualizar el store con el nuevo marcador
215      mapStore.addMarker(markerData);
216      
217      return marker;
218    }
219    
220    // Función para limpiar todos los marcadores
221    export function clearMarkers() {
222      mapMarkers.forEach(marker => {
223        if (map) {
224          map.removeLayer(marker);
225        }
226      });
227      
228      mapMarkers = [];
229      
230      // Actualizar el store
231      mapStore.clearMarkers();
232    }
233  
234    // Función para limpiar ruta
235    export function clearRoute() {
236      // Implementación simulada
237      console.log('Limpiando ruta (función simulada)');
238      
239      // En una implementación real, eliminaríamos la capa de ruta
240      if (map) {
241        map.eachLayer((layer: any) => {
242          if (layer._path && layer.options.className === 'route-line') {
243            map.removeLayer(layer);
244          }
245        });
246      }
247    }
248    
249    // Función para calcular ruta
250    export function calculateRoute(start?: any, end?: any) {
251      // Implementación simulada
252      console.log('Calculando ruta entre puntos (función simulada)');
253      
254      // En una implementación real, conectaríamos con un servicio de routing
255      // y dibujaríamos la ruta en el mapa
256      if (map && L && start && end) {
257        // Dibujamos una línea recta como simulación
258        const routeLine = L.polyline([
259          [start.lat, start.lng],
260          [end.lat, end.lng]
261        ], {
262          color: '#3388ff',
263          weight: 6,
264          opacity: 0.7,
265          className: 'route-line'
266        }).addTo(map);
267        
268        // Ajustar la vista para que se vea toda la ruta
269        map.fitBounds(routeLine.getBounds(), {
270          padding: [50, 50]
271        });
272      }
273    }
274    
275    // Exportar funciones que pueden ser llamadas desde fuera
276    export function setCenter(lat: number, lng: number, zoom: number | null = null) {
277      if (!map) return;
278      
279      map.setView([lat, lng], zoom !== null ? zoom : map.getZoom());
280      
281      // Actualizar el store
282      mapStore.setViewport({
283        center: { lat, lng },
284        zoom: zoom !== null ? zoom : map.getZoom()
285      });
286    }
287    
288    // Función para cambiar entre online/offline
289    export function toggleOfflineMode(useOffline: boolean) {
290      console.log('OfflineMap: Cambiando a modo', useOffline ? 'offline' : 'online');
291      mapStore.setOfflineMode(useOffline);
292    }
293  </script>
294  
295  <div bind:this={mapElement} id={mapId} style="height: {height}; width: 100%; position: absolute; top: 0; left: 0; right: 0; bottom: 0;"></div>
296  
297  <style>
298    @import "leaflet/dist/leaflet.css";
299    
300    /* Estilos adicionales */
301    :global(.leaflet-container) {
302      font-family: var(--font-primary);
303      background-color: var(--bg-secondary);
304      position: absolute;
305      top: 0;
306      left: 0;
307      right: 0;
308      bottom: 0;
309      width: 100%;
310      height: 100%;
311    }
312    
313    :global(.leaflet-control-attribution) {
314      font-size: 10px;
315    }
316    
317    :global(.leaflet-popup-content-wrapper) {
318      background-color: var(--bg-primary);
319      color: var(--text-color);
320      border-radius: var(--border-radius);
321    }
322    
323    :global(.leaflet-popup-tip) {
324      background-color: var(--bg-primary);
325    }
326    
327    :global(.dark .leaflet-popup-content-wrapper),
328    :global(.dark .leaflet-popup-tip) {
329      background-color: var(--bg-card-primary);
330      color: var(--text-color);
331    }
332  </style>