/ src / routes / components / MapResults.svelte
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>