Tworzenie aplikacji do wyszukiwania lokalnego za pomocą interfejsu Places UI Kit

1. Zanim zaczniesz

Z tego ćwiczenia w Codelabs dowiesz się, jak utworzyć w pełni interaktywną aplikację do wyszukiwania lokalnego za pomocą interfejsu Places UI Kit w Google Maps Platform.

Zrzut ekranu przedstawiający gotową aplikację PlaceFinder z mapą Nowego Jorku ze znacznikami, paskiem bocznym z wynikami wyszukiwania i otwartą kartą szczegółów.

Wymagania wstępne

  • projekt Google Cloud ze skonfigurowanymi niezbędnymi interfejsami API i danymi logowania;
  • Podstawowa znajomość języków HTML i CSS.
  • Znajomość nowoczesnego kodu JavaScript.
  • nowoczesna przeglądarka, np. najnowsza wersja Chrome;
  • dowolny edytor tekstu;

Jakie zadania wykonasz

  • Strukturyzowanie aplikacji do mapowania za pomocą klasy JavaScript.
  • Wyświetlanie mapy za pomocą komponentów internetowych
  • Użyj elementu wyszukiwania miejsca, aby przeprowadzić wyszukiwanie tekstowe i wyświetlić jego wyniki.
  • Programowe tworzenie niestandardowych znaczników mapy AdvancedMarkerElement i zarządzanie nimi.
  • Wyświetl element Szczegóły miejsca, gdy użytkownik wybierze lokalizację.
  • Użyj interfejsu Geocoding API, aby utworzyć dynamiczny i przyjazny dla użytkownika interfejs.

Czego potrzebujesz

  • projekt Google Cloud z włączonymi płatnościami;
  • Klucz interfejsu API Google Maps Platform
  • Identyfikator mapy
  • Włączone interfejsy API:
    • Maps JavaScript API
    • Pakiet UI do Miejsc
    • Geocoding API

2. Konfiguracja

W kolejnym kroku musisz włączyć interfejs Maps JavaScript API, pakiet UI Kit Miejsc i interfejs Geocoding API.

Konfigurowanie Google Maps Platform

Jeśli nie masz jeszcze konta Google Cloud Platform i projektu z włączonymi płatnościami, zapoznaj się z przewodnikiem Pierwsze kroki z Google Maps Platform, aby utworzyć konto rozliczeniowe i projekt.

  1. W konsoli Google Cloud kliknij menu projektu i wybierz projekt, którego chcesz użyć w tym samouczku.

  1. Włącz interfejsy API i pakiety SDK Google Maps Platform wymagane w tym samouczku w Google Cloud Marketplace. Aby to zrobić, wykonaj czynności opisane w tym filmie lub tej dokumentacji.
  2. Wygeneruj klucz interfejsu API na stronie Dane logowania w konsoli Cloud. Możesz wykonać czynności opisane w tym filmie lub tej dokumentacji. Wszystkie żądania wysyłane do Google Maps Platform wymagają klucza interfejsu API.

3. powłoka aplikacji i funkcjonalna mapa,

W pierwszym kroku utworzymy pełny układ wizualny naszej aplikacji i ustalimy przejrzystą strukturę opartą na klasach dla naszego kodu JavaScript. Daje nam to solidne podstawy do dalszego rozwoju. Po zakończeniu tej sekcji będziesz mieć stronę ze stylem, na której wyświetla się interaktywna mapa.

Tworzenie pliku HTML

Najpierw utwórz plik o nazwie index.html. Ten plik będzie zawierał pełną strukturę naszej aplikacji, w tym nagłówek, filtry wyszukiwania, pasek boczny, kontener mapy i niezbędne komponenty internetowe.

Skopiuj ten kod do pliku index.html. Pamiętaj, aby zastąpić YOUR_API_KEY_HERE własnym kluczem interfejsu Google Maps Platform API, a DEMO_MAP_ID własnym identyfikatorem mapy Google Maps Platform.

<!DOCTYPE html>
<html lang="en">
  <head>
    <title>Local Search App</title>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <!-- Google Fonts: Roboto -->
    <link rel="preconnect" href="https://fonts.googleapis.com">
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
    <link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;700&display=swap" rel="stylesheet">
    <!-- GMP Bootstrap Loader -->
    <script>
      (g=>{var h,a,k,p="The Google Maps JavaScript API",c="google",l="importLibrary",q="__ib__",m=document,b=window;b=b[c]||(b[c]={});var d=b.maps||(b.maps={}),r=new Set,e=new URLSearchParams,u=()=>h||(h=new Promise(async(f,n)=>{await (a=m.createElement("script"));e.set("libraries",[...r]+"");for(k in g)e.set(k.replace(/[A-Z]/g,t=>"_"+t[0].toLowerCase()),g[k]);e.set("callback",c+".maps."+q);a.src=`https://maps.${c}apis.com/maps/api/js?`+e;d[q]=f;a.onerror=()=>h=n(Error(p+" could not load."));a.nonce=m.querySelector("script[nonce]")?.nonce||"";m.head.append(a)}));d[l]?console.warn(p+" only loads once. Ignoring:",g):d[l]=(f,...n)=>r.add(f)&&u().then(()=>d[l](f,...n))})({
        key: "YOUR_API_KEY_HERE",
        v: "weekly",
        libraries: "places,maps,marker,geocoding"
      });
    </script>
    <link rel="stylesheet" type="text/css" href="style.css" />
  </head>
  <body>
    <!-- Header for search controls -->
    <header class="top-header">
        <div class="logo">
            <svg viewBox="0 0 24 24" width="28" height="28"><path d="M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7zm0 9.5c-1.38 0-2.5-1.12-2.5-2.5s1.12-2.5 2.5-2.5 2.5 1.12 2.5 2.5-1.12 2.5-2.5 2.5z" fill="currentColor"></path></svg>
            <span>PlaceFinder</span>
        </div>
        <div class="search-container">
            <input
              type="text"
              id="query-input"
              placeholder="e.g., burger in New York"
              value="burger"
            />
            <button id="search-button" aria-label="Search">Search</button>
        </div>
        <div class="filter-container">
            <label class="open-now-label">
              <input type="checkbox" id="open-now-filter"> Open Now
            </label>
            <select id="rating-filter" aria-label="Minimum rating">
              <option value="0" selected>Any rating</option>
              <option value="1">1+ </option>
              <option value="2">2+ ★★</option>
              <option value="3">3+ ★★★</option>
              <option value="4">4+ ★★★★</option>
              <option value="5">5 ★★★★★</option>
            </select>
             <select id="price-filter" aria-label="Price level">
              <option value="0" selected>Any Price</option>
              <option value="1">$</option>
              <option value="2">$$</option>
              <option value="3">$$$</option>
              <option value="4">$$$$</option>
            </select>
        </div>
    </header>

    <!-- Main content area -->
    <div class="app-container">
      <!-- Left Panel: Results -->
      <div class="sidebar">
          <div class="results-header">
            <h2 id="results-header-text">Results</h2>
          </div>
          <div class="results-container">
              <gmp-place-search id="place-search-list" class="hidden" selectable>
                <gmp-place-all-content></gmp-place-all-content>
                <gmp-place-text-search-request></gmp-place-text-search-request>
              </gmp-place-search>

              <div id="placeholder-message" class="placeholder">
                  <p>Your search results will appear here.</p>
              </div>

              <div id="loading-spinner" class="spinner-overlay">
                  <div class="spinner"></div>
              </div>
          </div>
      </div>

      <!-- Right Panel: Map -->
      <div class="map-container">
        <gmp-map
          center="40.758896,-73.985130"
          zoom="13"
          map-id="DEMO_MAP_ID"
        >
        </gmp-map>
        <div id="details-container">
            <gmp-place-details-compact>
                <gmp-place-details-place-request></gmp-place-details-place-request>
                <gmp-place-all-content></gmp-place-all-content>
            </gmp-place-details-compact>
        </div>
      </div>
    </div>
    <script src="script.js"></script>
  </body>
</html>

Tworzenie pliku CSS

Następnie utwórz plik o nazwie style.css. Dodamy teraz wszystkie niezbędne style, aby od początku uzyskać czysty, nowoczesny wygląd. Ten arkusz CSS odpowiada za ogólny układ, kolory, czcionki i wygląd wszystkich elementów interfejsu.

Skopiuj ten kod do pliku style.css:

/* style.css */
:root {
  --primary-color: #1a73e8;
  --text-color: #202124;
  --text-color-light: #5f6368;
  --background-color: #f8f9fa;
  --panel-background: #ffffff;
  --border-color: #dadce0;
  --shadow-color: rgba(0, 0, 0, 0.1);
}

body {
  font-family: 'Roboto', sans-serif;
  margin: 0;
  height: 100vh;
  overflow: hidden;
  display: flex;
  flex-direction: column;
  background-color: var(--background-color);
  color: var(--text-color);
}

.hidden {
  display: none !important;
}

.top-header {
  display: flex;
  align-items: center;
  padding: 12px 24px;
  border-bottom: 1px solid var(--border-color);
  background-color: var(--panel-background);
  gap: 24px;
  flex-shrink: 0;
}

.logo {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 22px;
  font-weight: 700;
  color: var(--primary-color);
}

.search-container {
  display: flex;
  flex-grow: 1;
  max-width: 720px;
}

.search-container input {
  width: 100%;
  padding: 12px 16px;
  border: 1px solid var(--border-color);
  border-radius: 8px 0 0 8px;
  font-size: 16px;
  transition: box-shadow 0.2s ease;
}

.search-container input:focus {
  outline: none;
  border-color: var(--primary-color);
  box-shadow: 0 0 0 2px rgba(26, 115, 232, 0.2);
}

.search-container button {
  padding: 0 20px;
  border: 1px solid var(--primary-color);
  border-radius: 0 8px 8px 0;
  background-color: var(--primary-color);
  color: white;
  cursor: pointer;
  font-size: 16px;
  font-weight: 500;
  transition: background-color 0.2s ease;
}

.search-container button:hover {
  background-color: #185abc;
}

.filter-container {
  display: flex;
  gap: 12px;
  align-items: center;
}

.filter-container select, .open-now-label {
  padding: 10px 14px;
  border: 1px solid var(--border-color);
  border-radius: 8px;
  background-color: var(--panel-background);
  font-size: 14px;
  cursor: pointer;
  transition: border-color 0.2s ease;
}

.filter-container select:hover, .open-now-label:hover {
  border-color: #c0c2c5;
}

.open-now-label {
  display: flex;
  align-items: center;
  gap: 8px;
  white-space: nowrap;
}

.app-container {
  display: flex;
  flex-grow: 1;
  overflow: hidden;
}

.sidebar {
  width: 35%;
  min-width: 380px;
  max-width: 480px;
  display: flex;
  flex-direction: column;
  border-right: 1px solid var(--border-color);
  background-color: var(--panel-background);
  overflow: hidden;
}

.results-header {
  padding: 16px 24px;
  border-bottom: 1px solid var(--border-color);
  flex-shrink: 0;
}

.results-header h2 {
  margin: 0;
  font-size: 18px;
  font-weight: 500;
}

.results-container {
  flex-grow: 1;
  position: relative;
  overflow-y: auto;
  overflow-x: hidden;
}

.placeholder {
  height: 100%;
  display: flex;
  justify-content: center;
  align-items: center;
  text-align: center;
  padding: 2rem;
  box-sizing: border-box;
}

.placeholder p {
  color: var(--text-color-light);
  font-size: 1.1rem;
}

gmp-place-search {
  width: 100%;
}

.map-container {
  flex-grow: 1;
  position: relative;
}

gmp-map {
  width: 100%;
  height: 100%;
}

.spinner-overlay {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background-color: rgba(255, 255, 255, 0.7);
  display: flex;
  justify-content: center;
  align-items: center;
  z-index: 100;
  opacity: 0;
  visibility: hidden;
  transition: opacity 0.3s, visibility 0.3s;
}
.spinner-overlay.visible {
  opacity: 1;
  visibility: visible;
}
.spinner {
  width: 48px;
  height: 48px;
  border: 4px solid #e0e0e0;
  border-top-color: var(--primary-color);
  border-radius: 50%;
  animation: spin 1s linear infinite;
}
@keyframes spin {
  to { transform: rotate(360deg); }
}

gmp-place-details-compact {
  width: 350px;
  display: none;
  border: none;
  border-radius: 12px;
  box-shadow: 0 4px 12px rgba(0,0,0,0.15);
}

gmp-place-details-compact::after {
    content: '';
    position: absolute;
    bottom: -12px;
    left: 50%;
    transform: translateX(-50%);
    width: 24px;
    height: 12px;
    background-color: var(--panel-background);
    clip-path: polygon(50% 100%, 0 0, 100% 0);
}

Tworzenie klasy aplikacji JavaScript

Na koniec utwórz plik o nazwie script.js. Nasza aplikacja będzie miała strukturę klasy JavaScript o nazwie PlaceFinderApp. Dzięki temu nasz kod jest uporządkowany, a stan jest zarządzany w sposób przejrzysty.

Ten początkowy kod zdefiniuje klasę, znajdzie wszystkie elementy HTML w constructor i utworzy metodę init() do wczytywania bibliotek Google Maps Platform.

Skopiuj ten kod do pliku script.js:

// script.js
class PlaceFinderApp {
  constructor() {
    // Get all DOM element references
    this.queryInput = document.getElementById('query-input');
    this.priceFilter = document.getElementById('price-filter');
    this.ratingFilter = document.getElementById('rating-filter');
    this.openNowFilter = document.getElementById('open-now-filter');
    this.searchButton = document.getElementById('search-button');
    this.placeSearch = document.getElementById('place-search-list');
    this.gMap = document.querySelector('gmp-map');
    this.loadingSpinner = document.getElementById('loading-spinner');
    this.resultsHeaderText = document.getElementById('results-header-text');
    this.placeholderMessage = document.getElementById('placeholder-message');
    this.placeDetailsWidget = document.querySelector('gmp-place-details-compact');
    this.placeDetailsRequest = this.placeDetailsWidget.querySelector('gmp-place-details-place-request');
    this.searchRequest = this.placeSearch.querySelector('gmp-place-text-search-request');

    // Initialize instance variables
    this.map = null;
    this.geocoder = null;
    this.markers = {};
    this.detailsPopup = null;
    this.PriceLevel = null;
    this.isSearchInProgress = false;

    // Start the application
    this.init();
  }

  async init() {
    // Import libraries
    await google.maps.importLibrary("maps");
    const { Place, PriceLevel } = await google.maps.importLibrary("places");
    const { AdvancedMarkerElement } = await google.maps.importLibrary("marker");
    const { Geocoder } = await google.maps.importLibrary("geocoding");

    // Make classes available to the instance
    this.PriceLevel = PriceLevel;
    this.AdvancedMarkerElement = AdvancedMarkerElement;

    this.map = this.gMap.innerMap;
    this.geocoder = new Geocoder();

    // We will add more initialization logic here in later steps.
  }
}

// Wait for the DOM to be ready, then create an instance of our app.
window.addEventListener('DOMContentLoaded', () => {
  new PlaceFinderApp();
});

Ograniczenia klucza interfejsu API

Aby ten Codelab działał, może być konieczne dodanie nowego ograniczenia do klucza interfejsu API. Więcej informacji i wskazówki znajdziesz w artykule Ograniczanie kluczy interfejsu API.

Sprawdzanie efektów pracy

Otwórz index.html w przeglądarce. Powinna się wyświetlić strona z nagłówkiem zawierającym pasek wyszukiwania i filtry, paskiem bocznym z komunikatem „Wyniki wyszukiwania pojawią się tutaj” oraz dużą mapą z Nowym Jorkiem w centrum. Na tym etapie elementy sterujące wyszukiwania nie działają jeszcze.

4. Implementowanie funkcji wyszukiwania

W tej sekcji ożywimy naszą aplikację, wdrażając podstawową funkcję wyszukiwania. Napiszemy kod, który będzie uruchamiany, gdy użytkownik kliknie przycisk „Szukaj”. Od początku będziemy tworzyć tę funkcję zgodnie z najlepszymi praktykami, aby sprawnie obsługiwać interakcje użytkowników i zapobiegać typowym błędom, takim jak wyścigi.

Po wykonaniu tego kroku możesz kliknąć przycisk wyszukiwania i zobaczyć wskaźnik ładowania, który będzie się wyświetlać, gdy aplikacja będzie pobierać dane w tle.

Tworzenie metody wyszukiwania

Najpierw określ metodę performSearch w klasie PlaceFinderApp. Ta funkcja będzie podstawą naszej logiki wyszukiwania. Wprowadzimy też zmienną instancji isSearchInProgress, która będzie pełnić rolę „strażnika”. Zapobiega to rozpoczęciu przez użytkownika nowego wyszukiwania, gdy jedno jest już w toku, co może prowadzić do błędów.

Logika w performSearch może wydawać się skomplikowana, więc ją rozbijemy:

  1. Najpierw sprawdza, czy wyszukiwanie jest już w toku. Jeśli tak, nie robi nic.
  2. Ustawia flagę isSearchInProgress na true, aby „zablokować” funkcję.
  3. Wyświetla spinner ładowania i przygotowuje interfejs do wyświetlenia nowych wyników.
  4. Ustawia właściwość textQuery żądania wyszukiwania na null. To kluczowy krok, który zmusza komponent internetowy do rozpoznania, że nadchodzi nowe żądanie.
  5. Używa przy tym metody setTimeout z opóźnieniem 0. Ta standardowa technika JavaScriptu planuje wykonanie pozostałej części kodu w następnym zadaniu przeglądarki, dzięki czemu komponent najpierw przetworzy wartość null. Nawet jeśli użytkownik wyszuka dokładnie to samo 2 razy, zawsze zostanie uruchomione nowe wyszukiwanie.

Dodawanie detektorów zdarzeń

Następnie musimy wywołać metodę performSearch, gdy użytkownik wchodzi w interakcję z aplikacją. Aby cały kod obsługi zdarzeń był w jednym miejscu, utworzymy nową metodę attachEventListeners. Na razie dodamy tylko detektor zdarzenia click przycisku wyszukiwania. Dodamy też symbol zastępczy dla innego zdarzenia, gmp-load, którego użyjemy w następnym kroku.

Aktualizowanie pliku JavaScript

Zaktualizuj plik script.js tym kodem. Nowe lub zmienione sekcje to metoda attachEventListeners i metoda performSearch.

// script.js
class PlaceFinderApp {
  constructor() {
    // Get all DOM element references
    this.queryInput = document.getElementById('query-input');
    this.priceFilter = document.getElementById('price-filter');
    this.ratingFilter = document.getElementById('rating-filter');
    this.openNowFilter = document.getElementById('open-now-filter');
    this.searchButton = document.getElementById('search-button');
    this.placeSearch = document.getElementById('place-search-list');
    this.gMap = document.querySelector('gmp-map');
    this.loadingSpinner = document.getElementById('loading-spinner');
    this.resultsHeaderText = document.getElementById('results-header-text');
    this.placeholderMessage = document.getElementById('placeholder-message');
    this.placeDetailsWidget = document.querySelector('gmp-place-details-compact');
    this.placeDetailsRequest = this.placeDetailsWidget.querySelector('gmp-place-details-place-request');
    this.searchRequest = this.placeSearch.querySelector('gmp-place-text-search-request');

    // Initialize instance variables
    this.map = null;
    this.geocoder = null;
    this.markers = {};
    this.detailsPopup = null;
    this.PriceLevel = null;
    this.isSearchInProgress = false;

    // Start the application
    this.init();
  }

  async init() {
    // Import libraries
    await google.maps.importLibrary("maps");
    const { Place, PriceLevel } = await google.maps.importLibrary("places");
    const { AdvancedMarkerElement } = await google.maps.importLibrary("marker");
    const { Geocoder } = await google.maps.importLibrary("geocoding");

    // Make classes available to the instance
    this.PriceLevel = PriceLevel;
    this.AdvancedMarkerElement = AdvancedMarkerElement;

    this.map = this.gMap.innerMap;
    this.geocoder = new Geocoder();

    // Call the new method to set up listeners
    this.attachEventListeners();
  }

  // NEW: Method to set up all event listeners
  attachEventListeners() {
    this.searchButton.addEventListener('click', this.performSearch.bind(this));
    // We will add the gmp-load listener in the next step
  }

  // NEW: Core search method
  async performSearch() {
    // Exit if a search is already in progress
    if (this.isSearchInProgress) {
      return;
    }
    // Set the lock
    this.isSearchInProgress = true;

    // Show the placeholder and spinner
    this.placeholderMessage.classList.add('hidden');
    this.placeSearch.classList.remove('hidden');
    this.showLoading(true);

    // Force a state change by clearing the query first.
    this.searchRequest.textQuery = null;

    // Defer setting the real properties to the next event loop cycle.
    setTimeout(async () => {
      const rawQuery = this.queryInput.value.trim();
      // If the query is empty, release the lock and hide the spinner
      if (!rawQuery) {
          this.showLoading(false);
          this.isSearchInProgress = false;
          return;
      };

      // For now, we just set the textQuery. We'll add filters later.
      this.searchRequest.textQuery = rawQuery;
      this.searchRequest.locationRestriction = this.map.getBounds();
    }, 0);
  }

  // NEW: Helper method to show/hide the spinner
  showLoading(visible) {
    this.loadingSpinner.classList.toggle('visible', visible);
  }
}

// Wait for the DOM to be ready, then create an instance of our app.
window.addEventListener('DOMContentLoaded', () => {
  new PlaceFinderApp();
});

Sprawdzanie efektów pracy

Zapisz plik script.js i odśwież go index.html w przeglądarce. Strona powinna wyglądać tak samo jak wcześniej. Teraz kliknij przycisk „Szukaj” w nagłówku.

Powinny nastąpić 2 rzeczy:

  1. Zniknie komunikat zastępczy „Tutaj wyświetlą się wyniki wyszukiwania”.
  2. Pojawi się wskaźnik wczytywania, który będzie się obracać.

Spinner będzie się kręcić w nieskończoność, ponieważ nie powiedzieliśmy mu jeszcze, kiedy ma się zatrzymać. Zrobimy to w następnej sekcji, w której wyświetlimy wyniki. Potwierdza to, że nasza funkcja wyszukiwania jest wywoływana prawidłowo.

5. Wyświetlanie wyników i dodawanie znaczników

Teraz, gdy wyzwalacz wyszukiwania działa, następnym zadaniem jest wyświetlenie wyników na ekranie. Kod w tej sekcji połączy logikę wyszukiwania z interfejsem. Gdy element wyszukiwania miejsca zakończy wczytywanie danych, zwolni „blokadę” wyszukiwania, ukryje wskaźnik wczytywania i wyświetli na mapie znacznik dla każdego wyniku.

Oczekiwanie na zakończenie wyszukiwania

Element wyszukiwania miejsca wywołuje zdarzenie gmp-load, gdy pobierze dane. To idealny sygnał, który pozwala nam przetworzyć wyniki.

Najpierw dodaj detektor zdarzeń dla tego zdarzenia w naszej metodzie attachEventListeners.

Tworzenie metod obsługi markerów

Następnie utworzymy 2 nowe metody pomocnicze: clearMarkersaddMarkers.

  • clearMarkers() usunie wszystkie znaczniki z poprzedniego wyszukiwania.
  • addMarkers() zostanie wywołana przez gmp-load. Pętla będzie przechodzić przez listę miejsc zwróconych przez wyszukiwarkę i tworzyć nowy element AdvancedMarkerElement dla każdego z nich. W tym miejscu ukryjemy też spinner ładowania i zwolnimy blokadę isSearchInProgress, co zakończy cykl wyszukiwania.

Zwróć uwagę, że znaczniki przechowujemy w obiekcie (this.markers), używając identyfikatora miejsca jako klucza. Jest to sposób zarządzania markerami, który umożliwia późniejsze znalezienie konkretnego markera.

Na koniec musimy wywołać funkcję clearMarkers() na początku każdego nowego wyszukiwania. Najlepszym miejscem na to jest performSearch.

Aktualizowanie pliku JavaScript

Zaktualizuj plik script.js, dodając nowe metody oraz zmiany w attachEventListenersperformSearch.

// script.js
class PlaceFinderApp {
  constructor() {
    // Get all DOM element references
    this.queryInput = document.getElementById('query-input');
    this.priceFilter = document.getElementById('price-filter');
    this.ratingFilter = document.getElementById('rating-filter');
    this.openNowFilter = document.getElementById('open-now-filter');
    this.searchButton = document.getElementById('search-button');
    this.placeSearch = document.getElementById('place-search-list');
    this.gMap = document.querySelector('gmp-map');
    this.loadingSpinner = document.getElementById('loading-spinner');
    this.resultsHeaderText = document.getElementById('results-header-text');
    this.placeholderMessage = document.getElementById('placeholder-message');
    this.placeDetailsWidget = document.querySelector('gmp-place-details-compact');
    this.placeDetailsRequest = this.placeDetailsWidget.querySelector('gmp-place-details-place-request');
    this.searchRequest = this.placeSearch.querySelector('gmp-place-text-search-request');

    // Initialize instance variables
    this.map = null;
    this.geocoder = null;
    this.markers = {};
    this.detailsPopup = null;
    this.PriceLevel = null;
    this.isSearchInProgress = false;

    // Start the application
    this.init();
  }

  async init() {
    // Import libraries
    await google.maps.importLibrary("maps");
    const { Place, PriceLevel } = await google.maps.importLibrary("places");
    const { AdvancedMarkerElement } = await google.maps.importLibrary("marker");
    const { Geocoder } = await google.maps.importLibrary("geocoding");

    // Make classes available to the instance
    this.PriceLevel = PriceLevel;
    this.AdvancedMarkerElement = AdvancedMarkerElement;

    this.map = this.gMap.innerMap;
    this.geocoder = new Geocoder();

    this.attachEventListeners();
  }

  attachEventListeners() {
    this.searchButton.addEventListener('click', this.performSearch.bind(this));
    // NEW: Listen for when the search component has loaded results
    this.placeSearch.addEventListener('gmp-load', this.addMarkers.bind(this));
  }

  // NEW: Method to clear markers from a previous search
  clearMarkers() {
    for (const marker of Object.values(this.markers)) {
      marker.map = null;
    }
    this.markers = {};
  }

  // NEW: Method to add markers for new search results
  addMarkers() {
    // Release the lock and hide the spinner
    this.isSearchInProgress = false;
    this.showLoading(false);

    const places = this.placeSearch.places;
    if (!places || places.length === 0) return;

    // Create a new marker for each place result
    for (const place of places) {
      if (!place.location || !place.id) continue;
      const marker = new this.AdvancedMarkerElement({
        map: this.map,
        position: place.location,
        title: place.displayName,
      });
      // Store marker by its place ID for access later
      this.markers[place.id] = marker;
    }
  }

  async performSearch() {
    if (this.isSearchInProgress) {
      return;
    }
    this.isSearchInProgress = true;
    this.placeholderMessage.classList.add('hidden');
    this.placeSearch.classList.remove('hidden');
    this.showLoading(true);

    // NEW: Clear old markers before starting a new search
    this.clearMarkers();

    this.searchRequest.textQuery = null;

    setTimeout(async () => {
      const rawQuery = this.queryInput.value.trim();
      if (!rawQuery) {
          this.showLoading(false);
          this.isSearchInProgress = false;
          return;
      };

      this.searchRequest.textQuery = rawQuery;
      this.searchRequest.locationRestriction = this.map.getBounds();
    }, 0);
  }

  showLoading(visible) {
    this.loadingSpinner.classList.toggle('visible', visible);
  }
}

window.addEventListener('DOMContentLoaded', () => {
  new PlaceFinderApp();
});

Sprawdzanie efektów pracy

Zapisz pliki i odśwież stronę w przeglądarce. Kliknij przycisk „Szukaj”.

Wskaźnik wczytywania powinien się na chwilę pojawić, a potem zniknąć. Na pasku bocznym pojawi się lista miejsc powiązanych z wyszukiwanym hasłem, a na mapie powinny się wyświetlić odpowiednie znaczniki. Markery nie reagują jeszcze na kliknięcie. Dodamy tę interaktywność w następnej sekcji.

6. Aktywowanie filtrów wyszukiwania i interaktywności listy

Nasza aplikacja może teraz wyświetlać wyniki wyszukiwania, ale nie jest jeszcze interaktywna. W tej sekcji omówimy wszystkie ustawienia użytkownika. Aktywujemy filtry, włączymy wyszukiwanie za pomocą klawisza „Enter” i połączymy elementy na liście wyników z odpowiednimi lokalizacjami na mapie.

Po wykonaniu tego kroku aplikacja będzie w pełni reagować na dane wejściowe użytkownika.

Aktywowanie filtrów wyszukiwania

Najpierw metoda performSearch zostanie zaktualizowana, aby odczytywać wartości ze wszystkich elementów sterujących filtrami w nagłówku. W przypadku każdego filtra (cena, ocena i „Otwarte teraz”) odpowiednia właściwość zostanie ustawiona w obiekcie searchRequest przed wykonaniem wyszukiwania.

Dodawanie detektorów zdarzeń do wszystkich elementów sterujących

Następnie rozszerzymy naszą metodę attachEventListeners. Do każdego elementu sterującego filtrem dodamy detektory zdarzeń change, a do pola wyszukiwania detektor keydown, aby wykrywać, kiedy użytkownik naciśnie klawisz „Enter”. Wszyscy nowi słuchacze będą wywoływać metodę performSearch.

Połącz listę wyników z mapą

Aby zapewnić płynność działania, kliknięcie elementu na liście wyników na pasku bocznym powinno powodować skupienie mapy na tej lokalizacji.

Nowa metoda handleResultClick będzie nasłuchiwać zdarzenia gmp-select, które jest wywoływane przez element wyszukiwania miejsc po kliknięciu elementu. Ta funkcja znajdzie lokalizację powiązanego miejsca i płynnie przesunie do niej mapę.

Aby to działało, upewnij się, że atrybut selectable jest obecny w komponencie gmp-place-search w index.html.

<gmp-place-search id="place-search-list" class="hidden" selectable>
    <gmp-place-all-content></gmp-place-all-content>
    <gmp-place-text-search-request></gmp-place-text-search-request>
</gmp-place-search>

Aktualizowanie pliku JavaScript

Zaktualizuj plik script.js, dodając ten kompletny kod. Ta wersja zawiera nową metodę handleResultClick oraz zaktualizowaną logikę w przypadku attachEventListenersperformSearch.

// script.js
class PlaceFinderApp {
  constructor() {
    // Get all DOM element references
    this.queryInput = document.getElementById('query-input');
    this.priceFilter = document.getElementById('price-filter');
    this.ratingFilter = document.getElementById('rating-filter');
    this.openNowFilter = document.getElementById('open-now-filter');
    this.searchButton = document.getElementById('search-button');
    this.placeSearch = document.getElementById('place-search-list');
    this.gMap = document.querySelector('gmp-map');
    this.loadingSpinner = document.getElementById('loading-spinner');
    this.resultsHeaderText = document.getElementById('results-header-text');
    this.placeholderMessage = document.getElementById('placeholder-message');
    this.placeDetailsWidget = document.querySelector('gmp-place-details-compact');
    this.placeDetailsRequest = this.placeDetailsWidget.querySelector('gmp-place-details-place-request');
    this.searchRequest = this.placeSearch.querySelector('gmp-place-text-search-request');

    // Initialize instance variables
    this.map = null;
    this.geocoder = null;
    this.markers = {};
    this.detailsPopup = null;
    this.PriceLevel = null;
    this.isSearchInProgress = false;

    // Start the application
    this.init();
  }

  async init() {
    // Import libraries
    await google.maps.importLibrary("maps");
    const { Place, PriceLevel } = await google.maps.importLibrary("places");
    const { AdvancedMarkerElement } = await google.maps.importLibrary("marker");
    const { Geocoder } = await google.maps.importLibrary("geocoding");

    // Make classes available to the instance
    this.PriceLevel = PriceLevel;
    this.AdvancedMarkerElement = AdvancedMarkerElement;

    this.map = this.gMap.innerMap;
    this.geocoder = new Geocoder();

    this.attachEventListeners();
  }

  // UPDATED: All event listeners are now attached
  attachEventListeners() {
    // Listen for the 'Enter' key press in the search input
    this.queryInput.addEventListener('keydown', (event) => {
      if (event.key === 'Enter') {
        event.preventDefault();
        this.performSearch();
      }
    });

    // Listen for a sidebar result click
    this.placeSearch.addEventListener('gmp-select', this.handleResultClick.bind(this));

    this.placeSearch.addEventListener('gmp-load', this.addMarkers.bind(this));
    this.searchButton.addEventListener('click', this.performSearch.bind(this));
    this.priceFilter.addEventListener('change', this.performSearch.bind(this));
    this.ratingFilter.addEventListener('change', this.performSearch.bind(this));
    this.openNowFilter.addEventListener('change', this.performSearch.bind(this));
  }

  clearMarkers() {
    for (const marker of Object.values(this.markers)) {
      marker.map = null;
    }
    this.markers = {};
  }

  addMarkers() {
    this.isSearchInProgress = false;
    this.showLoading(false);

    const places = this.placeSearch.places;
    if (!places || places.length === 0) return;

    for (const place of places) {
      if (!place.location || !place.id) continue;
      const marker = new this.AdvancedMarkerElement({
        map: this.map,
        position: place.location,
        title: place.displayName,
      });
      this.markers[place.id] = marker;
    }
  }

  // NEW: Function to handle clicks on the results list
  handleResultClick(event) {
    const place = event.place;
    if (!place || !place.location) return;
    // Pan the map to the selected place
    this.map.panTo(place.location);
  }

  // UPDATED: Search function now includes all filters
  async performSearch() {
    if (this.isSearchInProgress) {
      return;
    }
    this.isSearchInProgress = true;
    this.placeholderMessage.classList.add('hidden');
    this.placeSearch.classList.remove('hidden');
    this.showLoading(true);
    this.clearMarkers();

    this.searchRequest.textQuery = null;

    setTimeout(async () => {
      const rawQuery = this.queryInput.value.trim();
      if (!rawQuery) {
          this.showLoading(false);
          this.isSearchInProgress = false;
          return;
      };

      this.searchRequest.textQuery = rawQuery;
      this.searchRequest.locationRestriction = this.map.getBounds();

      // Add filter values to the request
      const selectedPrice = this.priceFilter.value;
      let priceLevels = [];
      switch (selectedPrice) {
        case "1": priceLevels = [this.PriceLevel.INEXPENSIVE]; break;
        case "2": priceLevels = [this.PriceLevel.MODERATE]; break;
        case "3": priceLevels = [this.PriceLevel.EXPENSIVE]; break;
        case "4": priceLevels = [this.PriceLevel.VERY_EXPENSIVE]; break;
        default: priceLevels = null; break;
      }
      this.searchRequest.priceLevels = priceLevels;

      const selectedRating = parseFloat(this.ratingFilter.value);
      this.searchRequest.minRating = selectedRating > 0 ? selectedRating : null;
      this.searchRequest.isOpenNow = this.openNowFilter.checked ? true : null;
    }, 0);
  }

  showLoading(visible) {
    this.loadingSpinner.classList.toggle('visible', visible);
  }
}

window.addEventListener('DOMContentLoaded', () => {
  new PlaceFinderApp();
});

Sprawdzanie efektów pracy

Zapisz plik script.js i odśwież stronę. Aplikacja powinna być teraz bardzo interaktywna.

Potwierdź te informacje:

  • Wyszukiwanie przez naciśnięcie klawisza „Enter” w polu wyszukiwania działa.
  • Zmiana dowolnego filtra (Cena, Ocena, Otwarte teraz) powoduje nowe wyszukiwanie i aktualizację wyników.
  • Kliknięcie elementu na liście wyników na pasku bocznym powoduje teraz płynne przesuwanie mapy do lokalizacji tego elementu.

W następnej sekcji zaimplementujemy kartę szczegółów, która będzie się pojawiać po kliknięciu znacznika.

7. Implementowanie elementu Informacje o miejscu

Nasza aplikacja jest już w pełni interaktywna, ale brakuje jej kluczowej funkcji: możliwości wyświetlania większej ilości informacji o wybranym miejscu. W tej sekcji zaimplementujemy element szczegółów miejsca, który będzie się wyświetlać, gdy użytkownik kliknie znacznik na mapie lub wybierze element w elemencie wyszukiwania miejsca.

Tworzenie kontenera karty szczegółów wielokrotnego użytku

Najskuteczniejszym sposobem wyświetlania szczegółów miejsca na mapie jest utworzenie jednego kontenera wielokrotnego użytku. Użyjemy AdvancedMarkerElement jako tego kontenera. Jego zawartością będzie ukryty widżet gmp-place-details-compact, który mamy już w index.html.

Nowa metoda initDetailsPopup będzie obsługiwać tworzenie tego markera wielokrotnego użytku. Zostanie utworzony raz podczas wczytywania aplikacji i będzie początkowo ukryty. Dodamy też odbiornik do głównej mapy, aby kliknięcie dowolnego miejsca na mapie powodowało ukrycie karty szczegółów.

Aktualizowanie zachowania po kliknięciu znacznika

Następnie musimy zaktualizować działanie po kliknięciu przez użytkownika znacznika miejsca. Za wyświetlanie karty szczegółów będzie teraz odpowiadać odbiornik 'click' w metodzie addMarkers.

Gdy użytkownik kliknie marker, odbiornik:

  1. Przesuń mapę do lokalizacji znacznika.
  2. Zaktualizuj kartę szczegółów, podając informacje o tym konkretnym miejscu.
  3. Umieść kartę szczegółów w lokalizacji znacznika i spraw, aby była widoczna.

Połącz kliknięcie listy z kliknięciem znacznika

Na koniec zaktualizujemy metodę handleResultClick. Zamiast tylko przesuwać mapę, będzie teraz programowo wywoływać zdarzenie click na odpowiednim znaczniku. Jest to zaawansowany wzorzec, który pozwala nam używać tej samej logiki w przypadku obu interakcji, dzięki czemu nasz kod jest przejrzysty i łatwy w utrzymaniu.

Aktualizowanie pliku JavaScript

Zaktualizuj plik script.js tym kodem. Nowe lub zmienione sekcje to metoda initDetailsPopup oraz zaktualizowane metody addMarkershandleResultClick.

// script.js
class PlaceFinderApp {
  constructor() {
    // Get all DOM element references
    this.queryInput = document.getElementById('query-input');
    this.priceFilter = document.getElementById('price-filter');
    this.ratingFilter = document.getElementById('rating-filter');
    this.openNowFilter = document.getElementById('open-now-filter');
    this.searchButton = document.getElementById('search-button');
    this.placeSearch = document.getElementById('place-search-list');
    this.gMap = document.querySelector('gmp-map');
    this.loadingSpinner = document.getElementById('loading-spinner');
    this.resultsHeaderText = document.getElementById('results-header-text');
    this.placeholderMessage = document.getElementById('placeholder-message');
    this.placeDetailsWidget = document.querySelector('gmp-place-details-compact');
    this.placeDetailsRequest = this.placeDetailsWidget.querySelector('gmp-place-details-place-request');
    this.searchRequest = this.placeSearch.querySelector('gmp-place-text-search-request');

    // Initialize instance variables
    this.map = null;
    this.geocoder = null;
    this.markers = {};
    this.detailsPopup = null;
    this.PriceLevel = null;
    this.isSearchInProgress = false;

    // Start the application
    this.init();
  }

  async init() {
    // Import libraries
    await google.maps.importLibrary("maps");
    const { Place, PriceLevel } = await google.maps.importLibrary("places");
    const { AdvancedMarkerElement } = await google.maps.importLibrary("marker");
    const { Geocoder } = await google.maps.importLibrary("geocoding");

    // Make classes available to the instance
    this.PriceLevel = PriceLevel;
    this.AdvancedMarkerElement = AdvancedMarkerElement;

    this.map = this.gMap.innerMap;
    this.geocoder = new Geocoder();

    // NEW: Call the method to initialize the details card
    this.initDetailsPopup();
    this.attachEventListeners();
  }

  attachEventListeners() {
    this.queryInput.addEventListener('keydown', (event) => {
      if (event.key === 'Enter') {
        event.preventDefault();
        this.performSearch();
      }
    });
    this.placeSearch.addEventListener('gmp-select', this.handleResultClick.bind(this));
    this.placeSearch.addEventListener('gmp-load', this.addMarkers.bind(this));
    this.searchButton.addEventListener('click', this.performSearch.bind(this));
    this.priceFilter.addEventListener('change', this.performSearch.bind(this));
    this.ratingFilter.addEventListener('change', this.performSearch.bind(this));
    this.openNowFilter.addEventListener('change', this.performSearch.bind(this));
  }

  // NEW: Method to set up the reusable details card
  initDetailsPopup() {
    this.detailsPopup = new this.AdvancedMarkerElement({
      content: this.placeDetailsWidget,
      map: null,
      zIndex: 100
    });
    this.map.addListener('click', () => { this.detailsPopup.map = null; });
  }

  clearMarkers() {
    for (const marker of Object.values(this.markers)) {
      marker.map = null;
    }
    this.markers = {};
  }

  // UPDATED: The marker's click listener now shows the details card
  addMarkers() {
    this.isSearchInProgress = false;
    this.showLoading(false);

    const places = this.placeSearch.places;
    if (!places || places.length === 0) return;

    for (const place of places) {
      if (!place.location || !place.id) continue;
      const marker = new this.AdvancedMarkerElement({
        map: this.map,
        position: place.location,
        title: place.displayName,
      });
      // Add the click listener to show the details card
      marker.addListener('click', (event) => {
        event.stop();
        this.map.panTo(place.location);
        this.placeDetailsRequest.place = place;
        this.placeDetailsWidget.style.display = 'block';
        this.detailsPopup.position = place.location;
        this.detailsPopup.map = this.map;
      });
      this.markers[place.id] = marker;
    }
  }

  // UPDATED: This now triggers the marker's click event
  handleResultClick(event) {
    const place = event.place;
    if (!place || !place.id) return;
    const marker = this.markers[place.id];
    if (marker) {
      // Programmatically trigger the marker's click event
      marker.click();
    }
  }

  async performSearch() {
    if (this.isSearchInProgress) return;
    this.isSearchInProgress = true;
    this.placeholderMessage.classList.add('hidden');
    this.placeSearch.classList.remove('hidden');
    this.showLoading(true);
    this.clearMarkers();
    // Hide the details card when a new search starts
    if (this.detailsPopup) this.detailsPopup.map = null;

    this.searchRequest.textQuery = null;

    setTimeout(async () => {
      const rawQuery = this.queryInput.value.trim();
      if (!rawQuery) {
          this.showLoading(false);
          this.isSearchInProgress = false;
          return;
      };

      this.searchRequest.textQuery = rawQuery;
      this.searchRequest.locationRestriction = this.map.getBounds();

      const selectedPrice = this.priceFilter.value;
      let priceLevels = [];
      switch (selectedPrice) {
        case "1": priceLevels = [this.PriceLevel.INEXPENSIVE]; break;
        case "2": priceLevels = [this.PriceLevel.MODERATE]; break;
        case "3": priceLevels = [this.PriceLevel.EXPENSIVE]; break;
        case "4": priceLevels = [this.PriceLevel.VERY_EXPENSIVE]; break;
        default: priceLevels = null; break;
      }
      this.searchRequest.priceLevels = priceLevels;

      const selectedRating = parseFloat(this.ratingFilter.value);
      this.searchRequest.minRating = selectedRating > 0 ? selectedRating : null;
      this.searchRequest.isOpenNow = this.openNowFilter.checked ? true : null;
    }, 0);
  }

  showLoading(visible) {
    this.loadingSpinner.classList.toggle('visible', visible);
  }
}

window.addEventListener('DOMContentLoaded', () => {
  new PlaceFinderApp();
});

Sprawdzanie efektów pracy

Zapisz plik script.js i odśwież stronę. Aplikacja powinna teraz wyświetlać szczegóły na żądanie.

Potwierdź te informacje:

  • Kliknięcie znacznika na mapie powoduje teraz wyśrodkowanie mapy i otwarcie nad znacznikiem karty ze stylizowanymi szczegółami.
  • Kliknięcie elementu na liście wyników na pasku bocznym daje dokładnie ten sam efekt.
  • Kliknięcie mapy poza kartą spowoduje jej zamknięcie.
  • Rozpoczęcie nowego wyszukiwania powoduje też zamknięcie otwartej karty szczegółów.

8. Dodaj ostatnie szlify

Nasza aplikacja jest już w pełni funkcjonalna, ale możemy jeszcze wprowadzić kilka poprawek, aby zwiększyć zadowolenie użytkowników. W tej ostatniej sekcji wdrożymy 2 kluczowe funkcje: dynamiczny nagłówek, który zapewnia lepszy kontekst wyników wyszukiwania, oraz automatyczne formatowanie zapytania użytkownika.

Tworzenie dynamicznego nagłówka wyników

Obecnie nagłówek paska bocznego zawsze brzmi „Wyniki”. Możemy to ulepszyć, aktualizując je tak, aby odzwierciedlało bieżące wyszukiwanie. Na przykład „Burgery w pobliżu Warszawy”.

W tym celu użyjemy interfejsu Geocoding API, aby przekonwertować współrzędne środka mapy na czytelną dla człowieka lokalizację, np. nazwę miasta. Tą logiką będzie się zajmować nowa metoda async o nazwie updateResultsHeader. Będzie ona wywoływana przy każdym wyszukiwaniu.

Formatowanie zapytania użytkownika

Aby interfejs wyglądał przejrzyście i spójnie, automatycznie sformatujemy wyszukiwane hasło użytkownika w formacie „Title Case” (np. „burger restaurant” zmieni się na „Burger Restaurant”. Przekształceniem tym zajmie się funkcja pomocnicza toTitleCase. Metoda performSearch zostanie zaktualizowana, aby używać tej funkcji w danych wejściowych użytkownika przed przeprowadzeniem wyszukiwania i zaktualizowaniem nagłówka.

Aktualizowanie pliku JavaScript

Zaktualizuj plik script.js ostateczną wersją kodu. Obejmuje to nowe metody toTitleCaseupdateResultsHeader oraz zaktualizowaną metodę performSearch, która je integruje.

// script.js
class PlaceFinderApp {
  constructor() {
    // Get all DOM element references
    this.queryInput = document.getElementById('query-input');
    this.priceFilter = document.getElementById('price-filter');
    this.ratingFilter = document.getElementById('rating-filter');
    this.openNowFilter = document.getElementById('open-now-filter');
    this.searchButton = document.getElementById('search-button');
    this.placeSearch = document.getElementById('place-search-list');
    this.gMap = document.querySelector('gmp-map');
    this.loadingSpinner = document.getElementById('loading-spinner');
    this.resultsHeaderText = document.getElementById('results-header-text');
    this.placeholderMessage = document.getElementById('placeholder-message');
    this.placeDetailsWidget = document.querySelector('gmp-place-details-compact');
    this.placeDetailsRequest = this.placeDetailsWidget.querySelector('gmp-place-details-place-request');
    this.searchRequest = this.placeSearch.querySelector('gmp-place-text-search-request');

    // Initialize instance variables
    this.map = null;
    this.geocoder = null;
    this.markers = {};
    this.detailsPopup = null;
    this.PriceLevel = null;
    this.isSearchInProgress = false;

    // Start the application
    this.init();
  }

  async init() {
    // Import libraries
    await google.maps.importLibrary("maps");
    const { Place, PriceLevel } = await google.maps.importLibrary("places");
    const { AdvancedMarkerElement } = await google.maps.importLibrary("marker");
    const { Geocoder } = await google.maps.importLibrary("geocoding");

    // Make classes available to the instance
    this.PriceLevel = PriceLevel;
    this.AdvancedMarkerElement = AdvancedMarkerElement;

    this.map = this.gMap.innerMap;
    this.geocoder = new Geocoder();

    this.initDetailsPopup();
    this.attachEventListeners();
  }

  attachEventListeners() {
    this.queryInput.addEventListener('keydown', (event) => {
      if (event.key === 'Enter') {
        event.preventDefault();
        this.performSearch();
      }
    });
    this.placeSearch.addEventListener('gmp-select', this.handleResultClick.bind(this));
    this.placeSearch.addEventListener('gmp-load', this.addMarkers.bind(this));
    this.searchButton.addEventListener('click', this.performSearch.bind(this));
    this.priceFilter.addEventListener('change', this.performSearch.bind(this));
    this.ratingFilter.addEventListener('change', this.performSearch.bind(this));
    this.openNowFilter.addEventListener('change', this.performSearch.bind(this));
  }

  initDetailsPopup() {
    this.detailsPopup = new this.AdvancedMarkerElement({
      content: this.placeDetailsWidget,
      map: null,
      zIndex: 100
    });
    this.map.addListener('click', () => { this.detailsPopup.map = null; });
  }

  // NEW: Helper function to format text to Title Case
  toTitleCase(str) {
    if (!str) return '';
    return str.toLowerCase().split(' ').map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(' ');
  }

  showLoading(visible) {
    this.loadingSpinner.classList.toggle('visible', visible);
  }

  clearMarkers() {
    for (const marker of Object.values(this.markers)) { marker.map = null; }
    this.markers = {};
  }

  addMarkers() {
    this.isSearchInProgress = false;
    this.showLoading(false);

    const places = this.placeSearch.places;
    if (!places || places.length === 0) return;

    for (const place of places) {
      if (!place.location || !place.id) continue;
      const marker = new this.AdvancedMarkerElement({
        map: this.map,
        position: place.location,
        title: place.displayName,
      });
      marker.addListener('click', (event) => {
        event.stop();
        this.map.panTo(place.location);
        this.placeDetailsRequest.place = place;
        this.placeDetailsWidget.style.display = 'block';
        this.detailsPopup.position = place.location;
        this.detailsPopup.map = this.map;
      });
      this.markers[place.id] = marker;
    }
  }

  handleResultClick(event) {
    const place = event.place;
    if (!place || !place.id) return;
    const marker = this.markers[place.id];
    if (marker) {
      marker.click();
    }
  }

  // UPDATED: Now integrates formatting and the dynamic header
  async performSearch() {
    if (this.isSearchInProgress) return;
    this.isSearchInProgress = true;
    this.placeholderMessage.classList.add('hidden');
    this.placeSearch.classList.remove('hidden');
    this.showLoading(true);
    this.clearMarkers();
    if (this.detailsPopup) this.detailsPopup.map = null;

    this.searchRequest.textQuery = null;

    setTimeout(async () => {
      const rawQuery = this.queryInput.value.trim();
      if (!rawQuery) {
          this.showLoading(false);
          this.isSearchInProgress = false;
          return;
      };

      // Format the query and update the input box value
      const formattedQuery = this.toTitleCase(rawQuery);
      this.queryInput.value = formattedQuery;

      // Update the header with the new query and location
      await this.updateResultsHeader(formattedQuery);

      // Pass the formatted query to the search request
      this.searchRequest.textQuery = formattedQuery;
      this.searchRequest.locationRestriction = this.map.getBounds();

      const selectedPrice = this.priceFilter.value;
      let priceLevels = [];
      switch (selectedPrice) {
        case "1": priceLevels = [this.PriceLevel.INEXPENSIVE]; break;
        case "2": priceLevels = [this.PriceLevel.MODERATE]; break;
        case "3": priceLevels = [this.PriceLevel.EXPENSIVE]; break;
        case "4": priceLevels = [this.PriceLevel.VERY_EXPENSIVE]; break;
        default: priceLevels = null; break;
      }
      this.searchRequest.priceLevels = priceLevels;

      const selectedRating = parseFloat(this.ratingFilter.value);
      this.searchRequest.minRating = selectedRating > 0 ? selectedRating : null;
      this.searchRequest.isOpenNow = this.openNowFilter.checked ? true : null;
    }, 0);
  }

  // NEW: Method to update the sidebar header with geocoded location
  async updateResultsHeader(query) {
    try {
      const response = await this.geocoder.geocode({ location: this.map.getCenter() });
      if (response.results && response.results.length > 0) {
        const cityResult = response.results.find(r => r.types.includes('locality')) || response.results[0];
        const city = cityResult.address_components[0].long_name;
        this.resultsHeaderText.textContent = `${query} near ${city}`;
      } else {
        this.resultsHeaderText.textContent = `${query} near current map area`;
      }
    } catch (error) {
      console.error("Geocoding failed:", error);
      this.resultsHeaderText.textContent = `Results for ${query}`;
    }
  }
}

window.addEventListener('DOMContentLoaded', () => {
  new PlaceFinderApp();
});

Sprawdzanie efektów pracy

Zapisz plik script.js i odśwież stronę.

Sprawdź funkcje:

  • W polu wyszukiwania wpisz pizza (wszystkie małe litery) i kliknij Szukaj. Tekst w polu powinien zmienić się na „Pizza”, a nagłówek na pasku bocznym powinien zostać zaktualizowany i wyświetlać „Pizza w pobliżu Nowego Jorku”.
  • Przesuń mapę do innego miasta, np. Bostonu, i wyszukaj ponownie. Nagłówek powinien zmienić się na „Pizza w pobliżu Bostonu”.

9. Gratulacje

Udało Ci się utworzyć kompletną, interaktywną aplikację do wyszukiwania lokalnego, która łączy prostotę interfejsu Places UI Kit z możliwościami podstawowych interfejsów API Google Maps Platform w JavaScript.

Czego się dowiedziałeś

  • Jak skonstruować aplikację do mapowania za pomocą klasy JavaScript do zarządzania stanem i logiką.
  • Jak używać interfejsu Places UI Kitinterfejsem Google Maps JavaScript API do szybkiego tworzenia interfejsu.
  • Jak programowo dodawać zaawansowane znaczniki i nimi zarządzać, aby wyświetlać na mapie niestandardowe ciekawe miejsca.
  • Jak używać usługi geokodowania, aby przekształcać współrzędne w czytelne dla użytkowników adresy, co zwiększa wygodę korzystania z usługi.
  • Jak identyfikować i naprawiać typowe sytuacje wyścigu w aplikacji interaktywnej za pomocą flag stanu i upewniać się, że właściwości komponentów są prawidłowo aktualizowane.

Co dalej?

Jakie inne codelaby chcesz zobaczyć?

Wizualizacja danych na mapach Więcej informacji o dostosowywaniu stylu map Tworzenie interakcji 3D na mapach

Nie możesz znaleźć warsztatów, które Cię najbardziej interesują? Zgłoś problem tutaj