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: '© <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: '© <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>