MapResults.svelte
1 <!-- 2 Componente para mostrar resultados de búsqueda en un mapa 3 Utiliza el sistema completo de mapas offline para ofrecer funcionalidad 4 sin dependencias externas 5 --> 6 <script lang="ts"> 7 import { onMount, tick } from 'svelte'; 8 import OfflineMap from '$lib/maps/components/OfflineMap.svelte'; 9 import { mapStore } from '$lib/stores'; 10 import { page } from '$app/stores'; 11 import { goto } from '$app/navigation'; 12 import { browser } from '$app/environment'; 13 14 // Modo de mapa independiente 15 export let isMapMode = true; 16 export let searchQuery = ''; 17 18 // Props para resultados (solo si no estamos en modo mapa independiente) 19 export let results = []; 20 export let loading = false; 21 export let loadMore = () => {}; 22 23 let mapComponent; 24 let showList = true; 25 let selectedItem = null; 26 let searchInput; 27 let searchResults = []; 28 let isSearching = false; 29 let selectedFilter = 'todos'; 30 let showRoutingPanel = false; 31 let routeOrigin = null; 32 let routeDestination = null; 33 34 // Referencia al elemento sentinel para carga infinita 35 let sentinel; 36 let observer; 37 38 // Función para crear el observador para carga infinita 39 function createObserver() { 40 if (sentinel && !observer) { 41 observer = new IntersectionObserver((entries) => { 42 if (entries[0].isIntersecting && !loading) { 43 console.log('🔄 Sentinel detectado, cargando más resultados'); 44 loadMore(); 45 } 46 }, { rootMargin: '200px' }); 47 48 observer.observe(sentinel); 49 console.log('👁️ Observer creado y observando el sentinel'); 50 } 51 } 52 53 onMount(async () => { 54 if (browser) { 55 console.log('MapResults montado, configurando mapa'); 56 57 // Esperar tick para que el DOM se actualice 58 await tick(); 59 60 // Usar MutationObserver para detectar cuando el mapa está realmente en el DOM 61 const container = document.querySelector('.map-container'); 62 if (container) { 63 // Usar ResizeObserver en lugar de intervalos para detectar cambios de tamaño 64 const resizeObserver = new ResizeObserver(entries => { 65 if (mapSystem?.map) { 66 mapSystem.map.invalidateSize(); 67 console.log('Mapa redimensionado por ResizeObserver'); 68 } 69 }); 70 71 resizeObserver.observe(container); 72 73 // Configurar sentinel para carga infinita 74 createObserver(); 75 76 // Añadir evento de redimensionamiento de ventana para actualizar mapa 77 const handleResize = () => { 78 if (mapSystem?.map) { 79 mapSystem.map.invalidateSize(); 80 } 81 }; 82 83 window.addEventListener('resize', handleResize); 84 85 // Asegurar tamaño correcto del mapa después de un breve tiempo 86 setTimeout(() => { 87 if (mapSystem?.map) { 88 mapSystem.map.invalidateSize(); 89 } 90 }, 500); 91 92 return () => { 93 if (observer) { 94 observer.disconnect(); 95 } 96 resizeObserver.disconnect(); 97 window.removeEventListener('resize', handleResize); 98 }; 99 } 100 } 101 }); 102 103 // Ejemplo de puntos de interés para modo mapa 104 const interestPoints = [ 105 { 106 title: 'Madrid', 107 description: 'Capital de España y ciudad más poblada del país', 108 coordinates: { lat: 40.416775, lng: -3.703790 }, 109 link: 'https://es.wikipedia.org/wiki/Madrid', 110 type: 'ciudad' 111 }, 112 { 113 title: 'Barcelona', 114 description: 'Ciudad costera y capital de Cataluña, famosa por su arquitectura', 115 coordinates: { lat: 41.385064, lng: 2.173404 }, 116 link: 'https://es.wikipedia.org/wiki/Barcelona', 117 type: 'ciudad' 118 }, 119 { 120 title: 'Valencia', 121 description: 'Ciudad del Mediterráneo conocida por las Fallas y la Ciudad de las Artes y las Ciencias', 122 coordinates: { lat: 39.469906, lng: -0.376288 }, 123 link: 'https://es.wikipedia.org/wiki/Valencia', 124 type: 'ciudad' 125 }, 126 { 127 title: 'Sevilla', 128 description: 'Capital de Andalucía, conocida por la Giralda y la Plaza de España', 129 coordinates: { lat: 37.389092, lng: -5.984459 }, 130 link: 'https://es.wikipedia.org/wiki/Sevilla', 131 type: 'ciudad' 132 }, 133 { 134 title: 'Bilbao', 135 description: 'Ciudad industrial con el museo Guggenheim, ejemplo de renovación urbana', 136 coordinates: { lat: 43.263012, lng: -2.934985 }, 137 link: 'https://es.wikipedia.org/wiki/Bilbao', 138 type: 'ciudad' 139 }, 140 { 141 title: 'Santiago de Compostela', 142 description: 'Capital de Galicia y destino del Camino de Santiago', 143 coordinates: { lat: 42.878213, lng: -8.544844 }, 144 link: 'https://es.wikipedia.org/wiki/Santiago_de_Compostela', 145 type: 'ciudad' 146 }, 147 { 148 title: 'Granada', 149 description: 'Ciudad andaluza conocida por la Alhambra y Sierra Nevada', 150 coordinates: { lat: 37.177336, lng: -3.598557 }, 151 link: 'https://es.wikipedia.org/wiki/Granada', 152 type: 'ciudad' 153 }, 154 { 155 title: 'Córdoba', 156 description: 'Ciudad histórica con la famosa Mezquita-Catedral', 157 coordinates: { lat: 37.888176, lng: -4.779383 }, 158 link: 'https://es.wikipedia.org/wiki/C%C3%B3rdoba_(Espa%C3%B1a)', 159 type: 'ciudad' 160 }, 161 { 162 title: 'Parque Nacional de Doñana', 163 description: 'Importante reserva natural y humedal en Andalucía', 164 coordinates: { lat: 37.042556, lng: -6.476364 }, 165 link: 'https://es.wikipedia.org/wiki/Parque_nacional_y_natural_de_Do%C3%B1ana', 166 type: 'parque' 167 }, 168 { 169 title: 'Sierra de Gredos', 170 description: 'Impresionante sistema montañoso en el Sistema Central', 171 coordinates: { lat: 40.259926, lng: -5.139841 }, 172 link: 'https://es.wikipedia.org/wiki/Sierra_de_Gredos', 173 type: 'parque' 174 }, 175 { 176 title: 'Museo del Prado', 177 description: 'Uno de los museos de arte más importantes del mundo', 178 coordinates: { lat: 40.413755, lng: -3.692459 }, 179 link: 'https://es.wikipedia.org/wiki/Museo_del_Prado', 180 type: 'museo' 181 }, 182 { 183 title: 'Sagrada Familia', 184 description: 'Basílica diseñada por Antoni Gaudí, símbolo de Barcelona', 185 coordinates: { lat: 41.403706, lng: 2.173504 }, 186 link: 'https://es.wikipedia.org/wiki/Templo_Expiatorio_de_la_Sagrada_Familia', 187 type: 'monumento' 188 } 189 ]; 190 191 // Función para buscar ubicaciones 192 async function searchLocations() { 193 isSearching = true; 194 195 try { 196 // Simulación de búsqueda (en producción usaríamos una API de geocoding) 197 // En una implementación real, esto conectaría con un servicio de geocodificación 198 199 // Simulamos una demora de red 200 await new Promise(resolve => setTimeout(resolve, 500)); 201 202 // Filtramos los puntos de interés que coincidan con la búsqueda 203 if (searchQuery && searchQuery.trim() !== '') { 204 const query = searchQuery.toLowerCase().trim(); 205 let filteredResults = interestPoints.filter(point => 206 point.title.toLowerCase().includes(query) || 207 point.description.toLowerCase().includes(query) 208 ); 209 210 // Aplicar filtro por tipo si está seleccionado 211 if (selectedFilter !== 'todos') { 212 filteredResults = filteredResults.filter(point => point.type === selectedFilter); 213 } 214 215 searchResults = filteredResults; 216 } else { 217 // Si no hay búsqueda, mostrar todos los puntos de interés 218 let allResults = [...interestPoints]; 219 220 // Aplicar filtro por tipo si está seleccionado 221 if (selectedFilter !== 'todos') { 222 allResults = allResults.filter(point => point.type === selectedFilter); 223 } 224 225 searchResults = allResults; 226 } 227 } catch (error) { 228 console.error('Error al buscar ubicaciones:', error); 229 searchResults = []; 230 } finally { 231 isSearching = false; 232 } 233 } 234 235 // Función para cambiar el filtro de tipo 236 function changeFilter(filter) { 237 selectedFilter = filter; 238 searchLocations(); 239 } 240 241 // Función para iniciar el cálculo de ruta 242 function startRouting() { 243 showRoutingPanel = true; 244 routeOrigin = null; 245 routeDestination = null; 246 247 // Limpiar marcadores y ruta existente 248 if (mapComponent) { 249 mapComponent.clearMarkers(); 250 mapComponent.clearRoute(); 251 } 252 } 253 254 // Función para calcular la ruta entre origen y destino 255 function calculateRouteBetween() { 256 if (!routeOrigin || !routeDestination) { 257 console.error('Se necesita origen y destino para calcular la ruta'); 258 return; 259 } 260 261 if (mapComponent) { 262 // Limpiar marcadores y ruta existente 263 mapComponent.clearMarkers(); 264 mapComponent.clearRoute(); 265 266 // Añadir marcador para el origen 267 mapComponent.addMarker({ 268 lat: routeOrigin.coordinates.lat, 269 lng: routeOrigin.coordinates.lng, 270 title: routeOrigin.title, 271 popup: `<strong>Origen: ${routeOrigin.title}</strong>` 272 }); 273 274 // Añadir marcador para el destino 275 mapComponent.addMarker({ 276 lat: routeDestination.coordinates.lat, 277 lng: routeDestination.coordinates.lng, 278 title: routeDestination.title, 279 popup: `<strong>Destino: ${routeDestination.title}</strong>` 280 }); 281 282 // Calcular la ruta 283 mapComponent.calculateRoute( 284 routeOrigin.coordinates, 285 routeDestination.coordinates 286 ); 287 } 288 } 289 290 // Al montar el componente, realizar una búsqueda inicial 291 onMount(() => { 292 if (isMapMode) { 293 searchLocations(); 294 } 295 }); 296 297 // Filtrar resultados que tengan coordenadas válidas (para modo no-mapa) 298 $: mapResults = isMapMode ? searchResults : results.filter(item => { 299 return item.coordinates && 300 typeof item.coordinates.lat === 'number' && 301 typeof item.coordinates.lng === 'number' && 302 !isNaN(item.coordinates.lat) && 303 !isNaN(item.coordinates.lng); 304 }); 305 306 // Función para seleccionar un resultado 307 function selectResult(result) { 308 selectedItem = result; 309 310 // Si tenemos acceso al mapa, centrarlo en las coordenadas seleccionadas 311 if (mapComponent && result.coordinates) { 312 // Usar funciones exportadas del componente del mapa 313 mapComponent.setCenter( 314 result.coordinates.lat, 315 result.coordinates.lng, 316 15 317 ); 318 } 319 } 320 321 // Manejar búsqueda desde la caja de búsqueda 322 function handleSearch(e) { 323 if (e.key === 'Enter' || e.type === 'click') { 324 searchLocations(); 325 } 326 } 327 328 // Función para alternar la vista de lista 329 function toggleList() { 330 showList = !showList; 331 } 332 333 // Extraer URL para uso reactivo 334 $: pageUrl = $page.url; 335 export {}; 336 337 // Función para volver a la página anterior 338 function goBack() { 339 const params = new URLSearchParams(pageUrl.searchParams); 340 params.delete('view'); 341 goto(`/?${params.toString()}`); 342 } 343 </script> 344 345 <div class="map-results-container"> 346 <div class="map-content {showList ? 'with-list' : 'full-map'}"> 347 <!-- Contenedor del mapa --> 348 <div class="map-container"> 349 <!-- Barra de búsqueda de ubicación (solo en modo mapa) --> 350 {#if isMapMode} 351 <div class="map-search-bar"> 352 <input 353 type="text" 354 bind:value={searchQuery} 355 bind:this={searchInput} 356 placeholder="Buscar ubicación..." 357 on:keydown={e => e.key === 'Enter' && handleSearch(e)} 358 /> 359 <button 360 class="search-button" 361 on:click={handleSearch} 362 disabled={isSearching} 363 aria-label="Buscar ubicación" 364 > 365 {#if isSearching} 366 <div class="mini-spinner"></div> 367 {:else} 368 <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> 369 <circle cx="11" cy="11" r="8"></circle> 370 <line x1="21" y1="21" x2="16.65" y2="16.65"></line> 371 </svg> 372 {/if} 373 </button> 374 </div> 375 {/if} 376 377 <OfflineMap 378 bind:this={mapComponent} 379 height="100%" 380 initialCenter={{ lat: 40.416775, lng: -3.703790 }} 381 initialZoom={6} 382 markers={mapResults} 383 mapId="search-results-map" 384 clickable={true} 385 /> 386 387 <!-- Botón para alternar la lista lateral --> 388 <div class="map-sidebar-toggle"> 389 <button class="sidebar-toggle-btn" on:click={toggleList} aria-label={showList ? 'Ocultar lista' : 'Mostrar lista'}> 390 <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"> 391 {#if showList} 392 <line x1="4" y1="6" x2="20" y2="6"></line> 393 <line x1="4" y1="12" x2="20" y2="12"></line> 394 <line x1="4" y1="18" x2="20" y2="18"></line> 395 {:else} 396 <rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect> 397 {/if} 398 </svg> 399 </button> 400 </div> 401 </div> 402 403 <!-- Panel lateral con lista de resultados --> 404 {#if showList} 405 <div class="map-list-container"> 406 <div class="map-list-header"> 407 <h2>{isMapMode ? 'Ubicaciones' : 'Resultados con ubicación'}</h2> 408 <button class="close-list-button" on:click={toggleList} aria-label="Cerrar lista"> 409 <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> 410 <line x1="18" y1="6" x2="6" y2="18"></line> 411 <line x1="6" y1="6" x2="18" y2="18"></line> 412 </svg> 413 </button> 414 </div> 415 416 {#if isMapMode} 417 <!-- Búsqueda en modo mapa --> 418 <div class="map-list-actions"> 419 <!-- Filtros por tipo de ubicación --> 420 <div class="filter-buttons"> 421 <button 422 class="filter-button {selectedFilter === 'todos' ? 'active' : ''}" 423 on:click={() => changeFilter('todos')} 424 > 425 Todos 426 </button> 427 <button 428 class="filter-button {selectedFilter === 'ciudad' ? 'active' : ''}" 429 on:click={() => changeFilter('ciudad')} 430 > 431 Ciudades 432 </button> 433 <button 434 class="filter-button {selectedFilter === 'parque' ? 'active' : ''}" 435 on:click={() => changeFilter('parque')} 436 > 437 Parques 438 </button> 439 <button 440 class="filter-button {selectedFilter === 'museo' ? 'active' : ''}" 441 on:click={() => changeFilter('museo')} 442 > 443 Museos 444 </button> 445 <button 446 class="filter-button {selectedFilter === 'monumento' ? 'active' : ''}" 447 on:click={() => changeFilter('monumento')} 448 > 449 Monumentos 450 </button> 451 </div> 452 453 <div class="action-buttons"> 454 <button 455 class="action-button" 456 on:click={() => { 457 // Limpiar ruta 458 if (mapComponent && typeof mapComponent.clearRoute === 'function') { 459 mapComponent.clearRoute(); 460 showRoutingPanel = false; 461 } 462 }} 463 > 464 <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> 465 <path d="M6 18L18 6M6 6l12 12"></path> 466 </svg> 467 Limpiar mapa 468 </button> 469 470 <button 471 class="action-button {showRoutingPanel ? 'active' : ''}" 472 on:click={startRouting} 473 > 474 <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> 475 <path d="M3 12h18M3 6h18M3 18h18"></path> 476 </svg> 477 Calcular ruta 478 </button> 479 </div> 480 481 <!-- Panel de cálculo de ruta --> 482 {#if showRoutingPanel} 483 <div class="routing-panel"> 484 <h4>Calcular ruta entre ubicaciones</h4> 485 486 <div class="route-selectors"> 487 <div class="route-selector"> 488 <label for="route-origin">Origen:</label> 489 <select 490 id="route-origin" 491 bind:value={routeOrigin} 492 class="route-select" 493 > 494 <option value={null}>Seleccionar origen</option> 495 {#each searchResults as point} 496 <option value={point}>{point.title}</option> 497 {/each} 498 </select> 499 </div> 500 501 <div class="route-selector"> 502 <label for="route-destination">Destino:</label> 503 <select 504 id="route-destination" 505 bind:value={routeDestination} 506 class="route-select" 507 > 508 <option value={null}>Seleccionar destino</option> 509 {#each searchResults as point} 510 <option value={point}>{point.title}</option> 511 {/each} 512 </select> 513 </div> 514 </div> 515 516 <button 517 class="calculate-route-button" 518 on:click={calculateRouteBetween} 519 disabled={!routeOrigin || !routeDestination} 520 > 521 <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> 522 <polygon points="22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3"></polygon> 523 </svg> 524 Calcular ruta 525 </button> 526 </div> 527 {/if} 528 </div> 529 {/if} 530 531 <div class="map-list"> 532 {#if mapResults.length > 0} 533 {#each mapResults as result, index} 534 <div 535 class="map-list-item {selectedItem === result ? 'selected' : ''}" 536 on:click={() => selectResult(result)} 537 role="button" 538 tabindex="0" 539 on:keydown={(e) => e.key === 'Enter' && selectResult(result)} 540 > 541 <!-- Mostrar tipo de ubicación si está disponible --> 542 {#if result.type} 543 <div class="item-type {result.type}"> 544 {#if result.type === 'ciudad'} 545 <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> 546 <path d="M2 12h20M2 20h20M2 4h20"></path> 547 </svg> 548 <span>Ciudad</span> 549 {:else if result.type === 'parque'} 550 <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> 551 <path d="M12 22v-5M9 7V2M15 2v5M5 8h14M5 16h14"></path> 552 <path d="M19 8a7 7 0 1 0-14 0"></path> 553 <path d="M5 16a7 7 0 1 0 14 0"></path> 554 </svg> 555 <span>Parque</span> 556 {:else if result.type === 'museo'} 557 <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> 558 <path d="M2 20h20M4 20V4h16v16"></path> 559 <path d="M12 2v2M8 6h8M8 10h8M8 14h8"></path> 560 </svg> 561 <span>Museo</span> 562 {:else if result.type === 'monumento'} 563 <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> 564 <path d="M12 2v20M4.93 10H19.07"></path> 565 <path d="M2 12h20"></path> 566 <path d="M12 2L2 12M12 2l10 10"></path> 567 </svg> 568 <span>Monumento</span> 569 {:else} 570 <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> 571 <path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"></path> 572 <circle cx="12" cy="10" r="3"></circle> 573 </svg> 574 <span>{result.type}</span> 575 {/if} 576 </div> 577 {/if} 578 579 <h3 class="item-title">{result.title}</h3> 580 <p class="item-description"> 581 {result.description.substring(0, 100)} 582 {result.description.length > 100 ? '...' : ''} 583 </p> 584 <div class="item-coords"> 585 <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> 586 <path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"></path> 587 <circle cx="12" cy="10" r="3"></circle> 588 </svg> 589 <span> 590 {result.coordinates.lat.toFixed(4)}, {result.coordinates.lng.toFixed(4)} 591 </span> 592 </div> 593 <div class="item-actions"> 594 <a href={result.link} class="item-link" target="_blank" rel="noopener noreferrer"> 595 Ver más 596 </a> 597 598 <!-- Si hay más de un resultado, mostrar botón para calcular ruta --> 599 {#if mapResults.length > 1 && index !== mapResults.length - 1} 600 <button 601 class="route-to-button" 602 on:click={(e) => { 603 e.stopPropagation(); 604 605 // Esperar a que se actualice el DOM 606 tick().then(() => { 607 // Si tenemos acceso al mapa 608 if (mapComponent) { 609 // Limpiar marcadores existentes 610 mapComponent.clearMarkers(); 611 // Limpiar ruta existente 612 if (typeof mapComponent.clearRoute === 'function') { 613 mapComponent.clearRoute(); 614 } 615 616 // Añadir marcador para el inicio 617 mapComponent.addMarker({ 618 lat: result.coordinates.lat, 619 lng: result.coordinates.lng, 620 title: result.title 621 }); 622 623 // Añadir marcador para el destino (siguiente resultado) 624 const nextResult = mapResults[index + 1]; 625 mapComponent.addMarker({ 626 lat: nextResult.coordinates.lat, 627 lng: nextResult.coordinates.lng, 628 title: nextResult.title 629 }); 630 631 // Si el mapa tiene función de cálculo de rutas 632 if (typeof mapComponent.calculateRoute === 'function') { 633 mapComponent.calculateRoute(); 634 } 635 } 636 }); 637 }} 638 > 639 Ruta al siguiente 640 </button> 641 {/if} 642 </div> 643 </div> 644 {/each} 645 646 <!-- Sentinel para carga infinita (solo en modo no-mapa) --> 647 {#if !isMapMode} 648 <div class="sentinel" bind:this={sentinel}></div> 649 650 {#if loading} 651 <div class="loading-indicator"> 652 <div class="spinner"></div> 653 <span>Cargando más resultados...</span> 654 </div> 655 {/if} 656 {/if} 657 {:else} 658 <div class="no-results"> 659 <p>No se encontraron resultados con coordenadas geográficas</p> 660 </div> 661 {/if} 662 </div> 663 </div> 664 {/if} 665 </div> 666 </div> 667 668 <style> 669 .map-results-container { 670 display: flex; 671 flex-direction: column; 672 width: 100%; 673 height: 100%; 674 position: absolute; 675 top: 0; 676 left: 0; 677 right: 0; 678 bottom: 0; 679 } 680 681 .map-content { 682 display: flex; 683 width: 100%; 684 height: 100%; 685 position: relative; 686 z-index: 10; 687 background-color: var(--bg-primary); 688 pointer-events: auto; 689 margin-top: 0; 690 } 691 692 /* Barra de búsqueda del mapa */ 693 .map-search-bar { 694 position: absolute; 695 top: 10px; 696 left: 50%; 697 transform: translateX(-50%); 698 z-index: 1000; 699 display: flex; 700 align-items: center; 701 background: var(--bg-primary); 702 border: 1px solid var(--border-color); 703 border-radius: 24px; 704 box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2); 705 width: 80%; 706 max-width: 400px; 707 padding: 0 8px; 708 } 709 710 .map-search-bar input { 711 flex: 1; 712 border: none; 713 outline: none; 714 background: transparent; 715 padding: 12px 12px; 716 font-size: 15px; 717 color: var(--text-color); 718 } 719 720 .map-search-bar .search-button { 721 background: var(--primary); 722 color: white; 723 border: none; 724 width: 32px; 725 height: 32px; 726 border-radius: 50%; 727 display: flex; 728 align-items: center; 729 justify-content: center; 730 cursor: pointer; 731 transition: all 0.2s; 732 } 733 734 .map-search-bar .search-button:hover { 735 transform: scale(1.05); 736 box-shadow: 0 1px 4px rgba(0, 0, 0, 0.2); 737 } 738 739 .map-search-bar .search-button:disabled { 740 opacity: 0.7; 741 cursor: not-allowed; 742 } 743 744 .mini-spinner { 745 width: 12px; 746 height: 12px; 747 border: 2px solid rgba(255,255,255,0.3); 748 border-top-color: white; 749 border-radius: 50%; 750 animation: spin 0.8s linear infinite; 751 } 752 753 .map-container { 754 position: absolute; 755 top: 0; 756 left: 0; 757 right: 0; 758 bottom: 0; 759 width: 100%; 760 height: 100%; 761 z-index: 5; 762 } 763 764 .map-sidebar-toggle { 765 position: absolute; 766 top: 70px; 767 left: 10px; 768 z-index: 1000; 769 } 770 771 .sidebar-toggle-btn { 772 display: flex; 773 align-items: center; 774 justify-content: center; 775 width: 40px; 776 height: 40px; 777 background: var(--bg-primary); 778 border: 1px solid var(--border-color); 779 border-radius: 4px; 780 cursor: pointer; 781 box-shadow: 0 2px 4px rgba(0, 0, 0, 0.15); 782 transition: all 0.2s ease; 783 } 784 785 .sidebar-toggle-btn:hover { 786 background: var(--bg-secondary); 787 } 788 789 .with-list .map-list-container { 790 width: 350px; 791 flex-shrink: 0; 792 z-index: 900; 793 height: 100%; 794 position: absolute; 795 right: 0; 796 top: 0; 797 overflow: hidden; 798 box-shadow: 0 0 10px rgba(0,0,0,0.2); 799 background: var(--bg-primary); 800 } 801 802 .map-list-container { 803 border-left: 1px solid var(--border-color); 804 overflow: hidden; 805 background: var(--bg-primary); 806 box-shadow: -2px 0 8px rgba(0, 0, 0, 0.08); 807 } 808 809 .map-list-header { 810 display: flex; 811 justify-content: space-between; 812 align-items: center; 813 padding: var(--space-md); 814 background: var(--bg-secondary); 815 border-bottom: 1px solid var(--border-color); 816 } 817 818 .map-list-header h2 { 819 margin: 0; 820 font-size: 1.1rem; 821 font-weight: var(--font-weight-medium); 822 } 823 824 .map-list-actions { 825 padding: 10px 15px; 826 border-bottom: 1px solid var(--border-color); 827 display: flex; 828 flex-direction: column; 829 gap: 12px; 830 } 831 832 .filter-buttons { 833 display: flex; 834 flex-wrap: wrap; 835 gap: 8px; 836 margin-bottom: 8px; 837 } 838 839 .filter-button { 840 background: var(--bg-secondary); 841 color: var(--text-color); 842 border: 1px solid var(--border-color); 843 border-radius: 16px; 844 padding: 4px 12px; 845 font-size: 13px; 846 cursor: pointer; 847 transition: all 0.2s; 848 } 849 850 .filter-button:hover { 851 background: var(--bg-hover); 852 } 853 854 .filter-button.active { 855 background: var(--primary); 856 color: white; 857 border-color: var(--primary); 858 } 859 860 .action-buttons { 861 display: flex; 862 flex-wrap: wrap; 863 gap: 8px; 864 } 865 866 .action-button { 867 display: flex; 868 align-items: center; 869 gap: 8px; 870 padding: 8px 12px; 871 background: var(--primary); 872 color: white; 873 border: none; 874 border-radius: 4px; 875 cursor: pointer; 876 font-size: 14px; 877 transition: background 0.2s; 878 } 879 880 .action-button:hover { 881 background: var(--primary-dark, #3367d6); 882 } 883 884 .action-button.active { 885 background: var(--primary-dark, #3367d6); 886 box-shadow: 0 0 0 2px rgba(var(--primary-rgb), 0.4); 887 } 888 889 .routing-panel { 890 background: var(--bg-card-primary); 891 border: 1px solid var(--border-color); 892 border-radius: 8px; 893 padding: 12px; 894 margin-top: 8px; 895 } 896 897 .routing-panel h4 { 898 margin: 0 0 12px; 899 font-size: 15px; 900 font-weight: var(--font-weight-medium); 901 color: var(--text-color); 902 } 903 904 .route-selectors { 905 display: flex; 906 flex-direction: column; 907 gap: 12px; 908 margin-bottom: 12px; 909 } 910 911 .route-selector { 912 display: flex; 913 flex-direction: column; 914 gap: 4px; 915 } 916 917 .route-selector label { 918 font-size: 14px; 919 color: var(--text-secondary); 920 } 921 922 .route-select { 923 width: 100%; 924 padding: 8px 10px; 925 border: 1px solid var(--border-color); 926 border-radius: 4px; 927 background: var(--bg-primary); 928 color: var(--text-color); 929 font-size: 14px; 930 appearance: none; 931 background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E"); 932 background-repeat: no-repeat; 933 background-position: right 10px center; 934 background-size: 16px; 935 } 936 937 .calculate-route-button { 938 width: 100%; 939 display: flex; 940 align-items: center; 941 justify-content: center; 942 gap: 8px; 943 padding: 10px; 944 background: var(--primary); 945 color: white; 946 border: none; 947 border-radius: 4px; 948 cursor: pointer; 949 font-size: 14px; 950 font-weight: var(--font-weight-medium); 951 transition: all 0.2s; 952 } 953 954 .calculate-route-button:hover:not(:disabled) { 955 background: var(--primary-dark, #3367d6); 956 transform: translateY(-1px); 957 } 958 959 .calculate-route-button:disabled { 960 opacity: 0.6; 961 cursor: not-allowed; 962 } 963 964 .close-list-button { 965 background: none; 966 border: none; 967 cursor: pointer; 968 padding: var(--space-xs); 969 display: flex; 970 align-items: center; 971 justify-content: center; 972 opacity: 0.7; 973 transition: opacity var(--transition-speed); 974 } 975 976 .close-list-button:hover { 977 opacity: 1; 978 } 979 980 .map-list { 981 height: calc(100% - 110px); 982 overflow-y: auto; 983 scrollbar-width: thin; 984 } 985 986 .map-list-item { 987 padding: var(--space-md); 988 border-bottom: 1px solid var(--border-color); 989 cursor: pointer; 990 transition: all var(--transition-speed); 991 } 992 993 .map-list-item:hover { 994 background: var(--bg-secondary); 995 } 996 997 .map-list-item.selected { 998 background: rgba(var(--primary-rgb), 0.1); 999 border-left: 3px solid var(--primary); 1000 } 1001 1002 .item-title { 1003 font-size: 1rem; 1004 font-weight: var(--font-weight-medium); 1005 margin: 0 0 var(--space-xs); 1006 color: var(--primary); 1007 } 1008 1009 .item-type { 1010 display: inline-flex; 1011 align-items: center; 1012 gap: 6px; 1013 background: var(--bg-secondary); 1014 padding: 3px 8px; 1015 border-radius: 12px; 1016 font-size: 12px; 1017 margin-bottom: 8px; 1018 color: var(--text-secondary); 1019 } 1020 1021 .item-type.ciudad { 1022 background-color: rgba(59, 130, 246, 0.15); 1023 color: #3b82f6; 1024 } 1025 1026 .item-type.parque { 1027 background-color: rgba(16, 185, 129, 0.15); 1028 color: #10b981; 1029 } 1030 1031 .item-type.museo { 1032 background-color: rgba(139, 92, 246, 0.15); 1033 color: #8b5cf6; 1034 } 1035 1036 .item-type.monumento { 1037 background-color: rgba(245, 158, 11, 0.15); 1038 color: #f59e0b; 1039 } 1040 1041 .item-type svg { 1042 opacity: 0.9; 1043 } 1044 1045 .item-description { 1046 font-size: 0.9rem; 1047 margin: 0 0 var(--space-xs); 1048 color: var(--text-secondary); 1049 line-height: 1.4; 1050 } 1051 1052 .item-coords { 1053 display: flex; 1054 align-items: center; 1055 gap: var(--space-xs); 1056 font-size: 0.8rem; 1057 color: var(--text-secondary); 1058 margin-bottom: var(--space-xs); 1059 } 1060 1061 .item-actions { 1062 display: flex; 1063 align-items: center; 1064 justify-content: space-between; 1065 margin-top: 10px; 1066 } 1067 1068 .item-link { 1069 font-size: 0.85rem; 1070 color: var(--primary); 1071 text-decoration: none; 1072 } 1073 1074 .item-link:hover { 1075 text-decoration: underline; 1076 } 1077 1078 .route-to-button { 1079 font-size: 0.85rem; 1080 background: none; 1081 border: 1px solid var(--primary); 1082 color: var(--primary); 1083 padding: 4px 8px; 1084 border-radius: 4px; 1085 cursor: pointer; 1086 transition: all 0.2s; 1087 } 1088 1089 .route-to-button:hover { 1090 background: var(--primary); 1091 color: white; 1092 } 1093 1094 .loading-indicator { 1095 display: flex; 1096 align-items: center; 1097 justify-content: center; 1098 gap: var(--space-sm); 1099 padding: var(--space-md); 1100 color: var(--text-secondary); 1101 } 1102 1103 .spinner { 1104 width: 16px; 1105 height: 16px; 1106 border: 2px solid var(--border-color); 1107 border-top-color: var(--primary); 1108 border-radius: 50%; 1109 animation: spin 0.8s linear infinite; 1110 } 1111 1112 @keyframes spin { 1113 to { transform: rotate(360deg); } 1114 } 1115 1116 .sentinel { 1117 height: 1px; 1118 margin-bottom: 200px; 1119 } 1120 1121 .no-results { 1122 padding: var(--space-xl); 1123 text-align: center; 1124 color: var(--text-secondary); 1125 } 1126 1127 /* Responsive */ 1128 @media (max-width: 768px) { 1129 .with-list .map-list-container { 1130 width: 100%; 1131 max-width: 100%; 1132 height: 50%; 1133 top: 50%; 1134 left: 0; 1135 right: 0; 1136 border-radius: 12px 12px 0 0; 1137 border-left: none; 1138 border-top: 1px solid var(--border-color); 1139 box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.1); 1140 } 1141 1142 .map-list { 1143 height: calc(100% - 110px); 1144 } 1145 1146 .sidebar-toggle-btn { 1147 width: 36px; 1148 height: 36px; 1149 } 1150 1151 .filter-buttons { 1152 justify-content: center; 1153 } 1154 1155 .action-buttons { 1156 justify-content: center; 1157 } 1158 1159 .routing-panel { 1160 padding: 10px; 1161 } 1162 } 1163 1164 @media (max-width: 480px) { 1165 .map-list-container { 1166 width: 100%; 1167 } 1168 1169 .map-sidebar-toggle { 1170 top: 60px; 1171 left: 10px; 1172 } 1173 1174 .with-list .map-list-container { 1175 height: 65%; 1176 top: 35%; 1177 } 1178 1179 .sidebar-toggle-btn { 1180 width: 32px; 1181 height: 32px; 1182 } 1183 1184 .item-actions { 1185 flex-direction: column; 1186 align-items: flex-start; 1187 gap: 8px; 1188 } 1189 1190 .route-to-button { 1191 width: 100%; 1192 text-align: center; 1193 padding: 6px 8px; 1194 } 1195 1196 .map-search-bar { 1197 width: 90%; 1198 top: 10px; 1199 } 1200 1201 .map-search-bar input { 1202 font-size: 14px; 1203 padding: 8px; 1204 } 1205 1206 .map-search-bar .search-button { 1207 width: 28px; 1208 height: 28px; 1209 } 1210 1211 .filter-buttons { 1212 justify-content: flex-start; 1213 overflow-x: auto; 1214 padding-bottom: 4px; 1215 margin-bottom: 4px; 1216 -webkit-overflow-scrolling: touch; 1217 scroll-snap-type: x mandatory; 1218 } 1219 1220 .filter-button { 1221 flex-shrink: 0; 1222 scroll-snap-align: start; 1223 } 1224 1225 .map-sidebar-toggle { 1226 top: 10px; 1227 left: 10px; 1228 z-index: 1500; 1229 } 1230 1231 .map-list-header h2 { 1232 font-size: 16px; 1233 } 1234 1235 .item-title { 1236 font-size: 15px; 1237 } 1238 1239 .item-description { 1240 font-size: 13px; 1241 } 1242 1243 .route-selector label { 1244 font-size: 13px; 1245 } 1246 1247 .routing-panel h4 { 1248 font-size: 14px; 1249 } 1250 1251 .calculate-route-button { 1252 padding: 8px; 1253 } 1254 } 1255 </style>