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.
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 API i Distance 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:
- Utwórz konto rozliczeniowe.
- Utwórz projekt.
- Włącz interfejsy API i pakiety SDK Google Maps Platform (wymienione w poprzedniej sekcji).
- 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 (udostępnienie środowiska i połączenie się z nim powinno zająć tylko kilka chwil).
Spowoduje to otwarcie nowej powłoki w dolnej części przeglądarki (wcześniej może się wyświetlić pełnoekranowa reklama wprowadzająca).
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.
Następnie kliknij ikonę Otwórz w nowym oknie, aby przenieść edytor i terminal na nową kartę.
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:
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.
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.
- W oknie wiersza poleceń instancji Cloud Shell zamknij serwer, wpisując [CTRL] + [C].
- Utwórz katalog
data
w kataloguaustin-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
i /*.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.
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:
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.
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 initAutocompleteWidget
w 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 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}¢erLng=${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:
- 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
// ...
- 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 funkcjifetchAndRenderStores
.
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:
- Kliknij tutaj, aby włączyć Cloud SQL Admin API
- 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ń.
- Aby uruchomić serwer proxy, otwórz nową kartę (w Cloud Shell lub własnym terminalu).
- 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. - 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()
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 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:
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)”.
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.
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.
Następnie kliknij u góry przycisk „+ Utwórz nowy styl mapy”.
- 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.
- Wybierz utworzony identyfikator mapy, aby powiązać go z tym stylem, a następnie ponownie kliknij Dalej.
- 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ń.
- 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:
- Dodaj identyfikator mapy jako parametr adresu URL do tagu skryptu w
index.html
. Add
identyfikator mapy jako argument konstruktora podczas tworzenia mapy w metodzieinitMap()
.
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.
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.
- Aby sprawdzić nazwę połączenia instancji, wejdź na https://console.cloud.google.com/sql/instances/locations/overview.
- Wklej ten kod na końcu pliku
app.yaml
. - Zastąp
YOUR_DB_PASSWORD_HERE
hasłem utworzonym wcześniej dla nazwy użytkownikapostgres
. - 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”.
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:
- Maps JavaScript API
- Usługa macierzy odległości, Maps JavaScript API (dostępny jest też Distance Matrix API)
- Biblioteka miejsc, Maps JavaScript API (znana też jako Places API)
- Elastyczne środowisko App Engine (Go)
- Cloud SQL API
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.