Tworzenie lokalizatora sklepów full stack za pomocą Google Maps Platform i Google Cloud

1. Wprowadzenie

Streszczenie

Wyobraź sobie, że masz wiele miejsc do umieszczenia na mapie i chcesz, aby użytkownicy mogli zobaczyć, gdzie się one znajdują, i wybrać miejsce, które chcą odwiedzić. Oto kilka typowych przykładów:

  • lokalizator sklepów w witrynie sprzedawcy,
  • mapę lokali wyborczych w nadchodzących wyborach,
  • katalog specjalnych lokalizacji, takich jak pojemniki na baterie do recyklingu;

Co utworzysz

W tym module stworzysz lokalizator, który korzysta z aktywnego pliku danych z lokalizacjami specjalistycznymi i pomaga użytkownikowi znaleźć lokalizację najbliższą jego punktu początkowego. Ten pełny lokalizator może obsługiwać znacznie większą liczbę miejsc niż prosty lokalizator sklepów, który jest ograniczony do 25 lokalizacji sklepów lub mniejszej liczby.

2ece59c64c06e9da.png

Czego się nauczysz

W tym samouczku używamy otwartego zbioru danych, aby symulować wstępnie wypełnione metadane dotyczące dużej liczby lokalizacji sklepów. Dzięki temu możesz skupić się na poznaniu kluczowych koncepcji technicznych.

  • Interfejs Maps JavaScript API: wyświetlanie dużej liczby lokalizacji na dostosowanej mapie internetowej.
  • GeoJSON: format, w którym są przechowywane metadane dotyczące lokalizacji.
  • Autouzupełnianie miejsc: pomaga użytkownikom szybciej i dokładniej podawać lokalizacje początkowe
  • Go: język programowania używany do tworzenia backendu aplikacji. Backend będzie wchodzić w interakcje z bazą danych i wysyłać wyniki zapytań z powrotem do frontendu w sformatowanym formacie JSON.
  • App Engine: do hostowania aplikacji internetowej.

Wymagania wstępne

  • Podstawowa znajomość języków HTML i JavaScript
  • konto Google,

2. Konfiguracja

W kroku 3 w sekcji poniżej włącz interfejsy Maps JavaScript API, Places APIDistance Matrix API na potrzeby tych ćwiczeń z programowania.

Pierwsze kroki z Google Maps Platform

Jeśli nie korzystasz jeszcze z Google Maps Platform, wykonaj te czynności, korzystając z przewodnika Wprowadzenie do Google Maps Platform lub z playlisty Wprowadzenie do Google Maps Platform:

  1. Utwórz konto rozliczeniowe.
  2. Utwórz projekt.
  3. Włącz interfejsy API i pakiety SDK Google Maps Platform (wymienione w poprzedniej sekcji).
  4. Wygeneruj klucz interfejsu API.

Aktywowanie Cloud Shell

W tym laboratorium wykorzystasz Cloud Shell, czyli środowisko wiersza poleceń działające w Google Cloud, które zapewnia dostęp do usług i zasobów działających w Google Cloud. Dzięki temu możesz hostować i uruchamiać projekt w całości z poziomu przeglądarki internetowej.

Aby aktywować Cloud Shell w konsoli Cloud, kliknij Aktywuj Cloud Shell 89665d8d348105cd.png (udostępnienie środowiska i połączenie się z nim powinno zająć tylko kilka chwil).

5f504766b9b3be17.png

Spowoduje to otwarcie nowej powłoki w dolnej części przeglądarki (wcześniej może się wyświetlić pełnoekranowa reklama wprowadzająca).

d3bb67d514893d1f.png

Potwierdź projekt

Po połączeniu z Cloud Shell zobaczysz, że jesteś już uwierzytelniony, a projekt jest już ustawiony na identyfikator projektu wybrany podczas konfiguracji.

$ gcloud auth list
Credentialed Accounts:
ACTIVE  ACCOUNT
  *     <myaccount>@<mydomain>.com
$ gcloud config list project
[core]
project = <YOUR_PROJECT_ID>

Jeśli z jakiegoś powodu projekt nie jest ustawiony, uruchom to polecenie:

gcloud config set project <YOUR_PROJECT_ID>

Włączanie interfejsu App Engine Flex API

Interfejs AppEngine Flex API należy włączyć ręcznie w konsoli Cloud. Włączenie interfejsu API spowoduje nie tylko jego aktywację, ale też utworzenie konta usługi środowiska elastycznego App Engine, czyli uwierzytelnionego konta, które będzie w imieniu użytkownika wchodzić w interakcje z usługami Google (np. bazami danych SQL).

3. Witaj, świecie

Backend: Hello World w Go

W instancji Cloud Shell zaczniesz od utworzenia aplikacji Go App Engine Flex, która będzie stanowić podstawę pozostałej części tego laboratorium.

Na pasku narzędzi Cloud Shell kliknij przycisk Otwórz edytor, aby otworzyć edytor kodu w nowej karcie. Ten internetowy edytor kodu umożliwia łatwe edytowanie plików w instancji Cloud Shell.

b63f7baad67b6601.png

Następnie kliknij ikonę Otwórz w nowym oknie, aby przenieść edytor i terminal na nową kartę.

3f6625ff8461c551.png

W terminalu u dołu nowej karty utwórz nowy katalog austin-recycling.

mkdir -p austin-recycling && cd $_

Następnie utworzysz małą aplikację Go App Engine, aby sprawdzić, czy wszystko działa. Hello World

Katalog austin-recycling powinien też pojawić się na liście folderów Edytora po lewej stronie. W katalogu austin-recycling utwórz plik o nazwie app.yaml. Wstaw do pliku app.yaml tę treść:

app.yaml

runtime: go
env: flex

manual_scaling:
  instances: 1
resources:
  cpu: 1
  memory_gb: 0.5
  disk_size_gb: 10

Ten plik konfiguracyjny konfiguruje aplikację App Engine do korzystania ze środowiska wykonawczego Go Flex. Więcej informacji o znaczeniu elementów konfiguracji w tym pliku znajdziesz w dokumentacji środowiska standardowego Google App Engine w Go.

Następnie utwórz plik main.go obok pliku app.yaml:

main.go

package main

import (
        "fmt"
        "log"
        "net/http"
        "os"
)

func main() {
        http.HandleFunc("/", handle)
        port := os.Getenv("PORT")
        if port == "" {
                port = "8080"
        }
        log.Printf("Listening on port %s", port)
        if err := http.ListenAndServe(":"+port, nil); err != nil {
                log.Fatal(err)
        }
}

func handle(w http.ResponseWriter, r *http.Request) {
        if r.URL.Path != "/" {
                http.NotFound(w, r)
                return
        }
        fmt.Fprint(w, "Hello world!")
}

Warto na chwilę się zatrzymać i zrozumieć, co ten kod robi, przynajmniej na wysokim poziomie. Zdefiniowano pakiet main, który uruchamia serwer HTTP nasłuchujący na porcie 8080 i rejestruje funkcję obsługi żądań HTTP pasujących do ścieżki "/".

Funkcja obsługi, wygodnie nazwana handler, zapisuje ciąg tekstowy "Hello, world!". Ten tekst zostanie przekazany z powrotem do przeglądarki, w której będzie można go odczytać. W dalszych krokach utworzysz moduły obsługi, które będą odpowiadać danymi GeoJSON zamiast prostych ciągów zakodowanych na stałe.

Po wykonaniu tych czynności powinien pojawić się edytor podobny do tego:

2084fdd5ef594ece.png

Wypróbuj

Aby przetestować tę aplikację, możesz uruchomić serwer programistyczny App Engine w instancji Cloud Shell. Wróć do wiersza poleceń Cloud Shell i wpisz to polecenie:

go run *.go

Zobaczysz kilka wierszy danych wyjściowych dziennika, które potwierdzą, że serwer programistyczny jest uruchomiony na instancji Cloud Shell, a aplikacja internetowa „hello world” nasłuchuje na porcie 8080 serwera lokalnego. Aby otworzyć kartę przeglądarki z tą aplikacją, naciśnij przycisk Podgląd w przeglądarce i wybierz element menu Podejrzyj na porcie 8080 na pasku narzędzi Cloud Shell.

4155fc1dc717ac67.png

Kliknięcie tej pozycji menu spowoduje otwarcie nowej karty w przeglądarce internetowej ze słowami „Hello, world!” wyświetlanymi z serwera deweloperskiego App Engine.

W następnym kroku dodasz do tej aplikacji dane dotyczące recyklingu w Austin i zaczniesz je wizualizować.

4. Pobieranie bieżących danych

GeoJSON, lingua franca świata GIS

W poprzednim kroku wspomnieliśmy, że w kodzie Go utworzysz procedury obsługi, które będą renderować dane GeoJSON w przeglądarce internetowej. Czym jest GeoJSON?

W świecie systemów informacji geograficznej (GIS) musimy mieć możliwość przekazywania wiedzy o obiektach geograficznych między systemami komputerowymi. Mapy są łatwe do odczytania dla ludzi, ale komputery zwykle wolą dane w bardziej przystępnych formatach.

GeoJSON to format kodowania struktur danych geograficznych, takich jak współrzędne punktów zbiórki odpadów w Austin w Teksasie. Format GeoJSON został ustandaryzowany przez Internet Engineering Task Force w ramach standardu RFC7946. GeoJSON jest zdefiniowany w terminach JSON, czyli JavaScript Object Notation, który został ustandaryzowany w dokumencie ECMA-404 przez tę samą organizację, która ustandaryzowała JavaScript, czyli Ecma International.

Ważne jest to, że GeoJSON to powszechnie obsługiwany format przesyłania informacji geograficznych. W tym samouczku GeoJSON jest używany w następujący sposób:

  • Użyj pakietów Go, aby przeanalizować dane z Austin i przekształcić je w wewnętrzną strukturę danych GIS, która będzie służyć do filtrowania żądanych danych.
  • Serializuj żądane dane do przesyłania między serwerem WWW a przeglądarką.
  • Użyj biblioteki JavaScript, aby przekształcić odpowiedź w markery na mapie.

Pozwoli Ci to zaoszczędzić sporo pisania kodu, ponieważ nie musisz tworzyć parserów i generatorów do konwertowania strumienia danych przesyłanego przez sieć na reprezentacje w pamięci.

Pobieranie danych

Portal otwartych danych miasta Austin w Teksasie udostępnia informacje geoprzestrzenne o zasobach publicznych do użytku publicznego. W tym ćwiczeniu z programowania wizualizujesz zbiór danych lokalizacji punktów zbiórki odpadów do recyklingu.

Dane zostaną zwizualizowane za pomocą znaczników na mapie renderowanych przy użyciu warstwy danych interfejsu Maps JavaScript API.

Zacznij od pobrania danych GeoJSON ze strony internetowej miasta Austin do swojej aplikacji.

  1. W oknie wiersza poleceń instancji Cloud Shell zamknij serwer, wpisując [CTRL] + [C].
  2. Utwórz katalog data w katalogu austin-recycling i przejdź do niego:
mkdir -p data && cd data

Teraz użyj polecenia curl, aby pobrać lokalizacje punktów recyklingu:

curl "https://data.austintexas.gov/resource/qzi7-nx8g.geojson" -o recycling-locations.geojson

Na koniec wróć do katalogu nadrzędnego.

cd ..

5. Mapowanie lokalizacji

Najpierw zaktualizuj plik app.yaml, aby odzwierciedlał bardziej rozbudowaną aplikację, która „nie jest już tylko aplikacją typu hello world”, którą zamierzasz utworzyć.

app.yaml

runtime: go
env: flex

handlers:
- url: /
  static_files: static/index.html
  upload: static/index.html
- url: /(.*\.(js|html|css))$
  static_files: static/\1
  upload: static/.*\.(js|html|css)$
- url: /.*
  script: auto

manual_scaling:
  instances: 1
resources:
  cpu: 1
  memory_gb: 0.5
  disk_size_gb: 10

Ta konfiguracja app.yaml kieruje żądania dotyczące /, /*.js, /*.css/*.html do zestawu plików statycznych. Oznacza to, że statyczny komponent HTML aplikacji będzie obsługiwany bezpośrednio przez infrastrukturę obsługi plików App Engine, a nie przez aplikację Go. Zmniejsza to obciążenie serwera i zwiększa szybkość obsługi.

Teraz możesz utworzyć backend aplikacji w Go.

Tworzenie backendu

Być może zauważysz, że plik app.yaml nie udostępnia pliku GeoJSON. Dzieje się tak, ponieważ GeoJSON będzie przetwarzany i wysyłany przez nasz backend w języku Go, co pozwoli nam w późniejszych krokach wbudować w niego ciekawe funkcje. Zmień plik main.go, aby wyglądał tak:

main.go

package main

import (
        "fmt"
        "log"
        "net/http"
        "os"
        "path/filepath"
)

var GeoJSON = make(map[string][]byte)

// cacheGeoJSON loads files under data into `GeoJSON`.
func cacheGeoJSON() {
        filenames, err := filepath.Glob("data/*")
        if err != nil {
                log.Fatal(err)
        }

        for _, f := range filenames {
                name := filepath.Base(f)
                dat, err := os.ReadFile(f)
                if err != nil {
                        log.Fatal(err)
                }
                GeoJSON[name] = dat
        }
}

func main() {
        // Cache the JSON so it doesn't have to be reloaded every time a request is made.
        cacheGeoJSON()


        // Request for data should be handled by Go.  Everything else should be directed
        // to the folder of static files.
        http.HandleFunc("/data/dropoffs", dropoffsHandler)
        http.Handle("/", http.FileServer(http.Dir("./static/")))

        // Open up a port for the webserver.
        port := os.Getenv("PORT")
        if port == "" {
                port = "8080"
        }
        log.Printf("Listening on port %s", port)

        if err := http.ListenAndServe(":"+port, nil); err != nil {
                log.Fatal(err)
        }
}

func helloHandler(w http.ResponseWriter, r *http.Request) {
        // Writes Hello, World! to the user's web browser via `w`
        fmt.Fprint(w, "Hello, world!")
}

func dropoffsHandler(w http.ResponseWriter, r *http.Request) {
        w.Header().Set("Content-type", "application/json")
        w.Write(GeoJSON["recycling-locations.geojson"])
}

Backend Go zapewnia nam już przydatną funkcję: instancja App Engine buforuje wszystkie te lokalizacje natychmiast po uruchomieniu. Pozwala to zaoszczędzić czas, ponieważ backend nie musi odczytywać pliku z dysku przy każdym odświeżaniu przez każdego użytkownika.

Tworzenie frontendu

Pierwszą rzeczą, którą musimy zrobić, jest utworzenie folderu do przechowywania wszystkich statycznych zasobów. W folderze nadrzędnym projektu utwórz folder static.

mkdir -p static && cd static

W tym folderze utworzymy 3 pliki.

  • index.html będzie zawierać cały kod HTML aplikacji do wyszukiwania sklepów na jednej stronie.
  • style.css , zgodnie z oczekiwaniami, będzie zawierać style.
  • app.js będzie odpowiadać za pobieranie GeoJSON, wywoływanie interfejsu Maps API i umieszczanie znaczników na mapie niestandardowej.

Utwórz te 3 pliki i umieść je w folderze static/ .

style.css

html,
body {
  height: 100%;
  margin: 0;
  padding: 0;
}

body {
  display: flex;
}

#map {
  height: 100%;
  flex-grow: 4;
  flex-basis: auto;
}

index.html

<html>
  <head>
    <title>Austin recycling drop-off locations</title>
    <link rel="stylesheet" type="text/css" href="style.css" />
    <script src="app.js"></script>

    <script
      defer
    src="https://maps.googleapis.com/maps/api/js?key=YOUR_API_KEY&v=weekly&libraries=places&callback=initialize&solution_channel=GMP_codelabs_fullstackstorelocator_v1_a"
    ></script>
  </head>

  <body>
    <div id="map"></div>
    <!-- Autocomplete div goes here -->
  </body>
</html>

Zwróć szczególną uwagę na adres URL src w tagu skryptu elementu head.

  • Zastąp tekst zastępczy „YOUR_API_KEY” kluczem interfejsu API wygenerowanym w kroku konfiguracji. Aby pobrać klucz interfejsu API lub wygenerować nowy, otwórz w Cloud Console stronę Interfejsy API i usługi –> Dane logowania.
  • Zwróć uwagę, że adres URL zawiera parametr callback=initialize.. Teraz utworzymy plik JavaScript zawierający tę funkcję wywołania zwrotnego. W tym miejscu aplikacja wczyta lokalizacje z zaplecza, wyśle je do interfejsu Maps API i użyje wyniku do oznaczenia niestandardowych lokalizacji na mapie. Wszystko to będzie pięknie renderowane na stronie internetowej.
  • Parametr libraries=places wczytuje bibliotekę Miejsc, która jest niezbędna do korzystania z funkcji takich jak autouzupełnianie adresu, które zostaną dodane później.

app.js

let distanceMatrixService;
let map;
let originMarker;
let infowindow;
let circles = [];
let stores = [];
// The location of Austin, TX
const AUSTIN = { lat: 30.262129, lng: -97.7468 };

async function initialize() {
  initMap();

  // TODO: Initialize an infoWindow

  // Fetch and render stores as circles on map
  fetchAndRenderStores(AUSTIN);

  // TODO: Initialize the Autocomplete widget
}

const initMap = () => {
  // TODO: Start Distance Matrix service

  // The map, centered on Austin, TX
  map = new google.maps.Map(document.querySelector("#map"), {
    center: AUSTIN,
    zoom: 14,
    // mapId: 'YOUR_MAP_ID_HERE',
    clickableIcons: false,
    fullscreenControl: false,
    mapTypeControl: false,
    rotateControl: true,
    scaleControl: false,
    streetViewControl: true,
    zoomControl: true,
  });
};

const fetchAndRenderStores = async (center) => {
  // Fetch the stores from the data source
  stores = (await fetchStores(center)).features;

  // Create circular markers based on the stores
  circles = stores.map((store) => storeToCircle(store, map));
};

const fetchStores = async (center) => {
  const url = `/data/dropoffs`;
  const response = await fetch(url);
  return response.json();
};

const storeToCircle = (store, map) => {
  const [lng, lat] = store.geometry.coordinates;
  const circle = new google.maps.Circle({
    radius: 50,
    strokeColor: "#579d42",
    strokeOpacity: 0.8,
    strokeWeight: 5,
    center: { lat, lng },
    map,
  });

  return circle;
};

Ten kod renderuje lokalizacje sklepów na mapie. Aby przetestować dotychczasowe zmiany, w wierszu poleceń wróć do katalogu nadrzędnego:

cd ..

Teraz ponownie uruchom aplikację w trybie programowania, używając tego polecenia:

go run *.go

Wyświetl podgląd tak jak poprzednio. Powinna pojawić się mapa z małymi zielonymi kółkami, jak na tym przykładzie.

58a6680e9c8e7396.png

Lokalizacje na mapie są już renderowane, a my jesteśmy dopiero w połowie ćwiczenia z programowania. Niesamowite. Teraz dodajmy trochę interaktywności.

6. Wyświetlanie szczegółów na żądanie

Reagowanie na zdarzenia kliknięcia na znacznikach mapy

Wyświetlanie wielu znaczników na mapie to dobry początek, ale musimy umożliwić odwiedzającym kliknięcie jednego z nich i wyświetlenie informacji o danym miejscu (np. nazwy firmy, adresu itp.). Nazwa małego okna informacyjnego, które zwykle pojawia się po kliknięciu znacznika w Mapach Google, to okno informacyjne.

Utwórz obiekt infoWindow. Dodaj do funkcji initialize ten kod, zastępując zakomentowany wiersz „// TODO: Initialize an info window”.

app.js – initialize

  // Add an info window that pops up when user clicks on an individual
  // location. Content of info window is entirely up to us.
  infowindow = new google.maps.InfoWindow();

Zastąp definicję funkcji fetchAndRenderStores tą nieco inną wersją, w której ostatnia linia wywołuje funkcję storeToCircle z dodatkowym argumentem infowindow:

app.js – fetchAndRenderStores

const fetchAndRenderStores = async (center) => {
  // Fetch the stores from the data source
  stores = (await fetchStores(center)).features;

  // Create circular markers based on the stores
  circles = stores.map((store) => storeToCircle(store, map, infowindow));
};

Zastąp definicję storeToCircle tą nieco dłuższą wersją, która teraz przyjmuje okno informacyjne jako trzeci argument:

app.js – storeToCircle

const storeToCircle = (store, map, infowindow) => {
  const [lng, lat] = store.geometry.coordinates;
  const circle = new google.maps.Circle({
    radius: 50,
    strokeColor: "#579d42",
    strokeOpacity: 0.8,
    strokeWeight: 5,
    center: { lat, lng },
    map,
  });
  circle.addListener("click", () => {
    infowindow.setContent(`${store.properties.business_name}<br />
      ${store.properties.address_address}<br />
      Austin, TX ${store.properties.zip_code}`);
    infowindow.setPosition({ lat, lng });
    infowindow.setOptions({ pixelOffset: new google.maps.Size(0, -30) });
    infowindow.open(map);
  });
  return circle;
};

Nowy kod powyżej wyświetla infoWindow z informacjami o wybranym sklepie za każdym razem, gdy klikniesz na mapie znacznik sklepu.

Jeśli serwer nadal działa, zatrzymaj go i uruchom ponownie. Odśwież stronę mapy i spróbuj kliknąć znacznik na mapie. Powinno pojawić się małe okienko z nazwą i adresem firmy, które będzie wyglądać mniej więcej tak:

1af0ab72ad0eadc5.png

7. Pobieranie lokalizacji początkowej użytkownika

Użytkownicy wyszukiwarek sklepów zwykle chcą wiedzieć, który sklep jest najbliżej nich lub adresu, z którego planują rozpocząć podróż. Dodaj pasek wyszukiwania autouzupełniania miejsc, aby użytkownik mógł łatwo wpisać adres początkowy. Autouzupełnianie miejsc zapewnia funkcję wpisywania z wyprzedzeniem podobną do autouzupełniania w innych paskach wyszukiwania Google, z tym że przewidywania dotyczą wszystkich miejsc na platformie Google Maps.

Tworzenie pola wprowadzania danych przez użytkownika

Wróć do edycji style.css, aby dodać style do paska wyszukiwania Autouzupełnianie i powiązanego z nim panelu bocznego z wynikami. Podczas aktualizowania stylów CSS dodamy też style dla przyszłego paska bocznego, który będzie wyświetlać informacje o sklepie w formie listy towarzyszącej mapie.

Dodaj ten kod na końcu pliku.

style.css

#panel {
  height: 100%;
  flex-basis: 0;
  flex-grow: 0;
  overflow: auto;
  transition: all 0.2s ease-out;
}

#panel.open {
  flex-basis: auto;
}

#panel .place {
  font-family: "open sans", arial, sans-serif;
  font-size: 1.2em;
  font-weight: 500;
  margin-block-end: 0px;
  padding-left: 18px;
  padding-right: 18px;
}

#panel .distanceText {
  color: silver;
  font-family: "open sans", arial, sans-serif;
  font-size: 1em;
  font-weight: 400;
  margin-block-start: 0.25em;
  padding-left: 18px;
  padding-right: 18px;
}

/* Styling for Autocomplete search bar */
#pac-card {
  background-color: #fff;
  border-radius: 2px 0 0 2px;
  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3);
  box-sizing: border-box;
  font-family: Roboto;
  margin: 10px 10px 0 0;
  -moz-box-sizing: border-box;
  outline: none;
}

#pac-container {
  padding-top: 12px;
  padding-bottom: 12px;
  margin-right: 12px;
}

#pac-input {
  background-color: #fff;
  font-family: Roboto;
  font-size: 15px;
  font-weight: 300;
  margin-left: 12px;
  padding: 0 11px 0 13px;
  text-overflow: ellipsis;
  width: 400px;
}

#pac-input:focus {
  border-color: #4d90fe;
}

#pac-title {
  color: #fff;
  background-color: #acbcc9;
  font-size: 18px;
  font-weight: 400;
  padding: 6px 12px;
}

.hidden {
  display: none;
}

Zarówno pasek wyszukiwania autouzupełniania, jak i wysuwany panel są początkowo ukryte, dopóki nie są potrzebne.

Przygotuj element div na widżet Autocomplete, zastępując komentarz w pliku index.html o treści "<!-- Autocomplete div goes here --> tym kodem: Podczas wprowadzania tej zmiany dodamy też element div dla wysuwanego panelu.

index.html

     <div id="panel" class="closed"></div>
     <div class="hidden">
      <div id="pac-card">
        <div id="pac-title">Find the nearest location</div>
        <div id="pac-container">
          <input
            id="pac-input"
            type="text"
            placeholder="Enter an address"
            class="pac-target-input"
            autocomplete="off"
          />
        </div>
      </div>
    </div>

Teraz zdefiniuj funkcję, która doda widżet autouzupełniania do mapy. W tym celu dodaj ten kod na końcu app.js.

app.js

const initAutocompleteWidget = () => {
  // Add search bar for auto-complete
  // Build and add the search bar
  const placesAutoCompleteCardElement = document.getElementById("pac-card");
  const placesAutoCompleteInputElement = placesAutoCompleteCardElement.querySelector(
    "input"
  );
  const options = {
    types: ["address"],
    componentRestrictions: { country: "us" },
    map,
  };
  map.controls[google.maps.ControlPosition.TOP_RIGHT].push(
    placesAutoCompleteCardElement
  );
  // Make the search bar into a Places Autocomplete search bar and select
  // which detail fields should be returned about the place that
  // the user selects from the suggestions.
  const autocomplete = new google.maps.places.Autocomplete(
    placesAutoCompleteInputElement,
    options
  );
  autocomplete.setFields(["address_components", "geometry", "name"]);
  map.addListener("bounds_changed", () => {
    autocomplete.setBounds(map.getBounds());
  });

  // TODO: Respond when a user selects an address
};

Kod ogranicza sugestie autouzupełniania do zwracania tylko adresów (ponieważ autouzupełnianie miejsc może też dopasowywać nazwy firm i lokalizacje administracyjne) i ogranicza zwracane adresy tylko do tych w Stanach Zjednoczonych. Dodanie tych opcjonalnych specyfikacji zmniejszy liczbę znaków, które użytkownik musi wpisać, aby zawęzić prognozy i wyświetlić szukany adres.

Następnie przenosi utworzone przez Ciebie automatyczne uzupełnianie div do prawego górnego rogu mapy i określa, które pola powinny być zwracane w odpowiedzi dla każdego miejsca.

Na koniec wywołaj funkcję initAutocompleteWidget na końcu funkcji initialize, zastępując komentarz „// TODO: Initialize the Autocomplete widget”.

app.js – initialize

 // Initialize the Places Autocomplete Widget
 initAutocompleteWidget();

Uruchom ponownie serwer, wpisując to polecenie, a następnie odśwież podgląd.

go run *.go

W prawym górnym rogu mapy powinien być teraz widoczny widżet autouzupełniania, który wyświetla adresy w Stanach Zjednoczonych pasujące do wpisywanego tekstu, z uwzględnieniem widocznego obszaru mapy.

58e9bbbcc4bf18d1.png

Aktualizowanie mapy po wybraniu adresu początkowego przez użytkownika

Teraz musisz obsłużyć sytuację, w której użytkownik wybierze prognozę z widżetu autouzupełniania, i użyć tej lokalizacji jako podstawy do obliczenia odległości do Twoich sklepów.

Dodaj ten kod na końcu pliku initAutocompleteWidgetapp.js, zastępując komentarz „// TODO: Respond when a user selects an address”.

app.js – initAutocompleteWidget

  // Respond when a user selects an address
  // Set the origin point when the user selects an address
  originMarker = new google.maps.Marker({ map: map });
  originMarker.setVisible(false);
  let originLocation = map.getCenter();
  autocomplete.addListener("place_changed", async () => {
    // circles.forEach((c) => c.setMap(null)); // clear existing stores
    originMarker.setVisible(false);
    originLocation = map.getCenter();
    const place = autocomplete.getPlace();

    if (!place.geometry) {
      // User entered the name of a Place that was not suggested and
      // pressed the Enter key, or the Place Details request failed.
      window.alert("No address available for input: '" + place.name + "'");
      return;
    }
    // Recenter the map to the selected address
    originLocation = place.geometry.location;
    map.setCenter(originLocation);
    map.setZoom(15);
    originMarker.setPosition(originLocation);
    originMarker.setVisible(true);

    // await fetchAndRenderStores(originLocation.toJSON());
    // TODO: Calculate the closest stores
  });

Kod dodaje odbiornik, dzięki czemu, gdy użytkownik kliknie jedną z sugestii, mapa zostanie ponownie wyśrodkowana na wybranym adresie, a miejsce docelowe zostanie ustawione jako podstawa obliczeń odległości. Obliczenia odległości zaimplementujesz w przyszłości.

Zatrzymaj i uruchom ponownie serwer, a następnie odśwież podgląd, aby zobaczyć, jak mapa wyśrodkowuje się po wpisaniu adresu w pasku wyszukiwania z autouzupełnianiem.

8. Skalowanie za pomocą Cloud SQL

Mamy już całkiem niezły lokalizator sklepów. Wykorzystuje to, że aplikacja będzie używać tylko około 100 lokalizacji, więc wczytuje je do pamięci na backendzie (zamiast wielokrotnie odczytywać je z pliku). A co, jeśli lokalizator musi działać w innej skali? Jeśli masz setki lokalizacji rozproszonych na dużym obszarze geograficznym (lub tysiące na całym świecie), przechowywanie wszystkich tych lokalizacji w pamięci nie jest już najlepszym pomysłem, a podział stref na poszczególne pliki spowoduje własne problemy.

Czas wczytać lokalizacje z bazy danych. W tym kroku przeniesiemy wszystkie lokalizacje z pliku GeoJSON do bazy danych Cloud SQL i zaktualizujemy backend Go, aby w przypadku każdego żądania pobierał wyniki z tej bazy danych zamiast z lokalnej pamięci podręcznej.

Tworzenie instancji Cloud SQL z bazą danych PostgreSQL

Instancję Cloud SQL możesz utworzyć w konsoli Google Cloud, ale jeszcze łatwiej jest użyć narzędzia gcloud, aby utworzyć ją z wiersza poleceń. W Cloud Shell utwórz instancję Cloud SQL za pomocą tego polecenia:

gcloud sql instances create locations \
--database-version=POSTGRES_12 \
--tier=db-custom-1-3840 --region=us-central1
  • Argument locations to nazwa, którą nadajemy tej instancji Cloud SQL.
  • Flaga tier umożliwia wybór spośród wygodnych wstępnie zdefiniowanych maszyn.
  • Wartość db-custom-1-3840 oznacza, że tworzona instancja powinna mieć 1 procesor wirtualny i około 3,75 GB pamięci.

Instancja Cloud SQL zostanie utworzona i zainicjowana za pomocą bazy danych PostgreSQL z domyślnym użytkownikiem postgres. Jakie jest hasło tego użytkownika? To świetne pytanie. Nie mają. Aby się zalogować, musisz skonfigurować co najmniej 1 z nich.

Ustaw hasło za pomocą tego polecenia:

gcloud sql users set-password postgres \
    --instance=locations --prompt-for-password

Gdy pojawi się prośba, wpisz wybrane hasło.

Włączanie rozszerzenia PostGIS

PostGIS to rozszerzenie PostgreSQL, które ułatwia przechowywanie standardowych typów danych geoprzestrzennych. W normalnych okolicznościach musielibyśmy przejść pełny proces instalacji, aby dodać PostGIS do naszej bazy danych. Na szczęście jest to jedno z rozszerzeń PostgreSQL obsługiwanych przez Cloud SQL.

Połącz się z instancją bazy danych, logując się jako użytkownik postgres za pomocą tego polecenia w terminalu Cloud Shell.

gcloud sql connect locations --user=postgres --quiet

Wpisz utworzone hasło. Teraz dodaj rozszerzenie PostGIS w wierszu poleceń postgres=>.

CREATE EXTENSION postgis;

Jeśli operacja się powiedzie, dane wyjściowe powinny wyglądać jak poniżej: CREATE EXTENSION.

Przykładowe dane wyjściowe polecenia

CREATE EXTENSION

Na koniec zamknij połączenie z bazą danych, wpisując polecenie quit w wierszu poleceń postgres=>.

\q

Importowanie danych geograficznych do bazy danych

Teraz musimy zaimportować wszystkie dane o lokalizacji z plików GeoJSON do naszej nowej bazy danych.

Na szczęście jest to powszechny problem, więc w internecie znajdziesz kilka narzędzi, które pozwolą Ci go zautomatyzować. Użyjemy narzędzia ogr2ogr, które konwertuje dane geoprzestrzenne między wieloma popularnymi formatami przechowywania. Jedną z tych opcji jest, jak się domyślasz, przekonwertowanie pliku GeoJSON na plik zrzutu SQL. Plik zrzutu SQL można następnie wykorzystać do utworzenia tabel i kolumn w bazie danych oraz wczytania do niej wszystkich danych, które znajdowały się w plikach GeoJSON.

Tworzenie pliku zrzutu SQL

Najpierw zainstaluj ogr2ogr.

sudo apt-get install gdal-bin

Następnie użyj narzędzia ogr2ogr, aby utworzyć plik zrzutu SQL. Ten plik utworzy tabelę o nazwie austinrecycling.

ogr2ogr --config PG_USE_COPY YES -f PGDump datadump.sql \
data/recycling-locations.geojson -nln austinrecycling

Powyższe polecenie jest oparte na uruchomieniu z folderu austin-recycling. Jeśli musisz uruchomić go z innego katalogu, zastąp data ścieżką do katalogu, w którym jest przechowywany plik recycling-locations.geojson.

Wypełnianie bazy danych lokalizacjami punktów recyklingu

Po wykonaniu ostatniego polecenia w katalogu, w którym zostało ono uruchomione, powinien pojawić się plik datadump.sql,. Po otwarciu zobaczysz nieco ponad sto wierszy kodu SQL, które tworzą tabelę austinrecycling i wypełniają ją lokalizacjami.

Teraz otwórz połączenie z bazą danych i uruchom ten skrypt za pomocą tego polecenia.

gcloud sql connect locations --user=postgres --quiet < datadump.sql

Jeśli skrypt zostanie uruchomiony prawidłowo, ostatnie wiersze danych wyjściowych będą wyglądać tak:

Przykładowe dane wyjściowe polecenia

ALTER TABLE
ALTER TABLE
ATLER TABLE
ALTER TABLE
COPY 103
COMMIT
WARNING: there is no transaction in progress
COMMIT

Aktualizowanie backendu Go w celu korzystania z Cloud SQL

Teraz, gdy mamy już wszystkie te dane w naszej bazie, czas zaktualizować kod.

Zaktualizuj interfejs, aby wysyłać informacje o lokalizacji

Zacznijmy od bardzo małej zmiany w interfejsie: ponieważ piszemy tę aplikację z myślą o skali, w której nie chcemy, aby każda lokalizacja była dostarczana do interfejsu za każdym razem, gdy jest wykonywane zapytanie, musimy przekazywać z interfejsu podstawowe informacje o lokalizacji, na której zależy użytkownikowi.

Otwórz app.js i zastąp definicję funkcji fetchStores tą wersją, aby uwzględnić w adresie URL interesującą Cię szerokość i długość geograficzną.

app.js – fetchStores

const fetchStores = async (center) => {
  const url = `/data/dropoffs?centerLat=${center.lat}&centerLng=${center.lng}`;
  const response = await fetch(url);
  return response.json();
};

Po wykonaniu tego kroku w samouczku w odpowiedzi będą zwracane tylko sklepy znajdujące się najbliżej współrzędnych mapy podanych w parametrze center. W przypadku początkowego pobierania danych w funkcji initialize przykładowy kod podany w tym module używa współrzędnych środkowych dla Austin w Teksasie.

Ponieważ fetchStores będzie teraz zwracać tylko podzbiór lokalizacji sklepów, musimy ponownie pobierać sklepy za każdym razem, gdy użytkownik zmieni lokalizację początkową.

Zaktualizuj funkcję initAutocompleteWidget, aby odświeżać lokalizacje za każdym razem, gdy ustawiane jest nowe miejsce docelowe. Wymaga to 2 zmian:

  1. W funkcji initAutocompleteWidget znajdź wywołanie zwrotne dla słuchacza place_changed. Odkomentuj wiersz, który usuwa istniejące okręgi, aby był on uruchamiany za każdym razem, gdy użytkownik wybierze adres z paska wyszukiwania autouzupełniania miejsc.

app.js – initAutocompleteWidget

  autocomplete.addListener("place_changed", async () => {
    circles.forEach((c) => c.setMap(null)); // clear existing stores
    // ...
  1. Gdy wybrane miejsce wylotu ulegnie zmianie, zmienna originLocation zostanie zaktualizowana. Na końcu wywołania zwrotnego „place_changed” usuń komentarz z wiersza powyżej wiersza „// TODO: Calculate the closest stores”, aby przekazać to nowe pochodzenie do nowego wywołania funkcji fetchAndRenderStores.

app.js – initAutocompleteWidget

    await fetchAndRenderStores(originLocation.toJSON());
    // TODO: Calculate the closest stores

Zaktualizuj backend, aby zamiast płaskiego pliku JSON używać CloudSQL.

Usuwanie odczytywania i buforowania płaskich plików GeoJSON

Najpierw zmień main.go, aby usunąć kod, który wczytuje i buforuje płaski plik GeoJSON. Możemy też usunąć funkcję dropoffsHandler, ponieważ w innym pliku napiszemy funkcję opartą na Cloud SQL.

Nowy main.go będzie znacznie krótszy.

main.go

package main

import (

        "log"
        "net/http"
        "os"
)

func main() {

        initConnectionPool()

        // Request for data should be handled by Go.  Everything else should be directed
        // to the folder of static files.
        http.HandleFunc("/data/dropoffs", dropoffsHandler)
        http.Handle("/", http.FileServer(http.Dir("./static/")))

        // Open up a port for the webserver.
        port := os.Getenv("PORT")
        if port == "" {
                port = "8080"
        }
        log.Printf("Listening on port %s", port)
        if err := http.ListenAndServe(":"+port, nil); err != nil {
                log.Fatal(err)
        }
}

Tworzenie nowego modułu obsługi żądań lokalizacji

Teraz utwórzmy kolejny plik, locations.go, również w katalogu austin-recycling. Zacznij od ponownego wdrożenia procedury obsługi żądań lokalizacji.

locations.go

package main

import (
        "database/sql"
        "fmt"
        "log"
        "net/http"
        "os"

        _ "github.com/jackc/pgx/stdlib"
)

// queryBasic demonstrates issuing a query and reading results.
func dropoffsHandler(w http.ResponseWriter, r *http.Request) {
        w.Header().Set("Content-type", "application/json")
        centerLat := r.FormValue("centerLat")
        centerLng := r.FormValue("centerLng")
        geoJSON, err := getGeoJSONFromDatabase(centerLat, centerLng)
        if err != nil {
                str := fmt.Sprintf("Couldn't encode results: %s", err)
                http.Error(w, str, 500)
                return
        }
        fmt.Fprintf(w, geoJSON)
}

Procedura obsługi wykonuje te ważne zadania:

  • Pobiera szerokość i długość geograficzną z obiektu żądania (pamiętasz, jak dodaliśmy je do adresu URL? )
  • Wywołuje funkcję getGeoJsonFromDatabase, która zwraca ciąg GeoJSON (napiszemy go później).
  • Używa ResponseWriter, aby wydrukować ten ciąg GeoJSON w odpowiedzi.

Następnie utworzymy pulę połączeń, aby zapewnić dobrą skalowalność bazy danych w przypadku wielu użytkowników jednocześnie.

Tworzenie puli połączeń

Pula połączeń to zbiór aktywnych połączeń z bazą danych, które serwer może ponownie wykorzystać do obsługi żądań użytkowników. Pozwala to znacznie zmniejszyć obciążenie, gdy rośnie liczba aktywnych użytkowników, ponieważ serwer nie musi poświęcać czasu na tworzenie i zamykanie połączeń dla każdego aktywnego użytkownika. W poprzedniej sekcji zaimportowaliśmy bibliotekę github.com/jackc/pgx/stdlib.. Jest to popularna biblioteka do pracy z pulami połączeń w Go.

Na końcu pliku locations.go utwórz funkcję initConnectionPool (wywoływaną z pliku main.go), która inicjuje pulę połączeń. Aby zachować przejrzystość, w tym fragmencie kodu użyto kilku metod pomocniczych. configureConnectionPool to wygodne miejsce do dostosowywania ustawień puli, takich jak liczba połączeń i czas życia każdego połączenia. mustGetEnv opakowuje wywołania, aby uzyskać wymagane zmienne środowiskowe, dzięki czemu w przypadku braku krytycznych informacji (takich jak adres IP lub nazwa bazy danych, z którą ma się połączyć) może wyświetlać przydatne komunikaty o błędach.

locations.go

// The connection pool
var db *sql.DB

// Each struct instance contains a single row from the query result.
type result struct {
        featureCollection string
}

func initConnectionPool() {
        // If the optional DB_TCP_HOST environment variable is set, it contains
        // the IP address and port number of a TCP connection pool to be created,
        // such as "127.0.0.1:5432". If DB_TCP_HOST is not set, a Unix socket
        // connection pool will be created instead.
        if os.Getenv("DB_TCP_HOST") != "" {
                var (
                        dbUser    = mustGetenv("DB_USER")
                        dbPwd     = mustGetenv("DB_PASS")
                        dbTCPHost = mustGetenv("DB_TCP_HOST")
                        dbPort    = mustGetenv("DB_PORT")
                        dbName    = mustGetenv("DB_NAME")
                )

                var dbURI string
                dbURI = fmt.Sprintf("host=%s user=%s password=%s port=%s database=%s", dbTCPHost, dbUser, dbPwd, dbPort, dbName)

                // dbPool is the pool of database connections.
                dbPool, err := sql.Open("pgx", dbURI)
                if err != nil {
                        dbPool = nil
                        log.Fatalf("sql.Open: %v", err)
                }

                configureConnectionPool(dbPool)

                if err != nil {

                        log.Fatalf("initConnectionPool: unable to connect: %s", err)
                }
                db = dbPool
        }
}

// configureConnectionPool sets database connection pool properties.
// For more information, see https://golang.org/pkg/database/sql
func configureConnectionPool(dbPool *sql.DB) {
        // Set maximum number of connections in idle connection pool.
        dbPool.SetMaxIdleConns(5)
        // Set maximum number of open connections to the database.
        dbPool.SetMaxOpenConns(7)
        // Set Maximum time (in seconds) that a connection can remain open.
        dbPool.SetConnMaxLifetime(1800)
}

// mustGetEnv is a helper function for getting environment variables.
// Displays a warning if the environment variable is not set.
func mustGetenv(k string) string {
        v := os.Getenv(k)
        if v == "" {
                log.Fatalf("Warning: %s environment variable not set.\n", k)
        }
        return v
}

Wysyłaj zapytania do bazy danych o lokalizacje i otrzymuj w odpowiedzi dane w formacie JSON.

Teraz napiszemy zapytanie do bazy danych, które przyjmuje współrzędne mapy i zwraca 25 najbliższych lokalizacji. Co więcej, dzięki zaawansowanym funkcjom nowoczesnej bazy danych zwróci te dane w formacie GeoJSON. W rezultacie z punktu widzenia kodu interfejsu nic się nie zmieniło. Zanim wysłał żądanie do adresu URL i otrzymał wiele plików GeoJSON. Teraz wysyła żądanie na adres URL i otrzymuje z powrotem wiele danych w formacie GeoJSON.

Oto funkcja, która to umożliwia. Dodaj tę funkcję po kodzie obsługi i puli połączeń, który został właśnie napisany u dołu pliku locations.go.

locations.go

func getGeoJSONFromDatabase(centerLat string, centerLng string) (string, error) {

        // Obviously you can one-line this, but for testing purposes let's make it easy to modify on the fly.
        const milesRadius = 10
        const milesToMeters = 1609
        const radiusInMeters = milesRadius * milesToMeters

        const tableName = "austinrecycling"

        var queryStr = fmt.Sprintf(
                `SELECT jsonb_build_object(
                        'type',
                        'FeatureCollection',
                        'features',
                        jsonb_agg(feature)
                )
        FROM (
                        SELECT jsonb_build_object(
                                        'type',
                                        'Feature',
                                        'id',
                                        ogc_fid,
                                        'geometry',
                                        ST_AsGeoJSON(wkb_geometry)::jsonb,
                                        'properties',
                                        to_jsonb(row) - 'ogc_fid' - 'wkb_geometry'
                                ) AS feature
                        FROM (
                                        SELECT *,
                                                ST_Distance(
                                                        ST_GEOGFromWKB(wkb_geometry),
                                                        -- Los Angeles (LAX)
                                                        ST_GEOGFromWKB(st_makepoint(%v, %v))
                                                ) as distance
                                        from %v
                                        order by distance
                                        limit 25
                                ) row
                        where distance < %v
                ) features
                `, centerLng, centerLat, tableName, radiusInMeters)

        log.Println(queryStr)

        rows, err := db.Query(queryStr)

        defer rows.Close()

        rows.Next()
        queryResult := result{}
        err = rows.Scan(&queryResult.featureCollection)
        return queryResult.featureCollection, err
}

Ta funkcja służy głównie do konfigurowania, zamykania i obsługi błędów związanych z wysyłaniem żądań do bazy danych. Przyjrzyjmy się rzeczywistemu SQL-owi, który wykonuje wiele bardzo interesujących operacji na poziomie bazy danych, więc nie musisz się martwić implementacją żadnej z nich w kodzie.

Surowe zapytanie, które jest wysyłane po przeanalizowaniu ciągu znaków i wstawieniu wszystkich literałów ciągu znaków w odpowiednich miejscach, wygląda tak:

parsed.sql

SELECT jsonb_build_object(
        'type',
        'FeatureCollection',
        'features',
        jsonb_agg(feature)
    )
FROM (
        SELECT jsonb_build_object(
                'type',
                'Feature',
                'id',
                ogc_fid,
                'geometry',
                ST_AsGeoJSON(wkb_geometry)::jsonb,
                'properties',
                to_jsonb(row) - 'ogc_fid' - 'wkb_geometry'
            ) AS feature
        FROM (
                SELECT *,
                    ST_Distance(
                        ST_GEOGFromWKB(wkb_geometry),
                        -- Los Angeles (LAX)
                        ST_GEOGFromWKB(st_makepoint(-97.7624043, 30.523725))
                    ) as distance
                from austinrecycling
                order by distance
                limit 25
            ) row
        where distance < 16090
    ) features

To zapytanie można traktować jako jedno zapytanie główne i kilka funkcji opakowujących JSON.

SELECT * ... LIMIT 25 wybiera wszystkie pola dla każdej lokalizacji. Następnie używa funkcji ST_DISTANCE (należącej do pakietu funkcji pomiaru geograficznego PostGIS), aby określić odległość między każdą lokalizacją w bazie danych a parą współrzędnych geograficznych lokalizacji podanej przez użytkownika w interfejsie. Pamiętaj, że w przeciwieństwie do macierzy odległości, która może podawać odległość dojazdu, są to odległości geoprzestrzenne. Następnie używa tej odległości do sortowania i zwraca 25 najbliższych lokalizacji do lokalizacji określonej przez użytkownika.

**SELECT json_build_object(‘type', ‘F**eature') otacza poprzednie zapytanie, pobiera wyniki i używa ich do utworzenia obiektu GeoJSON Feature. Niespodziewanie w tym zapytaniu stosowany jest też maksymalny promień. Wartość „16090” to liczba metrów w 10 milach, czyli limit określony przez backend Go. Jeśli zastanawiasz się, dlaczego klauzula WHERE nie została dodana do zapytania wewnętrznego (w którym określana jest odległość każdej lokalizacji), to dlatego, że w sposób, w jaki SQL wykonuje zapytania w tle, to pole mogło nie zostać obliczone w momencie sprawdzania klauzuli WHERE. Jeśli spróbujesz przenieść tę klauzulę WHERE do zapytania wewnętrznego, pojawi się błąd.

**SELECT json_build_object(‘type', ‘FeatureColl**ection') To zapytanie umieszcza wszystkie wiersze wynikowe z zapytania generującego JSON w obiekcie GeoJSON FeatureCollection.

Dodawanie biblioteki PGX do projektu

Musimy dodać do projektu jedną zależność: sterownik i zestaw narzędzi PostGres, który umożliwia pulę połączeń. Najłatwiej to zrobić za pomocą modułów Go. Zainicjuj moduł za pomocą tego polecenia w Cloud Shell:

go mod init my_locator

Następnie uruchom to polecenie, aby przeskanować kod pod kątem zależności, dodać listę zależności do pliku mod i je pobrać.

go mod tidy

Na koniec uruchom to polecenie, aby pobrać zależności bezpośrednio do katalogu projektu, dzięki czemu można łatwo utworzyć kontener na potrzeby App Engine Flex.

go mod vendor

OK, możesz już to przetestować.

Wypróbuj

OK, zrobiliśmy już BARDZO dużo. Zobaczmy, jak to działa.

Aby maszyna deweloperska (nawet Cloud Shell) mogła połączyć się z bazą danych, musimy użyć serwera proxy Cloud SQL do zarządzania połączeniem z bazą danych. Aby skonfigurować Cloud SQL Proxy:

  1. Kliknij tutaj, aby włączyć Cloud SQL Admin API
  2. Jeśli korzystasz z lokalnego komputera deweloperskiego, zainstaluj narzędzie serwera proxy Cloud SQL. Jeśli używasz Cloud Shell, możesz pominąć ten krok, ponieważ jest już zainstalowany. Pamiętaj, że instrukcje będą odnosić się do konta usługi. Zostało już utworzone dla Ciebie konto, a w następnej sekcji omówimy dodawanie do niego niezbędnych uprawnień.
  3. Aby uruchomić serwer proxy, otwórz nową kartę (w Cloud Shell lub własnym terminalu).

bcca42933bfbd497.png

  1. Otwórz stronę https://console.cloud.google.com/sql/instances/locations/overview i przewiń w dół, aby znaleźć pole Nazwa połączenia. Skopiuj tę nazwę, aby użyć jej w następnym poleceniu.
  2. Na tej karcie uruchom serwer proxy Cloud SQL za pomocą tego polecenia, zastępując CONNECTION_NAME nazwą połączenia podaną w poprzednim kroku.
cloud_sql_proxy -instances=CONNECTION_NAME=tcp:5432

Wróć na pierwszą kartę Cloud Shell i zdefiniuj zmienne środowiskowe, których Go będzie potrzebować do komunikacji z backendem bazy danych, a następnie uruchom serwer w ten sam sposób co wcześniej:

Przejdź do katalogu głównego projektu, jeśli jeszcze w nim nie jesteś.

cd YOUR_PROJECT_ROOT

Utwórz te 5 zmiennych środowiskowych (zastąp YOUR_PASSWORD_HERE hasłem utworzonym powyżej).

export DB_USER=postgres
export DB_PASS=YOUR_PASSWORD_HERE
export DB_TCP_HOST=127.0.0.1 # Proxy
export DB_PORT=5432 #Default for PostGres
export DB_NAME=postgres

Uruchom lokalną instancję.

go run *.go

Otwórz okno podglądu. Powinno ono działać tak, jakby nic się nie zmieniło: możesz wpisać adres początkowy, powiększać i pomniejszać mapę oraz klikać lokalizacje punktów recyklingu. Teraz jednak jest ona oparta na bazie danych i przygotowana do skalowania.

9. Wyświetl listę najbliższych sklepów

Interfejs Directions API działa podobnie jak funkcja wyznaczania trasy w aplikacji Mapy Google – wystarczy wpisać jeden punkt początkowy i jeden punkt docelowy, aby otrzymać trasę między nimi. Interfejs Distance Matrix API rozwija tę koncepcję, aby identyfikować optymalne pary między wieloma możliwymi punktami początkowymi i wieloma możliwymi miejscami docelowymi na podstawie czasu podróży i odległości. W tym przypadku, aby pomóc użytkownikowi znaleźć najbliższy sklep w wybranej lokalizacji, podajesz jeden punkt początkowy i tablicę lokalizacji sklepów jako miejsca docelowe.

Dodaj odległość od miejsca pochodzenia do każdego sklepu

Na początku definicji funkcji initMap zastąp komentarz „// TODO: Start Distance Matrix service” tym kodem:

app.js - initMap

distanceMatrixService = new google.maps.DistanceMatrixService();

Na końcu pliku app.js dodaj nową funkcję o nazwie calculateDistances.

app.js

async function calculateDistances(origin, stores) {
  // Retrieve the distances of each store from the origin
  // The returned list will be in the same order as the destinations list
  const response = await getDistanceMatrix({
    origins: [origin],
    destinations: stores.map((store) => {
      const [lng, lat] = store.geometry.coordinates;
      return { lat, lng };
    }),
    travelMode: google.maps.TravelMode.DRIVING,
    unitSystem: google.maps.UnitSystem.METRIC,
  });
  response.rows[0].elements.forEach((element, index) => {
    stores[index].properties.distanceText = element.distance.text;
    stores[index].properties.distanceValue = element.distance.value;
  });
}

const getDistanceMatrix = (request) => {
  return new Promise((resolve, reject) => {
    const callback = (response, status) => {
      if (status === google.maps.DistanceMatrixStatus.OK) {
        resolve(response);
      } else {
        reject(response);
      }
    };
    distanceMatrixService.getDistanceMatrix(request, callback);
  });
};

Funkcja wywołuje interfejs Distance Matrix API, używając przekazanego do niej punktu początkowego jako pojedynczego punktu początkowego, a lokalizacji sklepów jako tablicy miejsc docelowych. Następnie tworzy tablicę obiektów zawierającą identyfikator sklepu, odległość wyrażoną w postaci czytelnego dla człowieka ciągu tekstowego, odległość w metrach jako wartość liczbową i sortuje tablicę.

Zaktualizuj funkcję initAutocompleteWidget, aby obliczać odległości od sklepów za każdym razem, gdy w pasku wyszukiwania Autouzupełnianie miejsc wybierane jest nowe miejsce początkowe. U dołu funkcji initAutocompleteWidget zastąp komentarz „// TODO: Calculate the closest stores” tym kodem:

app.js – initAutocompleteWidget

    // Use the selected address as the origin to calculate distances
    // to each of the store locations
    await calculateDistances(originLocation, stores);
    renderStoresPanel();

Wyświetlanie listy sklepów posortowanych według odległości

Użytkownik oczekuje, że zobaczy listę sklepów uporządkowaną od najbliższego do najdalszego. Wypełnij listę w panelu bocznym dla każdego sklepu, korzystając z listy zmodyfikowanej przez funkcję calculateDistances, aby określić kolejność wyświetlania sklepów.

Na końcu pliku app.js dodaj 2 nowe funkcje o nazwach renderStoresPanel()storeToPanelRow().

app.js

function renderStoresPanel() {
  const panel = document.getElementById("panel");

  if (stores.length == 0) {
    panel.classList.remove("open");
    return;
  }

  // Clear the previous panel rows
  while (panel.lastChild) {
    panel.removeChild(panel.lastChild);
  }
  stores
    .sort((a, b) => a.properties.distanceValue - b.properties.distanceValue)
    .forEach((store) => {
      panel.appendChild(storeToPanelRow(store));
    });
  // Open the panel
  panel.classList.add("open");
  return;
}

const storeToPanelRow = (store) => {
  // Add store details with text formatting
  const rowElement = document.createElement("div");
  const nameElement = document.createElement("p");
  nameElement.classList.add("place");
  nameElement.textContent = store.properties.business_name;
  rowElement.appendChild(nameElement);
  const distanceTextElement = document.createElement("p");
  distanceTextElement.classList.add("distanceText");
  distanceTextElement.textContent = store.properties.distanceText;
  rowElement.appendChild(distanceTextElement);
  return rowElement;
};

Uruchom ponownie serwer i odśwież podgląd, wykonując to polecenie:

go run *.go

Na koniec wpisz adres w Austin w Teksasie na pasku wyszukiwania autouzupełniania i kliknij jedną z sugestii.

Mapa powinna wyśrodkować się na tym adresie, a po jej prawej stronie powinien się pojawić pasek boczny z listą lokalizacji sklepów w kolejności odległości od wybranego adresu. Przykład:

96e35794dd0e88c9.png

10. Nadawanie stylu mapie

Skutecznym sposobem na wyróżnienie mapy jest dodanie do niej stylu. Dzięki definiowaniu stylów map w Google Cloud dostosowywanie map jest kontrolowane z poziomu konsoli Cloud za pomocą funkcji definiowania stylów map w Google Cloud (wersja beta). Jeśli wolisz dostosować styl mapy za pomocą funkcji, która nie jest w wersji beta, możesz skorzystać z dokumentacji stylów mapy, aby wygenerować kod JSON do programowego dostosowywania stylu mapy. Poniższe instrukcje przeprowadzą Cię przez proces definiowania stylów map w Google Cloud (wersja beta).

Tworzenie identyfikatora mapy

Najpierw otwórz konsolę Cloud i w polu wyszukiwania wpisz „Zarządzanie mapami”. Kliknij wynik „Zarządzanie mapami (Mapy Google)”64036dd0ed200200.png.

U góry (bezpośrednio pod polem wyszukiwania) zobaczysz przycisk Utwórz nowy identyfikator mapy. Kliknij tę opcję i wpisz dowolną nazwę. W sekcji Typ mapy wybierz JavaScript, a gdy pojawią się dodatkowe opcje, wybierz z listy Wektorowa. Wynik powinien wyglądać podobnie do tego na obrazie poniżej.

70f55a759b4c4212.png

Kliknij „Dalej”, a otrzymasz nowy identyfikator mapy. Możesz go teraz skopiować, ale nie martw się, łatwo go później znaleźć.

Następnie utworzymy styl, który zastosujemy do tej mapy.

Tworzenie stylu mapy

Jeśli nadal jesteś w sekcji Mapy w konsoli Cloud, u dołu menu nawigacyjnego po lewej stronie kliknij „Style mapy”. Możesz też, podobnie jak w przypadku tworzenia identyfikatora mapy, znaleźć odpowiednią stronę, wpisując w polu wyszukiwania „Style map” i wybierając z wyników „Style map (Mapy Google)”, jak na ilustracji poniżej.

9284cd200f1a9223.png

Następnie kliknij u góry przycisk „+ Utwórz nowy styl mapy”.

  1. Jeśli chcesz dopasować styl do mapy pokazanej w tym ćwiczeniu, kliknij kartę „IMPORT JSON” i wklej poniższy blok JSON. Jeśli chcesz utworzyć własny styl, wybierz styl mapy, od którego chcesz zacząć. Następnie kliknij Dalej.
  2. Wybierz utworzony identyfikator mapy, aby powiązać go z tym stylem, a następnie ponownie kliknij Dalej.
  3. Na tym etapie możesz dodatkowo dostosować styl mapy. Jeśli chcesz to zrobić, kliknij Dostosuj w edytorze stylów i eksperymentuj z kolorami oraz opcjami, aż uzyskasz styl mapy, który Ci się spodoba. W przeciwnym razie kliknij Pomiń.
  4. W następnym kroku wpisz nazwę i opis stylu, a następnie kliknij Zapisz i opublikuj.

Oto opcjonalny obiekt JSON do zaimportowania w pierwszym kroku.

[
  {
    "elementType": "geometry",
    "stylers": [
      {
        "color": "#d6d2c4"
      }
    ]
  },
  {
    "elementType": "labels.icon",
    "stylers": [
      {
        "visibility": "off"
      }
    ]
  },
  {
    "elementType": "labels.text.fill",
    "stylers": [
      {
        "color": "#616161"
      }
    ]
  },
  {
    "elementType": "labels.text.stroke",
    "stylers": [
      {
        "color": "#f5f5f5"
      }
    ]
  },
  {
    "featureType": "administrative.land_parcel",
    "elementType": "labels.text.fill",
    "stylers": [
      {
        "color": "#bdbdbd"
      }
    ]
  },
  {
    "featureType": "landscape.man_made",
    "elementType": "geometry.fill",
    "stylers": [
      {
        "color": "#c0baa5"
      },
      {
        "visibility": "on"
      }
    ]
  },
  {
    "featureType": "landscape.man_made",
    "elementType": "geometry.stroke",
    "stylers": [
      {
        "color": "#9cadb7"
      },
      {
        "visibility": "on"
      }
    ]
  },
  {
    "featureType": "poi",
    "elementType": "labels.text.fill",
    "stylers": [
      {
        "color": "#757575"
      }
    ]
  },
  {
    "featureType": "poi.park",
    "elementType": "labels.text.fill",
    "stylers": [
      {
        "color": "#9e9e9e"
      }
    ]
  },
  {
    "featureType": "road",
    "elementType": "geometry",
    "stylers": [
      {
        "color": "#ffffff"
      }
    ]
  },
  {
    "featureType": "road.arterial",
    "elementType": "geometry",
    "stylers": [
      {
        "weight": 1
      }
    ]
  },
  {
    "featureType": "road.arterial",
    "elementType": "labels.text.fill",
    "stylers": [
      {
        "color": "#757575"
      }
    ]
  },
  {
    "featureType": "road.highway",
    "elementType": "geometry",
    "stylers": [
      {
        "color": "#bf5700"
      }
    ]
  },
  {
    "featureType": "road.highway",
    "elementType": "geometry.stroke",
    "stylers": [
      {
        "visibility": "off"
      }
    ]
  },
  {
    "featureType": "road.highway",
    "elementType": "labels.text.fill",
    "stylers": [
      {
        "color": "#616161"
      }
    ]
  },
  {
    "featureType": "road.local",
    "elementType": "geometry",
    "stylers": [
      {
        "weight": 0.5
      }
    ]
  },
  {
    "featureType": "road.local",
    "elementType": "labels.text.fill",
    "stylers": [
      {
        "color": "#9e9e9e"
      }
    ]
  },
  {
    "featureType": "transit.line",
    "elementType": "geometry",
    "stylers": [
      {
        "color": "#e5e5e5"
      }
    ]
  },
  {
    "featureType": "transit.station",
    "elementType": "geometry",
    "stylers": [
      {
        "color": "#eeeeee"
      }
    ]
  },
  {
    "featureType": "water",
    "elementType": "geometry",
    "stylers": [
      {
        "color": "#333f48"
      }
    ]
  },
  {
    "featureType": "water",
    "elementType": "labels.text.fill",
    "stylers": [
      {
        "color": "#9e9e9e"
      }
    ]
  }
]

Dodawanie identyfikatora mapy do kodu

Skoro już udało Ci się utworzyć styl mapy, jak go użyć na własnej mapie? Musisz wprowadzić 2 niewielkie zmiany:

  1. Dodaj identyfikator mapy jako parametr adresu URL do tagu skryptu w index.html.
  2. Add identyfikator mapy jako argument konstruktora podczas tworzenia mapy w metodzie initMap().

W pliku HTML zastąp tag skryptu, który wczytuje interfejs Maps JavaScript API, adresem URL wczytywania podanym poniżej. Zastąp symbole zastępcze „YOUR_API_KEY” i „YOUR_MAP_ID”:

index.html

...
<script async defer src="https://maps.googleapis.com/maps/api/js?key=YOUR_API_KEY&v=weekly&libraries=places&callback=initialize&map_ids=YOUR_MAP_ID&solution_channel=GMP_codelabs_fullstackstorelocator_v1_a">
  </script>
...

W metodzie initMap klasy app.js, w której zdefiniowana jest stała map, usuń znacznik komentarza z wiersza właściwości mapId i zastąp „YOUR_MAP_ID_HERE” utworzonym właśnie identyfikatorem mapy:

app.js – initMap

...

// The map, centered on Austin, TX
 const map = new google.maps.Map(document.querySelector('#map'), {
   center: austin,
   zoom: 14,
   mapId: 'YOUR_MAP_ID_HERE',
// ...
});
...

Uruchom ponownie serwer.

go run *.go

Po odświeżeniu podglądu mapa powinna wyglądać zgodnie z Twoimi preferencjami. Oto przykład użycia stylów JSON powyżej.

2ece59c64c06e9da.png

11. Wdrażanie w środowisku produkcyjnym

Jeśli chcesz zobaczyć, jak aplikacja działa w App Engine Flex (a nie tylko na lokalnym serwerze internetowym na komputerze deweloperskim lub w Cloud Shell, jak dotychczas), jest to bardzo proste. Aby dostęp do bazy danych działał w środowisku produkcyjnym, musimy dodać jeszcze kilka elementów. Wszystkie te informacje znajdziesz na stronie dokumentacji Łączenie się z Cloud SQL z App Engine Flex.

Dodawanie zmiennych środowiskowych do pliku app.yaml

Najpierw wszystkie zmienne środowiskowe, których używasz do testowania lokalnego, musisz dodać na końcu pliku app.yaml aplikacji.

  1. Aby sprawdzić nazwę połączenia instancji, wejdź na https://console.cloud.google.com/sql/instances/locations/overview.
  2. Wklej ten kod na końcu pliku app.yaml.
  3. Zastąp YOUR_DB_PASSWORD_HERE hasłem utworzonym wcześniej dla nazwy użytkownika postgres.
  4. Zastąp YOUR_CONNECTION_NAME_HERE wartością z kroku 1.

app.yaml

# ...
# Set environment variables
env_variables:
    DB_USER: postgres
    DB_PASS: YOUR_DB_PASSWORD_HERE
    DB_NAME: postgres
    DB_TCP_HOST: 172.17.0.1
    DB_PORT: 5432

#Enable TCP Port
# You can look up your instance connection name by going to the page for
# your instance in the Cloud Console here : https://console.cloud.google.com/sql/instances/
beta_settings:
  cloud_sql_instances: YOUR_CONNECTION_NAME_HERE=tcp:5432

Pamiętaj, że DB_TCP_HOST powinna mieć wartość 172.17.0.1, ponieważ ta aplikacja łączy się przez App Engine Flex**.** Dzieje się tak, ponieważ będzie się on komunikować z Cloud SQL za pomocą serwera proxy, podobnie jak Ty.

Dodawanie uprawnień klienta SQL do konta usługi App Engine Flex

Otwórz stronę Administracja w Cloud Console i wyszukaj konto usługi, którego nazwa ma format service-PROJECT_NUMBER@gae-api-prod.google.com.iam.gserviceaccount.com. Jest to konto usługi, którego App Engine Flex będzie używać do łączenia się z bazą danych. Kliknij przycisk Edytuj na końcu wiersza i dodaj rolę „Klient Cloud SQL”.

b04ccc0b4022b905.png

Skopiuj kod projektu do ścieżki Go.

Aby App Engine mógł uruchomić Twój kod, musi mieć możliwość znalezienia odpowiednich plików w ścieżce Go. Sprawdź, czy jesteś w katalogu głównym projektu.

cd YOUR_PROJECT_ROOT

Skopiuj katalog do ścieżki go.

mkdir -p ~/gopath/src/austin-recycling
cp -r ./ ~/gopath/src/austin-recycling

Przejdź do tego katalogu.

cd ~/gopath/src/austin-recycling

Wdrażanie aplikacji

Użyj interfejsu wiersza poleceń gcloud, aby wdrożyć aplikację. Wdrożenie zajmie trochę czasu.

gcloud app deploy

Użyj polecenia browse, aby uzyskać link, który możesz kliknąć, aby zobaczyć w działaniu w pełni wdrożoną, profesjonalną i estetyczną wyszukiwarkę sklepów.

gcloud app browse

Jeśli polecenie gcloud było uruchamiane poza Cloud Shell, uruchomienie polecenia gcloud app browse spowoduje otwarcie nowej karty przeglądarki.

12. (Zalecane) Oczyść

Wykonanie tego ćwiczenia nie spowoduje przekroczenia limitów bezpłatnego poziomu przetwarzania BigQuery i wywołań interfejsu API Google Maps Platform, ale jeśli wykonasz je wyłącznie w celach edukacyjnych i chcesz uniknąć przyszłych opłat, najprostszym sposobem na usunięcie zasobów powiązanych z tym projektem jest usunięcie samego projektu.

Usuwanie projektu

W konsoli GCP otwórz stronę Cloud Resource Manager:

Na liście projektów wybierz projekt, nad którym pracowaliśmy, i kliknij Usuń. Pojawi się prośba o wpisanie identyfikatora projektu. Wpisz go i kliknij Wyłącz.

Możesz też usunąć cały projekt bezpośrednio z Cloud Shell za pomocą gcloud, uruchamiając to polecenie i zastępując symbol zastępczy GOOGLE_CLOUD_PROJECT identyfikatorem projektu:

gcloud projects delete GOOGLE_CLOUD_PROJECT

13. Gratulacje

Gratulacje! Udało Ci się ukończyć ćwiczenie z programowania.

lub przewiniesz do ostatniej strony. Gratulacje! Przewinięto do ostatniej strony.

W trakcie tego ćwiczenia z programowania korzystaliśmy z tych technologii:

Więcej informacji

Wciąż jest wiele do nauczenia się o tych technologiach. Poniżej znajdziesz przydatne linki do tematów, których nie udało nam się omówić w tym samouczku, ale które mogą Ci się przydać podczas tworzenia rozwiązania do lokalizowania sklepów dostosowanego do Twoich potrzeb.