Utwórz lokalizator sklepów ze stosami z Google Maps Platform i Google Cloud

1. Wprowadzenie

W skrócie

Załóżmy, że chcesz umieścić na mapie wiele miejsc i chcesz, aby użytkownicy mogli je zobaczyć i dowiedzieć się, które z nich chcą odwiedzić. Typowe przykłady:

  • lokalizator sklepów w witrynie sklepu
  • mapę miejsc sondaży, które zbliżają się do wyborów
  • katalog specjalistycznych lokalizacji, takich jak pojemniki do recyklingu baterii

Co stworzysz

W ramach tego ćwiczenia utworzysz lokalizator, który pobiera z aktywnego pliku danych wyspecjalizowanych lokalizacji i pomaga użytkownikowi znaleźć najbliższą lokalizację. Ten lokalny stos może obsłużyć znacznie więcej miejsc niż prosty lokalizator sklepów, który ogranicza liczbę lokalizacji do 25.

2ece59c64c06e9da.png

Czego się nauczysz

To ćwiczenie wymaga otwartego zbioru danych do symulowania wstępnie wypełnionych metadanych dotyczących większej liczby lokalizacji sklepów. Dzięki temu możesz skupić się na omówieniu kluczowych kwestii technicznych.

  • Maps JavaScript API: wyświetlanie dużej liczby lokalizacji na niestandardowej mapie internetowej
  • GeoJSON: format zapisu metadanych dotyczących lokalizacji.
  • Autouzupełnianie miejsc: pomóż użytkownikom szybciej i dokładniej podać lokalizacje początkowe
  • Go: język programowania, który służy do programowania backendu. Backend współdziała z bazą danych i wysyła wyniki zapytań z powrotem do interfejsu w formacie JSON.
  • App Engine: hostowanie aplikacji internetowej

Wymagania wstępne

  • Podstawowa wiedza o języku HTML i JavaScript
  • konto Google,

2. Konfiguracja

W kroku 3 tej sekcji włącz ćwiczenia z interfejsu Maps JavaScript API, Places API i Odległość Matrix API.

Pierwsze kroki z Google Maps Platform

Jeśli korzystasz z Google Maps Platform po raz pierwszy, zapoznaj się z przewodnikiem dla początkujących po Google Maps Platform lub obejrzyj tę playlistę, aby poznać te wskazówki:

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

Aktywowanie Cloud Shell

W tym ćwiczeniu wykorzystujesz Cloud Shell – środowisko wiersza poleceń działające w Google Cloud, które umożliwia dostęp do usług i zasobów działających w Google Cloud. Dzięki temu możesz hostować i uruchamiać projekt całkowicie w przeglądarce.

Aby aktywować Cloud Shell w Cloud Console, kliknij Aktywuj Cloud Shell 89665d8d348105cd.png (udostępnienie i połączenie się ze środowiskiem zajmie tylko kilka chwil).

5f504766b9b3be17.png

Spowoduje to otwarcie nowej powłoki w dolnej części przeglądarki po wyświetleniu wprowadzającej reklamy pełnoekranowej.

d3bb67d514893d1f.png

Potwierdź swój projekt

Po połączeniu z Cloud Shell powinno być widoczne, że projekt jest 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 został skonfigurowany, uruchom to polecenie:

gcloud config set project <YOUR_PROJECT_ID>

Włącz interfejs API AppEngine Flex

Interfejs API AppEngine Flex trzeba włączyć ręcznie z Cloud Console. W ten sposób nie tylko włączysz interfejs API, ale utworzysz też konto usługi elastycznego środowiska AppEngine, czyli uwierzytelnione konto, które będzie wchodzić w interakcje z usługami Google (na przykład bazami danych SQL) w imieniu użytkownika.

3. Witaj świecie

Backend: Hello World in Go

Na początek utwórz instancję aplikacji Go App Engine Flex, która będzie podstawą reszty ćwiczeń z programowania.

Na pasku narzędzi Cloud Shell kliknij przycisk Otwórz edytor, by 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 utwórz małą aplikację Go App Engine, aby sprawdzić, czy wszystko działa. Witaj świecie.

Katalog austin-recycling powinien się też pojawić na liście folderów edytora po lewej stronie. W katalogu austin-recycling utwórz plik o nazwie app.yaml. W pliku app.yaml umieść 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 konfiguracji konfiguruje aplikację App Engine tak, aby używała środowiska wykonawczego Go Flex. Podstawowe informacje na temat znaczenia elementów konfiguracji w tym pliku znajdziesz w dokumentacji standardowego środowiska Google App Engine 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 wstrzymać się na tym etapie, by dowiedzieć się, do czego służy ten kod, 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 zgodnych ze ścieżką "/".

Funkcja obsługi, którą ręcznie nazywa się handler, zapisuje ciąg tekstowy "Hello, world!". Tekst zostanie przekierowany z powrotem do przeglądarki, w której będzie można go przeczytać. W kolejnych krokach utworzymy moduły obsługi, które reagują na dane GeoJSON, a nie na proste ciągi zakodowane na stałe.

Po wykonaniu tych czynności będziesz mieć edytor podobny do tego:

2084fdd5ef594ece.png

Przetestuj

Aby przetestować tę aplikację, możesz uruchomić serwer programistyczny App Engine wewnątrz instancji Cloud Shell. Wróć do wiersza poleceń Cloud Shell i wpisz te informacje:

go run *.go

W wynikach testu zobaczysz kilka logów z informacją, że rzeczywiście działasz na serwerze deweloperskim w instancji Cloud Shell, a aplikacja internetowa Hello World na porcie 8080 obsługuje nas w trybie Hello world. W tej aplikacji możesz otworzyć kartę przeglądarki, naciskając przycisk Podgląd w przeglądarce, a potem wybierając w menu Cloud Shell pasek narzędzi Podgląd na port 8080.

4155fc1dc717ac67.png

Kliknięcie tego elementu menu otworzy w przeglądarce nową kartę ze słowami „Hello, world!”" udostępnianymi z serwera programistycznego App Engine.

W następnym kroku dodasz do tej aplikacji dane o recyklingu miasta Austin i zaczniesz wizualizować je.

4. Pobierz bieżące dane

GeoJSON, lingua franca w GIS

W poprzednim kroku wspomnieliśmy, że utworzysz moduły obsługi w kodzie Go, które renderują dane GeoJSON w przeglądarce. Czym jest GeoJSON?

W świecie systemu informacji geograficznej (GIS) musimy być w stanie przekazywać wiedzę o podmiotach geograficznych między systemami komputerowymi. Mapy są doskonałe dla ludzi, ale komputery zazwyczaj wolą mieć dane w bardziej przystępnych formatach.

GeoJSON to format kodowania struktur danych geograficznych, takich jak współrzędne recyklingu punktów w Austin w stanie Teksas. Format GeoJSON został ustandaryzowany w standardzie Internet Engineering Task Force o nazwie RFC7946. GeoJSON definiuje się na podstawie JSON (JavaScript Object Notation), który został ujednolicony w standardzie ECMA-404 przez tę samą organizację, która ustandaryzowała kod JavaScript Ecma International.

Bardzo ważne jest, aby GeoJSON to powszechnie obsługiwany format przewodów do przekazywania wiedzy geograficznej. Te ćwiczenia z programowania korzystają z GeoJSON na te sposoby:

  • Użyj pakietów Go do analizowania danych z Austin w wewnętrznej strukturze danych GIS, których używasz do filtrowania żądanych danych.
  • Zserializuj żądane dane do komunikacji między serwerem WWW a przeglądarką.
  • Użyj biblioteki JavaScript, aby przekonwertować odpowiedź na znaczniki na mapie.

Zaoszczędzisz dzięki temu dużo pisania, ponieważ nie musisz pisać parsatorów i generatorów, aby konwertować strumień danych przewodowo na pamięć.

Pobieranie danych

Portal miejski Austin, Texas Open Data Portal udostępnia publicznie dostępne informacje o zasobach publicznych. Dzięki nim dowiesz się, jak wizualizować zbiór danych dotyczący punktów recyklingu.

Możesz zwizualizować dane ze znacznikami na mapie przy użyciu warstwy danych interfejsu Maps JavaScript API.

Zacznij od pobrania do aplikacji danych GeoJSON ze strony City of Austin.

  1. W oknie wiersza poleceń Cloud Shell wyłącz serwer, naciskając [CTRL] + [C].
  2. Utwórz katalog data w katalogu austin-recycling i zmień go na ten katalog:
mkdir -p data && cd data

Teraz użyj parametru curl, aby pobrać adresy do recyklingu:

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

Na koniec utwórz kopię zapasową w katalogu nadrzędnym.

cd ..

5. Mapowanie lokalizacji

Najpierw zaktualizuj plik app.yaml, aby był bardziej stabilny &"a nie tylko Hello world – aplikacja, którą chcesz 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 i /*.html do zbioru plików statycznych. Oznacza to, że statyczny komponent HTML Twojej aplikacji będzie wyświetlany bezpośrednio przez infrastrukturę do udostępniania plików w App Engine, a nie przez aplikację Go. Zmniejsza to obciążenie serwera i zwiększa szybkość udostępniania.

Czas stworzyć backend w Go!

Tworzenie backendu

Jak pewnie zauważyłaś, jedną z ciekawych rzeczy, których nie robi Twój plik app.yaml, jest ujawnianie pliku GeoJSON. Wynika to z faktu, że GeoJSON zostanie przetworzony i wysłany przez nasze backendy. Dzięki temu w kolejnych krokach będziemy mogli tworzyć zaawansowane funkcje. Zmień plik main.go, aby wyświetlał się tak:

main.go

package main

import (
        "fmt"
        "io/ioutil"
        "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 := ioutil.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 już daje nam cenną funkcję: instancja AppEngine buforuje wszystkie te lokalizacje od razu po uruchomieniu. Dzięki temu zaoszczędzisz czas, ponieważ backend nie będzie musiał odczytywać pliku na dysku przy każdym odświeżeniu przez użytkownika.

Utwórz interfejs

Najpierw musimy utworzyć folder, w którym będą przechowywane wszystkie zasoby statyczne. W folderze nadrzędnym swojego projektu utwórz folder static.

mkdir -p static && cd static

Utworzymy 3 pliki w tym folderze.

  • index.html będzie zawierać cały kod HTML Twojej jednostronicowej aplikacji lokalizatora sklepów.
  • Zgodnie z oczekiwaniami interfejs style.css zawiera styl
  • app.js jest odpowiedzialny za pobranie danych GeoJSON, wywołanie interfejsu API Map Google i umieszczenie znaczników na mapie niestandardowej.

Utwórz te 3 pliki i umieść je w 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 URL src w tagu skryptu elementu head.

  • Zastąp tekst zastępczy YOUR_API_KEY znakiem klucza wygenerowanym podczas konfiguracji. Aby pobrać swój klucz interfejsu API lub wygenerować nowy, otwórz stronę Interfejsy API i > Dane logowania w Cloud Console.
  • Pamiętaj, że adres URL zawiera parametr callback=initialize. Będziemy teraz tworzyć plik JavaScript z tą funkcją wywołania zwrotnego. To tutaj Twoja aplikacja wczytuje lokalizacje z backendu, przesyła je do interfejsu API Map Google, a potem używa wyniku do oznaczania niestandardowych lokalizacji na mapie. Wszystkie te elementy świetnie wyglądają na Twojej stronie internetowej.
  • Parametr libraries=places ładuje Bibliotekę miejsc, która jest niezbędna do obsługi funkcji takich jak autouzupełnianie adresu, które zostanie 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 sprawdzić, co już mamy, wróć z katalogu nadrzędnego do wiersza nadrzędnego:

cd ..

Teraz ponownie uruchom aplikację w trybie programisty, używając:

go run *.go

Wyświetl podgląd tak samo jak wcześniej. Powinna wyświetlić się mapa z małymi, zielonymi okręgami.

58a6680e9c8e7396.png

renderujesz już lokalizacje na mapie i w połowie ćwiczeń z programowania jesteś w połowie! Niesamowite! Teraz dodajmy interaktywność.

6. Pokaż szczegóły na żądanie

Reagowanie na zdarzenia kliknięcia na znacznikach na mapie

Wyświetlenie kilku znaczników na mapie to dobry początek, jednak tak naprawdę wymagamy, aby użytkownik mógł kliknąć jeden z tych znaczników i zobaczyć informacje o danej lokalizacji (takie jak nazwa firmy, adres itp.). Nazwa niewielkiego okna informacyjnego, które wyświetla się zwykle po kliknięciu znacznika Map Google, to Okno informacyjne.

Utwórz obiekt infoWindow. Do funkcji initialize dodaj ten fragment, zastępując komentarz z tekstem „// TODO: Initialize an info window"”.

app.js – inicjowanie

  // 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 nieco inną wersją, co powoduje, że ostatni wiersz wywołuje użytkownika storeToCircle 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ę obiektu storeToCircle nieco dłuższą wersją, która teraz określa 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;
};

Powyższy nowy kod powoduje wyświetlenie infoWindow z wybranym sklepem po kliknięciu znacznika sklepu na mapie.

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

1af0ab72ad0eadc5.png

7. Pobierz lokalizację początkową użytkownika

Użytkownicy lokalizatorów sklepów zazwyczaj chcą wiedzieć, który sklep znajduje się najbliżej nich lub adres, dla którego planują rozpocząć podróż. Dodawanie paska wyszukiwania autouzupełniania miejsc, aby użytkownik mógł łatwo wpisać adres początkowy. Autouzupełnianie miejsc działa podobnie jak na innych paskach wyszukiwania Google. Różnica polega na tym, że prognozy pochodzą ze wszystkich miejsc w Google Maps Platform.

Utwórz pole do wprowadzania danych użytkownika

Wróć, aby edytować styl style.css i dodać styl paska wyszukiwania autouzupełniania oraz powiązanego panelu bocznego z wynikami. W trakcie aktualizowania stylów CSS będziemy także dodawać style do przyszłego paska bocznego, który będzie zawierał informacje o sklepie jako listę obok mapy.

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 panel wysunięcia są początkowo ukryte, dopóki nie są potrzebne.

Przygotuj element div dla widżetu autouzupełniania, zastępując komentarz w pliku index.html z tekstem "<!-- Autocomplete div goes here -->&quot. Wprowadzając tę zmianę, dodajemy też element div do 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>

Następnie zdefiniuj funkcję, aby dodać do mapy widżet autouzupełniania, dodając na końcu tego kodu ten kod: 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 sugestii autouzupełniania do adresów zwrotnych (ponieważ autouzupełnianie miejsc może też odpowiadać nazwom instytucji i adresom administracyjnym) oraz ogranicza adresy zwrócone tylko do adresów w Stanach Zjednoczonych. Dodanie tych opcjonalnych specyfikacji zmniejsza liczbę znaków, które użytkownik musi wpisać, aby zawęzić zapytania i wyświetlić adres, którego szukają.

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

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

app.js – zainicjuj

 // Initialize the Places Autocomplete Widget
 initAutocompleteWidget();

Uruchom serwer ponownie, uruchamiając następujące polecenie, a następnie odśwież podgląd.

go run *.go

W prawym górnym rogu mapy powinien wyświetlić się widżet autouzupełniania, który pokazuje adresy ze Stanów Zjednoczonych pasujące do wpisanego adresu.

58e9bbbcc4bf18d1

Aktualizacja mapy, gdy użytkownik wybierze adres początkowy

Teraz musisz zdecydować, kiedy użytkownik wybiera podpowiedź z widżetu Autouzupełnianie i użyć tej lokalizacji jako podstawy do obliczenia odległości do Twoich sklepów.

Dodaj ten kod na końcu initAutocompleteWidget (app.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 detektor, a gdy użytkownik kliknie jedną z sugestii, użytkownik zostanie ponownie wyrównany na mapie pod kątem wybranego adresu i wyznaczy punkt początkowy do obliczenia dystansu. Obliczenia dystansu musisz zaimplementować w przyszłym kroku.

Zatrzymaj i ponownie uruchom serwer, a potem odśwież podgląd, by ponownie wyśrodkować mapę, gdy wpiszesz adres na pasku wyszukiwania autouzupełniania.

8. Skaluj za pomocą Cloud SQL

Do tej pory mamy świetny lokalizator sklepów. Wykorzystuje to tylko około stu miejsc, z których aplikacja będzie korzystać – wczytuje je do pamięci backendu, zamiast odczytywać je wielokrotnie. Co jednak, jeśli lokalizator musi działać na inną skalę? Jeśli masz setki lokalizacji rozproszonych na dużym obszarze geograficznym (lub w innych miejscach na całym świecie), dobrym pomysłem jest utrwalenie ich wszystkich w pamięci, a podział stref na poszczególne pliki spowoduje wprowadzenie nowych problemów.

Czas załadować lokalizacje z bazy danych. Na tym etapie przenieśmy wszystkie lokalizacje z pliku GeoJSON do bazy danych Cloud SQL i zaktualizujemy backend Go, aby pobierać wyniki z tej bazy zamiast ze swojej lokalnej pamięci podręcznej za każdym razem, gdy pojawi się prośba.

Tworzenie instancji Cloud SQL z bazą danych PostGres

Możesz utworzyć instancję Cloud SQL za pomocą Google Cloud Console, ale jeszcze łatwiej możesz ją utworzyć za pomocą narzędzia gcloud. 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ą chcemy nadać tej instancji Cloud SQL.
  • Flaga tier to sposób na wybranie niektórych z wygodnych wstępnie zdefiniowanych maszyn.
  • Wartość db-custom-1-3840 oznacza, że utworzona instancja powinna mieć jeden procesor wirtualny i około 3,75 GB pamięci.

Instancja Cloud SQL zostanie utworzona i zainicjowana z bazą danych PostGresSQL z domyślnym użytkownikiem postgres. Jakie jest hasło tego użytkownika? To świetne pytanie. Nie mają. Zanim się zalogujesz, musisz ją skonfigurować.

Ustaw hasło przy użyciu następującego polecenia:

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

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

Włącz rozszerzenie PostGIS

PostGIS to rozszerzenie PostGresSQL, które ułatwia przechowywanie standardowych typów danych geoprzestrzennych. W normalnych warunkach musimy przejść pełną procedurę instalacji, aby dodać PostGIS do naszej bazy danych. Na szczęście jest to jedno z obsługiwanych w Cloud SQL rozszerzeń do PostGresSQL.

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

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

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

CREATE EXTENSION postgis;

Jeśli operacja się uda, wynik powinien być następujący: UTWÓRZ ROZSZERZENIE, jak pokazano poniżej.

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 te dane o lokalizacji z plików GeoJSON do naszej nowej bazy danych.

Na szczęście jest to dobrze rozwiązanie i w internecie można znaleźć kilka narzędzi, które pozwolą zautomatyzować ten proces. Wykorzystamy narzędzie ogr2ogr, które będzie konwertować wiele popularnych formatów do przechowywania danych geoprzestrzennych. Spośród tych opcji wiesz, że można przekonwertować formularz GeoJSON na plik zrzutu SQL. Tego pliku można użyć do utworzenia tabel i kolumn bazy danych, a potem wczytać je ze wszystkich danych, które znajdują się w plikach GeoJSON.

Utwórz plik zrzutu SQL

Najpierw zainstaluj ogr2ogr.

sudo apt-get install gdal-bin

Następnie użyj polecenia 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

To polecenie działa w oparciu o uruchomienie w folderze austin-recycling. Jeśli chcesz uruchomić go z innego katalogu, zastąp data ścieżką do katalogu, w którym jest przechowywany plik recycling-locations.geojson.

Uzupełnianie bazy danych o punkty recyklingu

Po wykonaniu tego polecenia powinno być już plik (datadump.sql,) w tym samym katalogu, w którym uruchomiono polecenie. Jeśli się otworzysz, zobaczysz około stu wierszy SQL, tworząc tabelę austinrecycling i wypełniając ją lokalizacjami.

Teraz otwórz połączenie z bazą danych i uruchom ten skrypt, korzystając z tego polecenia.

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

Jeśli skrypt zostanie uruchomiony, oto kilka ostatnich wierszy danych wyjściowych, 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

Zaktualizuj backend, aby używać Cloud SQL

Mając już te dane w naszej bazie danych, możemy zaktualizować nasz kod.

Zaktualizuj interfejs, aby wysyłać informacje o lokalizacji

Zacznijmy od jednej niewielkiej aktualizacji w interfejsie: ponieważ piszemy teraz dla tej aplikacji w skali, w której nie chcemy wyświetlać każdą lokalizację za każdym razem, gdy zapytanie jest uruchomione, musimy przekazać kilka podstawowych informacji o interfejsie użytkownika, które jest istotne dla użytkownika.

Otwórz app.js i zastąp definicję funkcji fetchStores tą wersją, by podać długość i szerokość geograficzną tego adresu URL.

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 ukończeniu tego ćwiczenia z ćwiczeniami odpowiedź zwróci tylko sklepy znajdujące się najbliżej współrzędnych mapy podanych w parametrze center. W przypadku przykładowego kodu w funkcji initialize przykładowy kod podany w tym module używa środkowych współrzędnych w Austin w Teksasie.

Ponieważ fetchStores zwraca teraz tylko podzbiór lokalizacji sklepów, będziemy musieli pobrać je ponownie za każdym razem, gdy użytkownik zmieni lokalizację początkową.

Zaktualizuj funkcję initAutocompleteWidget, by odświeżać lokalizacje po ustawieniu nowego punktu początkowego. Wymagają 2 zmian:

  1. W narzędziu initAutocompleteWidget znajdź wywołanie zwrotne detektora place_changed. Odznacz wiersz, który czyści istniejące kręgi, co spowoduje, że będzie on uruchamiany za każdym razem, gdy użytkownik wybierze adres w autouzupełnianiu wyników wyszukiwania miejsc.

app.js – initAutocompleteWidget

  autocomplete.addListener("place_changed", async () => {
    circles.forEach((c) => c.setMap(null)); // clear existing stores
    // ...
  1. Po każdej zmianie wybranego punktu początkowego zmienna originLocation jest aktualizowana. Na końcu &backt;place_changed" wywołania komentarza usuń komentarz nad wierszem &"// TODO: Calculate the closest stores", aby przekazać to nowe źródło do nowego wywołania funkcji fetchAndRenderStores.

app.js – initAutocompleteWidget

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

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

Usuń odczyt i pamięć podręczną plików GeoJSON

Najpierw zmień main.go, by usunąć kod, który wczytuje i zapisuje w pamięci podręcznej prosty plik GeoJSON. Możemy też pozbyć się funkcji dropoffsHandler, ponieważ jedną z technologii Cloud SQL zapiszemy w innym pliku.

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)
        }
}

Utwórz nowy moduł obsługi żądań lokalizacji

Teraz utwórz nowy plik locations.go, także w katalogu recyklingu austin. Zacznij od ponownego wdrożenia modułu obsługi w przypadku żą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)
}

Moduł obsługi wykonuje te istotne zadania:

  • Pobiera szerokość i długość geograficzną z obiektu żądania (pamiętasz, jak dodaliśmy je do adresu URL? )
  • Zostaną uruchomione wywołanie getGeoJsonFromDatabase, które zwróci ciąg danych GeoJSON (napiszemy to później).
  • Wykorzystuje ResponseWriter do wydrukowania tego ciągu GeoJSON w odpowiedzi.

Następnie utworzymy pulę połączeń, aby ułatwić skalowanie wykorzystania bazy danych w tym samym czasie przez użytkowników.

Utwórz pulę połączeń

Pula połączenia to zbiór aktywnych połączeń bazy danych, których serwer może ponownie używać do żądań użytkowników usług. Zmniejsza to koszty pracy w miarę skalowania się liczby aktywnych użytkowników, bo serwer nie musi tracić czasu na tworzenie i niszczenie połączeń. Jak pewnie wiesz, 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 funkcji locations.go utwórz funkcję initConnectionPool (wywołaną z metody main.go), która inicjuje pulę połączeń. Dla ułatwienia używamy kilku metod pomocniczych. configureConnectionPool to wygodne miejsce do zmiany ustawień puli, takich jak liczba połączeń i czas trwania połączenia. mustGetEnv opakowuje wywołania w celu pobrania wymaganych zmiennych środowiskowych, więc przydatne komunikaty o błędach mogą być wyświetlane w przypadku, gdy w instancji brakuje ważnych informacji (takich jak adres IP lub nazwa bazy danych, z którą można się połączyć).

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
}

Wyślij zapytanie do bazy danych o lokalizacje i pobierz kod JSON.

Teraz utworzymy zapytanie do bazy danych, które zawiera współrzędne mapy i zwraca 25 najbliższych lokalizacji. Nie tylko to, ale ze względu na fantazyjną, nowoczesną funkcję bazodanowa zwraca te dane jako GeoJSON. Końcowa różnica wynika z tego, że co widać w kodzie interfejsu, nic się nie zmieniło. Przed uruchomieniem żądania do adresu URL i wykonaniem szeregu danych GeoJSON. Teraz powoduje wysłanie żądania do adresu URL i zwraca szereg GeoGeo.

Za pomocą tej funkcji Magia Dodaj tę funkcję za modułem obsługi i kodem puli, który został właśnie zapisany u dołu tabeli 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, usuwania i obsługi błędów w celu uruchomienia żądania do bazy danych. Przyjrzymy się rzeczywistemu programowi SQL, który ma w obszarze bazy danych bardzo wiele interesujących funkcji, więc nie musisz się martwić implementacją ich w kodzie.

Nieprzetworzone zapytanie, które zostanie przeprowadzone, gdy zostanie już przeanalizowany, a wszystkie literały ciągu pojawią się w odpowiednich miejscach, będą wyglądać tak:

analiza.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 wyświetlić jako jedno zapytanie główne i niektóre funkcje dodawania tagów JSON.

SELECT * ... LIMIT 25 wybiera wszystkie pola dla każdej lokalizacji. Następnie wykorzystuje funkcję ST_DISTANCE (należącą do zestawu pomiarów geograficznych) do określenia odległości między każdą lokalizacją bazy danych a długą i długą parą lokalizacji podanej w interfejsie użytkownika. Pamiętaj, że w odróżnieniu od macierzy odległości, która może określać dystans dojazdu, są to dystanse GeoSpatial. Aby zwiększyć wydajność, wykorzystuje następnie odległość do sortowania i zwracania 25 lokalizacji znajdujących się najbliżej lokalizacji użytkownika.

**SELECT json_build_object(‘type', ‘F**eature') opakowuje poprzednie zapytanie, wyniki i wykorzystywane do utworzenia obiektu GeoJSON. Nieoczekiwane zapytanie powoduje też stosowanie maksymalnego promienia "16090" liczby metrów na 10 milach, czyli stałego limitu ustalonego przez backend Go. Jeśli nie zastanawiasz się, dlaczego klauzula WHERE nie została dodana do wewnętrznego zapytania (gdzie odległość jest określona dla każdej lokalizacji), jest to spowodowane tym, jak SQL wykonuje się za kulisami, to pole mogło nie zostać obliczone podczas sprawdzania klauzuli WHERE. Jeśli spróbujesz przenieść tę klauzulę WHERE do wewnętrznego zapytania, wystąpi błąd.

**SELECT json_build_object(‘type', ‘FeatureColl**ection') To zapytanie pakuje wszystkie powstałe wiersze z zapytania wygenerowanego w obiekcie JSON w elemencie GeoJSON FeatureCollection.

Dodawanie biblioteki PGX do projektu

Musimy dodać do Twojego projektu jedną zależność: PostGres Driver & Tools, który umożliwia łączenie połączeń. Najłatwiej zrobić to 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 zeskanować kod pod kątem zależności, dodać listę zależności do pliku mod i pobrać je.

go mod tidy

Na koniec uruchom to polecenie, aby pobrać zależności bezpośrednio do katalogu projektu, co pozwoli na łatwe utworzenie kontenera na potrzeby AppEngine Flex.

go mod vendor

Wszystko jest gotowe do przetestowania.

Przetestuj

Dobrze, mnóstwo zadań. Zobaczmy, jak to działa.

Aby Twój komputer deweloperski (a nawet usługa Cloud Shell) mógł połączyć się z bazą danych, do zarządzania połączeniem z bazą danych musimy używać serwera proxy Cloud SQL. Aby skonfigurować serwer proxy Cloud SQL:

  1. Tutaj dowiesz się, jak włączyć interfejs Cloud SQL Admin API
  2. Jeśli korzystasz z komputera lokalnego, zainstaluj narzędzie proxy Cloud SQL. Jeśli korzystasz z Cloud Shell, możesz pominąć ten krok. Jest już zainstalowany. Pamiętaj, że te instrukcje odnoszą się do konta usługi. Jeden z nich został już utworzony. W następnej sekcji opisujemy, jak dodać do niego niezbędne uprawnienia.
  3. Utwórz nową kartę (w Cloud Shell lub własnym terminalu), aby uruchomić serwer proxy.

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 widoczną 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 bazą danych, a potem uruchom serwer w taki sam sposób jak wcześniej:

Przejdź do katalogu głównego projektu, jeśli jeszcze go tam nie ma.

cd YOUR_PROJECT_ROOT

Utwórz 5 zmiennych środowisk (zastąp ciąg YOUR_PASSWORD_HERE utworzonym wcześniej hasłem).

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 instancję lokalną.

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ększyć mapę i kliknąć miejsca, w których można z niego korzystać. Teraz baza danych jest gotowa na skalowanie.

9. Lista najbliższych sklepów

Interfejs DIRECTION API działa bardzo podobnie do wysyłania próśb o wskazówki dojazdu w aplikacji Mapy Google – wprowadzenie jednego miejsca docelowego i jednego miejsca docelowego pozwala uzyskać trasę między nimi. Interfejs Odległość z matrycy interfejsu API umożliwia dokładniejsze zdefiniowanie optymalnego parowania między wieloma możliwymi miejscami docelowymi i wieloma miejscami docelowymi na podstawie czasu podróży i odległości. Aby pomóc użytkownikowi znaleźć najbliższy sklep pod wybranym adresem, podasz 1 źródło i tablicę sklepów w miejscu docelowym.

dodać odległość od punktu początkowego do każdego sklepu,

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

app.js – initMap

distanceMatrixService = new google.maps.DistanceMatrixService();

Dodaj nową funkcję na końcu elementu app.js 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);
  });
};

Ta funkcja wywołuje interfejs API dystansu, korzystając z przekazanego do niego punktu początkowego jako pojedynczego punktu początkowego i lokalizacji sklepów jako tablicy miejsc docelowych. Następnie tworzy tablicę obiektów przechowujących identyfikator sklepu, odległość wyrażoną w czytelnym dla człowieka ciągu, odległość w metrach jako wartość liczbową i sortuje tablicę.

Zaktualizuj funkcję initAutocompleteWidget, aby obliczać odległości w sklepach za każdym razem, gdy na pasku wyszukiwania miejsca autouzupełniania w miejscu zostanie wybrana nowa lokalizacja. Na dole funkcji initAutocompleteWidget zastąp komentarz &#quot; 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świetlić listę sklepów posortowaną według odległości;

Użytkownik oczekuje, że zobaczy listę sklepów uporządkowanych od najbliższej do najbliższej. Aby określić kolejność wyświetlania w sklepie, wypełnij informacje w panelu bocznym dla każdego sklepu za pomocą listy zmodyfikowanej przez funkcję calculateDistances.

Na końcu funkcji app.js dodaj 2 nowe funkcje o nazwie renderStoresPanel() i 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 serwer ponownie i odśwież podgląd, uruchamiając następujące polecenie.

go run *.go

Na koniec wpisz adres Austin (Teksas) w pasku wyszukiwania autouzupełniania i kliknij jedną z sugestii.

Mapa powinna zostać wyśrodkowana na tym adresie i pojawi się pasek boczny z listą lokalizacji sklepów uporządkowaną w określonej odległości od wybranego adresu. Oto przykład:

96e35794dd0e88c9.png

10. Dostosuj styl mapy

Jednym ze skutecznych sposobów na wyróżnienie mapy jest dodanie jej stylu. Styl map jest określany w chmurze, więc za jego pomocą można dostosować styl map, korzystając z usługi Cloud Styl. Jeśli wolisz dostosować styl mapy za pomocą funkcji, która nie jest w wersji beta, możesz skorzystać z dokumentacji dotyczącej stylu mapy, która pomaga wygenerować kod JSON na potrzeby programowego określania stylu mapy. Poniżej znajdziesz wskazówki dotyczące korzystania ze stylu mapy w Google Cloud (beta).

Utwórz identyfikator mapy

Najpierw otwórz Cloud Console i w polu wyszukiwania wpisz „"Map Management"”. Kliknij wynik o nazwie "Map Management (Mapy Google).&quot.64036dd0ed200200.png

U góry (pod polem wyszukiwania) pojawi się przycisk Utwórz nowy identyfikator mapy. Kliknij ją i wpisz dowolną nazwę. Pamiętaj, by jako typ mapy wybrać JavaScript, a gdy pojawią się dodatkowe opcje, wybierz z listy Wektor. Końcowy wynik powinien wyglądać podobnie do poniższego obrazu.

70f55a759b4c4212.png

Kliknij „Dalej”, a zobaczysz nowy identyfikator mapy. Jeśli chcesz, możesz skopiować ją teraz, ale nie przejmuj się – później możesz ją łatwo znaleźć.

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

Utwórz styl mapy

Jeśli nadal jesteś w sekcji Map Google w Cloud Console, kliknij „Style mapy” u dołu menu nawigacyjnego po lewej stronie. W przeciwnym razie, podobnie jak w przypadku tworzenia identyfikatora mapy, możesz znaleźć właściwą stronę, wpisując w polu wyszukiwania &" Style map (Mapy Google)&quot, tak jak na poniższym obrazie.

9284cd200f1a9223.png

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

  1. Jeśli chcesz dostosować styl do mapy widocznej w tym module, kliknij kartę IMPORTUJ JSON i wklej poniższe obiekty blob JSON. Jeśli chcesz utworzyć własny widok, wybierz styl mapy, od którego chcesz zacząć. Następnie kliknij Dalej.
  2. Wybierz utworzony przed chwilą identyfikator mapy, by powiązać go z tym stylem, i ponownie kliknij Dalej.
  3. Na tym etapie możesz dodatkowo dostosować styl mapy. Jeśli chcesz zapoznać się z tymi funkcjami, kliknij Dostosuj w edytorze stylu i eksperymentuj z kolorami i opcjami, aż znajdziesz styl mapy, który Ci się podoba. W przeciwnym razie kliknij Pomiń.
  4. W następnym kroku wpisz nazwę i opis stylu, a potem kliknij Zapisz i opublikuj.

Oto opcjonalny obiekt blob JSON zaimportowany 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"
      }
    ]
  }
]

Dodaj identyfikator mapy do kodu

Skoro wiesz już, jak utworzyć ten styl mapy, jak możesz go teraz użyć na swojej mapie? Musisz wprowadzić dwie drobne zmiany:

  1. Dodaj identyfikator mapy jako parametr URL do tagu skryptu w index.html
  2. Add Identyfikator mapy jako argument konstruktora, gdy tworzysz mapę za pomocą metody initMap().

Zastąp tag skryptu, który wczytuje interfejs API JavaScript Map Google w pliku HTML, adresem URL wczytującym poniżej, zastępując obiekty zastępcze ciągu "YOUR_API_KEY" &"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 app.js, w której zdefiniowano stałą map, usuń komentarz do wiersza właściwości mapId i zastąp "YOUR_MAP_ID_HERE" nowo utworzonym 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 serwer ponownie.

go run *.go

Po odświeżeniu podglądu mapa powinna wyglądać zgodnie z Twoimi preferencjami. Oto przykład, który został opisany powyżej.

2ece59c64c06e9da.png

11. Wdróż w wersji produkcyjnej

Jeśli chcesz, aby Twoja aplikacja działała z AppEngine Flex (a nie tylko z lokalnego serwera WWW na Twoim komputerze programistycznym lub w Cloud Shell, co do tego właśnie służy), to bardzo łatwe. Musimy jeszcze dodać kilka rzeczy, aby dostęp do bazy danych działał w środowisku produkcyjnym. Zostało to opisane na stronie z informacjami o łączeniu Flex App Engine z Cloud SQL.

Dodaj zmienne środowiskowe do pliku App.yaml

Najpierw musisz dodać wszystkie zmienne środowiskowe, których używałeś do testowania lokalnego, na dole pliku app.yaml aplikacji.

  1. Nazwę połączenia instancji znajdziesz na https://console.cloud.google.com/sql/instances/locations/overview.
  2. Wklej ten kod na końcu strony 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

Uwaga: DB_TCP_HOST powinien mieć wartość 172.17.0.1, ponieważ ta aplikacja łączy się przez AppEngine Flex**.** Wynika to z tego, że komunikowania się z Cloud SQL przez serwer proxy (podobnie jak wtedy).

Dodaj uprawnienia klienta SQL do konta usługi Flex Engine

Otwórz stronę Administrator w Cloud Console i znajdź konto usługi o nazwie service-PROJECT_NUMBER@gae-api-prod.google.com.iam.gserviceaccount.com. To konto usługi będzie używane przez App Engine Flex 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 działał z kodem, 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

Wdróż aplikację za pomocą interfejsu wiersza poleceń gcloud. Wdrożenie może trochę potrwać.

gcloud app deploy

Użyj polecenia browse, aby uzyskać link, którego kliknięcie spowoduje wyświetlenie w pełni działającego, estetycznego lokalizatora sklepów.

gcloud app browse

Jeśli przeglądarka gcloud działała poza powłoką Cloud Shell, uruchomienie przeglądarki gcloud app browse spowodowałoby otwarcie nowej karty przeglądarki.

12. (Zalecane) Czyszczenie danych

Wykonanie tych ćwiczeń z programowania pozostanie w granicach bezpłatnego poziomu na potrzeby przetwarzania BigQuery i wywołań interfejsu Maps Platform API, ale jeśli zostało to wykonane wyłącznie w ramach ćwiczenia edukacyjnego i nie chcesz ponosić opłat w przyszłości, 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 już pracujemy, i kliknij Usuń. Pojawi się prośba o podanie identyfikatora projektu. Wpisz wartość i kliknij Wyłącz.

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

gcloud projects delete GOOGLE_CLOUD_PROJECT

13. Gratulacje

Gratulacje! Ćwiczenia z programowania zostały ukończone.

Albo przeskakujesz do ostatniej strony. Gratulacje! Udało Ci się przejść do ostatniej strony.

W trakcie tych ćwiczeń z pracy nad tymi technologiami pracowaliśmy nad tymi sprawami:

Więcej informacji

Wciąż masz mnóstwo informacji na temat wszystkich tych technologii. Poniżej znajdziesz linki do przydatnych tematów z tego kursu, ale z pewnością pomogą Ci one w ustaleniu lokalizatora sklepów dostosowanego do Twoich potrzeb.