Eine vollständige Filialsuche mit der Google Maps Platform und Google Cloud erstellen

1. Einführung

Zusammenfassung

Stellen Sie sich vor, Sie haben viele Orte auf einer Karte und die Nutzer sollen sehen können, wo sie sich befinden und welchen Ort sie besuchen möchten. Hier einige Beispiele:

  • eine Filialsuche auf der Website eines Einzelhändlers
  • Karte mit Wahllokalen für anstehende Wahlen
  • ein Verzeichnis von Fachgeschäften wie Batterierecyclingbehältern

Aufgaben

In diesem Codelab erstellen Sie eine Standortsuche, die auf einem Live-Datenfeed spezialisierter Standorte basiert und Nutzern hilft, den nächstgelegenen Standort zu finden. Diese erweiterte Filialsuche kann eine viel größere Anzahl von Orten verarbeiten als die einfache Filialsuche, die mit maximal 25 Geschäftsstandorten möglich ist.

2ece59c64c06e9da.png

Lerninhalte

In diesem Codelab wird ein Open-Dataset verwendet, um vorab ausgefüllte Metadaten für eine große Anzahl von Ladengeschäften zu simulieren, sodass Sie sich auf das Erlernen der wichtigsten technischen Konzepte konzentrieren können.

  • Maps JavaScript API: Viele Standorte auf einer benutzerdefinierten Webkarte einblenden
  • GeoJSON: ein Format, in dem Metadaten zu Standorten gespeichert werden.
  • Place Autocomplete: Hiermit können Nutzer Startpositionen schneller und genauer angeben
  • Go: Die Programmiersprache, die bei der Entwicklung des Anwendungs-Back-Ends verwendet wird. Das Back-End interagiert mit der Datenbank und sendet die Abfrageergebnisse im formatierten JSON-Format an das Front-End.
  • App Engine: zum Hosten der Webanwendung

Vorbereitung

  • Grundkenntnisse in HTML und JavaScript
  • Ein Google-Konto

2. Einrichten

Aktivieren Sie in Schritt 3 des folgenden Abschnitts die Maps JavaScript API, die Places API und die Distance Matrix API für dieses Codelab.

Einführung in Google Maps Platform

Wenn du die Google Maps Platform noch nicht verwendet hast, folge der Einführung in die Google Maps Platform oder folge der Anleitung unter Erste Schritte mit der Google Maps Platform-Playlist.

  1. Erstellen Sie ein Rechnungskonto.
  2. Projekt erstellen
  3. Aktivieren Sie die im vorherigen Abschnitt aufgeführten Google Maps Platform APIs und SDKs.
  4. Generieren Sie den API-Schlüssel.

Cloud Shell aktivieren

In diesem Codelab nutzen Sie Cloud Shell, eine Befehlszeilenumgebung, die in Google Cloud ausgeführt wird und Zugriff auf Produkte und Ressourcen bietet, die in Google Cloud ausgeführt werden. So können Sie Ihr Projekt komplett über Ihren Webbrowser hosten und ausführen.

Klicken Sie auf Cloud Shell aktivieren 89665d8d348105cd, um Cloud Shell über die Cloud Console zu aktivieren. Die Bereitstellung und Verbindung mit der Umgebung dauert nur einen Moment.

5f504766b9b3be17.png

Dadurch wird unter Umständen eine neue Shell im unteren Teil des Browsers geöffnet, in der ein Interstitial eingeblendet wird.

d3bb67d514893d1f.png

Projekt bestätigen

Wenn Sie eine Verbindung zu Cloud Shell hergestellt haben, sehen Sie, dass Sie bereits authentifiziert sind und dass das Projekt bereits auf die Projekt-ID gesetzt ist, die Sie bei der Einrichtung ausgewählt haben.

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

Führen Sie den folgenden Befehl aus, wenn das Projekt nicht festgelegt ist:

gcloud config set project <YOUR_PROJECT_ID>

App Engine Flex API aktivieren

Die AppEngine Flex API muss manuell über die Cloud Console aktiviert werden. Dadurch wird nicht nur die API aktiviert, sondern auch das AppEngine-Umgebungsdienstkonto für flexible Umgebungen erstellt, also das authentifizierte Konto, das für den Nutzer mit Google-Diensten wie SQL-Datenbanken interagiert.

3. Hello World

Back-End: Hello World in Go

In Ihrer Cloud Shell-Instanz erstellen Sie zuerst eine Go App Engine Flex-Anwendung, die als Grundlage für den Rest des Codelabs dient.

Klicken Sie in der Symbolleiste von Cloud Shell auf die Schaltfläche Editor öffnen, um einen Code-Editor in einem neuen Tab zu öffnen. Mit diesem webbasierten Codeeditor können Sie Dateien ganz einfach in der Cloud Shell-Instanz bearbeiten.

b63f7baad67b6601

Klicken Sie dann auf das Symbol In neuem Fenster öffnen, um den Editor und das Terminal auf einen neuen Tab zu verschieben.

3f6625ff8461c551

Erstellen Sie im Terminal unten auf dem neuen Tab ein neues austin-recycling-Verzeichnis.

mkdir -p austin-recycling && cd $_

Als Nächstes erstellen Sie eine kleine Go App Engine-Anwendung, damit alles funktioniert. Hello World!

Das Verzeichnis austin-recycling sollte auch links in der Ordnerliste des Editors angezeigt werden. Erstellen Sie im Verzeichnis austin-recycling eine Datei namens app.yaml. Füge in die Datei app.yaml Folgendes ein:

app.yaml (in englischer Sprache)

runtime: go
env: flex

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

Diese Konfigurationsdatei konfiguriert Ihre App Engine-Anwendung für die Verwendung der Go Flex-Laufzeit. Hintergrundinformationen zur Konfiguration der einzelnen Elemente der Datei finden Sie in der Dokumentation zur Google App Engine-Standardumgebung.

Erstellen Sie als Nächstes neben der Datei app.yaml eine main.go-Datei:

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

Wir empfehlen Ihnen, an dieser Stelle einen kurzen Moment aufzuspüren, um die Funktionsweise dieses Codes zu verstehen. Sie haben ein Paket main definiert, das einen HTTP-Server startet, der Port 8080 überwacht und eine Handler-Funktion für HTTP-Anfragen registriert, die mit dem Pfad „"/"“ übereinstimmen.

Die Handler-Funktion, optional handler genannt, schreibt den Textstring "Hello, world!" aus. Dieser Text wird an Ihren Browser weitergeleitet, wo Sie ihn lesen können. Zukünftig werden Handler erstellt, die mit GeoJSON-Daten anstatt mit einfachen hartcodierten Strings antworten.

Nachdem Sie die folgenden Schritte ausgeführt haben, sollten Sie einen Editor haben, der so aussieht:

2084fdd5ef594ece

Testen

Sie können den App Engine-Entwicklungsserver in der Cloud Shell-Instanz ausführen, um diese Anwendung zu testen. Kehren Sie zur Cloud Shell-Befehlszeile zurück und geben Sie Folgendes ein:

go run *.go

Sie sehen einige Zeilen der Logausgabe, die belegen, dass Sie den Entwicklungsserver der Cloud Shell-Instanz tatsächlich ausführen, wobei die Hello World-Webanwendung den Localhost-Port 8080 überwacht. Sie können in dieser App einen Webbrowser-Tab öffnen. Klicken Sie dazu auf die Schaltfläche Webvorschau und wählen Sie in der Cloud Shell-Symbolleiste den Menüpunkt Vorschau auf Port 8080 aus.

4155fc1dc717ac67.png

Wenn Sie auf diesen Menüpunkt klicken, wird ein neuer Tab mit den Wörtern „Hello, world!“ in Ihrem Webbrowser geöffnet, der vom App Engine-Entwicklungsserver bereitgestellt wird.

Im nächsten Schritt fügen Sie dieser App die Daten des Recyclingunternehmens City of Austin hinzu und visualisieren sie.

4. Aktuelle Daten abrufen

GeoJSON, die Lingua-Franca aus der GIS-Welt

Im vorherigen Schritt wurde erwähnt, dass Sie Handler in Ihrem Go-Code erstellen, die GeoJSON-Daten für den Webbrowser rendern. Aber was ist GeoJSON?

In der GIS-Welt (Geo Information System) müssen wir in der Lage sein, Informationen zu geografischen Entitäten zwischen Computersystemen zu vermitteln. Karten sind für Menschen sehr gut zu lesen, doch ihre Computer bevorzugen ihre Daten eher in übersichtlicheren Formaten.

GeoJSON ist ein Format zum Codieren geografischer Datenstrukturen, z. B. die Koordinaten der Recyclingstellen in Austin, Texas,. GeoJSON ist in einem Internet Engineering Task Force-Standard namens RFC7946 standardisiert. GeoJSON wird als JSON, JavaScript Object Notation definiert, das selbst in ECMA-404 von derselben Organisation, die JavaScript standardisiert hat, Ecma International, standardisiert wurde.

Das Wichtigste ist, dass GeoJSON ein weitverbreitetes Wiresformat für die geografische Erkenntnis ist. In diesem Codelab wird GeoJSON folgendermaßen verwendet:

  • Mit Go-Paketen können Sie die Austin-Daten in eine interne GIS-spezifische Datenstruktur parsen, mit der Sie die angeforderten Daten filtern.
  • Fordern Sie die angeforderten Daten für die Übertragung zwischen dem Webserver und dem Browser an.
  • Verwenden Sie eine JavaScript-Bibliothek, um die Antwort in Markierungen auf einer Karte zu konvertieren.

Dadurch müssen Sie viel Code eingeben, da Sie keine Parser und Generatoren erstellen müssen, um den Wire-wire-Datenstream in In-Memory-Darstellungen zu konvertieren.

Daten abrufen

Das Open of Portal der Stadt Austin, Texas stellt raumbezogene Informationen zu öffentlichen Ressourcen für die Öffentlichkeit zur Verfügung. In diesem Codelab visualisieren Sie den Recyclingstellen.

Sie visualisieren die Daten mit Markierungen auf der Karte, die mithilfe der Datenschicht der Maps JavaScript API gerendert werden.

Laden Sie zuerst die GeoJSON-Daten von der Website von City of Austin in Ihre App herunter.

  1. Fahren Sie den Server in der Cloud Shell-Instanz herunter, indem Sie [Strg] + [C] drücken.
  2. Erstellen Sie im Verzeichnis austin-recycling ein Verzeichnis data und wechseln Sie zu diesem Verzeichnis:
mkdir -p data && cd data

Verwenden Sie dann „curl“, um die Recyclingstellen abzurufen:

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

Wechseln Sie schließlich zurück in das übergeordnete Verzeichnis.

cd ..

5. Standorte zuordnen

Aktualisiere zuerst die Datei app.yaml, um die stabilere & nicht mehr nur eine Hello World-App darzustellen.

app.yaml (in englischer Sprache)

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

Diese app.yaml-Konfiguration leitet Anfragen für /, /*.js, /*.css und /*.html an eine Reihe statischer Dateien weiter. Das bedeutet, dass die statische HTML-Komponente Ihrer Anwendung direkt von der App Engine-Dateibereitstellungsinfrastruktur bereitgestellt wird und nicht von Ihrer Go-Anwendung. Dadurch wird die Serverlast reduziert und die Bereitstellungsgeschwindigkeit erhöht.

Jetzt ist es an der Zeit, dein Anwendungs-Back-End in Go zu erstellen!

Back-End erstellen

Vielleicht hast du ja schon bemerkt, dass eine app.yaml-Datei, die nicht verfügbar ist, nur mit der GeoJSON-Datei geteilt wird. Das liegt daran, dass das GeoJSON von unserem Go-Back-End verarbeitet und gesendet wird. Dann können wir in einigen späteren Schritten noch spezielle Funktionen einbinden. Ändern Sie die Datei main.go so:

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

Das Go-Back-End ist bereits eine nützliche Funktion: Die App Engine-Instanz speichert alle diese Standorte im Cache, sobald sie gestartet werden. Dadurch wird Zeit gespart, da das Back-End die Datei nicht bei jeder Aktualisierung des Nutzers von der Festplatte lesen muss.

Front-End erstellen

Zuerst müssen wir einen Ordner erstellen, in dem alle statischen Assets abgelegt werden. Erstellen Sie im übergeordneten Ordner des Projekts den Ordner static.

mkdir -p static && cd static

Wir erstellen jetzt 3 Dateien in diesem Ordner.

  • index.html enthält den gesamten HTML-Code Ihrer einseitigen Filialsuche-App.
  • style.css enthält wie erwartet den Stil
  • app.js ist dafür verantwortlich, das GeoJSON abzurufen, Aufrufe an die Maps API zu senden und Markierungen auf deiner benutzerdefinierten Karte zu platzieren.

Erstellen Sie diese drei Dateien und legen Sie sie in static/ ab.

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>

Achten Sie besonders auf die src-URL im Skript-Tag des head-Elements.

  • Ersetzen Sie den Platzhaltertext YOUR_API_KEY durch den API-Schlüssel, den Sie während der Einrichtung generiert haben. Rufen Sie in der Cloud Console die Seite APIs & Dienste; Anmeldedaten auf, um Ihren API-Schlüssel abzurufen oder einen neuen zu generieren.
  • Beachten Sie, dass die URL den Parameter callback=initialize. enthält Were jetzt die JavaScript-Datei erstellen, die diese Callback-Funktion enthält. Hier lädt Ihre Anwendung die Standorte aus dem Back-End, sendet sie an die Google Maps API und verwendet das Ergebnis, um benutzerdefinierte Standorte auf der Karte zu kennzeichnen, die optimal auf Ihre Webseite abgestimmt sind.
  • Der Parameter libraries=places lädt die Places-Bibliothek, die für Funktionen wie die automatische Vervollständigung von Adressen erforderlich ist, die später hinzugefügt wird.

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

Mit diesem Code werden die Standorte auf einer Karte dargestellt. So testen Sie, was wir bisher über die Befehlszeile in das übergeordnete Verzeichnis zurückgegeben haben:

cd ..

Führen Sie nun Ihre Anwendung im Entwicklungsmodus mit folgendem Befehl aus:

go run *.go

Vorschau ansehen. Du solltest eine Karte mit kleinen grünen Kreisen wie diesem sehen.

58a6680e9c8e7396

Du renderst bereits Standorte auf der Karte und hast schon die Hälfte des Codelabs angesehen. Unglaublich. Jetzt etwas interaktiver machen.

6. Details nach Bedarf anzeigen

Auf Klickereignisse auf Kartenmarkierungen reagieren

Ein paar Markierungen auf der Karte zu zeigen, ist ein guter Anfang, aber wir brauchen einen Besucher wirklich, um auf eine dieser Markierungen klicken zu können und Informationen über diesen Ort zu sehen (z. B. den Namen des Unternehmens, die Adresse usw.). Der Name des kleinen Informationsfensters, das normalerweise beim Klicken auf eine Google Maps-Markierung erscheint, ist das Infofenster.

Erstelle ein infoWindow-Objekt. Fügen Sie Folgendes in die Funktion initialize ein, wobei Sie die kommentierte Zeile mit der Auszeichnung // TODO: Initialize an info window" ersetzen:

app.js – initialisieren

  // 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();

Ersetze die fetchAndRenderStores-Funktionsdefinition durch diese etwas andere Version, die die finale Zeile so ändert, dass storeToCircle mit dem zusätzlichen Argument infowindow aufgerufen wird:

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

Ersetze die storeToCircle-Definition durch diese etwas längere Version, für die jetzt ein Infofenster als drittes Argument verwendet wird:

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

Der neue Code oben zeigt ein infoWindow mit den ausgewählten Informationen an, wenn auf eine Händlermarkierung auf der Karte geklickt wird.

Wenn Ihr Server noch läuft, beenden Sie ihn und starten Sie ihn neu. Aktualisieren Sie Ihre Kartenseite und klicken Sie auf eine Kartenmarkierung. Ein kleines Fenster mit dem Namen und der Adresse des Unternehmens sollte in etwa so aussehen:

1af0ab72ad0eadc5.png

7. Ausgangsposition des Nutzers abrufen

Nutzer der Filialsuche möchten in der Regel wissen, welches Geschäft in der Nähe ist oder an welcher Adresse sie beginnen möchten. Fügen Sie eine Place Autocomplete-Suchleiste hinzu, damit der Nutzer ganz einfach eine Startadresse eingeben kann. Die Place Autocomplete-Funktion funktioniert ähnlich wie die in anderen Google-Suchleisten ähnlich wie die automatische Vervollständigung, außer dass die Vervollständigungen alle Orte in der Google Maps Platform sind.

Nutzer-Eingabefeld erstellen

Gehen Sie zurück zu „style.css“, um Stile für die Suchleiste mit automatischer Vervollständigung und die zugehörige Seitenleiste hinzuzufügen. Während wir die CSS-Stile aktualisieren, fügen wir Stile für eine zukünftige Seitenleiste hinzu, die die Informationen zum Geschäft als Liste neben der Karte anzeigt.

Fügen Sie diesen Code am Ende der Datei ein.

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

Sowohl die automatische Vervollständigung als auch der Schiebebereich sind anfangs ausgeblendet, bis sie benötigt werden.

Bereiten Sie ein div-Element für das Autocomplete-Widget vor. Ersetzen Sie dazu den Kommentar in der Datei „index.html“ mit dem Code "<!-- Autocomplete div goes here --> durch den folgenden Code. Während dieser Bearbeitung fügen wir auch das div-Element für den Bereich mit der Folie hinzu.

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>

Definieren Sie jetzt eine Funktion, um der Karte das Autocomplete-Widget hinzuzufügen, indem Sie den folgenden Code am Ende von app.js hinzufügen.

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

Durch den Code werden die Vorschläge automatisch vervollständigt, sodass nur Adressen zurückgegeben werden, weil Place Autocomplete auch mit Namen von Einrichtungen und Verwaltungsstandorten übereinstimmen kann. Außerdem werden die Adressen auf diejenigen in den USA beschränkt. Wenn Sie diese optionalen Spezifikationen hinzufügen, reduziert sich die Anzahl der Zeichen, die der Nutzer eingeben muss, um die Vervollständigungen einzugrenzen, sodass die gewünschte Adresse angezeigt wird.

Anschließend wird die von Google erstellte automatische Vervollständigung div oben rechts auf der Karte verschoben und angegeben, welche Felder zu jedem Ort in der Antwort zurückgegeben werden sollen.

Rufen Sie zuletzt die Funktion initAutocompleteWidget am Ende der initialize-Funktion auf. Ersetzen Sie dabei den Kommentar, der "// TODO: Initialize the Autocomplete widget" lautet.

app.js - initialisieren

 // Initialize the Places Autocomplete Widget
 initAutocompleteWidget();

Starten Sie Ihren Server mit dem folgenden Befehl neu und aktualisieren Sie dann die Vorschau.

go run *.go

Daraufhin sollte jetzt rechts oben auf der Karte ein Autocomplete-Widget angezeigt werden. US-Adressen, die Ihrer Eingabe entsprechen, sind auf den sichtbaren Bereich der Karte ausgerichtet.

58e9bbbcc4bf18d1

Karte aktualisieren, wenn ein Nutzer eine Startadresse auswählt

Jetzt müssen Sie verarbeiten, wenn der Nutzer eine Vorhersage aus dem Autocomplete-Widget auswählt und diesen Standort als Grundlage für die Berechnung der Entfernungen zu Ihren Geschäften verwendet.

Fügen Sie den folgenden Code am Ende von initAutocompleteWidget in app.js hinzu und ersetzen Sie dabei den Kommentar // 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
  });

Durch den Code wird ein Listener hinzugefügt. Wenn der Nutzer dann auf einen der Vorschläge klickt, wird die Karte auf die ausgewählte Adresse aktualisiert und der Ausgangspunkt als Grundlage für die Entfernungsberechnungen verwendet. Die Entfernungsberechnungen implementieren Sie in einem zukünftigen Schritt.

Beenden Sie den Server und starten Sie ihn neu, wenn die Adresse in die Suchleiste der automatischen Vervollständigung eingegeben wurde.

8. Mit Cloud SQL skalieren

Bisher haben wir eine ziemlich gute Filialsuche. Sie nutzt die Tatsache dort, dass die App nur an ungefähr 100 verschiedenen Stellen der App verwendet wird, indem sie einfach nicht mehr aus der Datei gelesen werden muss, sondern in den Back-End geladen wird. Aber was ist, wenn Ihre Standortsuche auf einer anderen Skala funktioniert? Wenn Sie Hunderte von Orten in einem großen Gebiet (oder Tausenden von Orten auf der ganzen Welt) haben, ist es nicht mehr toll, diese Orte im Arbeitsspeicher zu speichern. Wenn Sie die einzelnen Zonen in einzelne Dateien aufteilen, führt dies zu Problemen.

Es ist Zeit, Ihre Standorte aus einer Datenbank zu laden. In diesem Schritt migrieren wir alle Speicherorte in Ihrer GeoJSON-Datei in eine Cloud SQL-Datenbank und aktualisieren das Go-Back-End, damit bei jeder Anfrage Ergebnisse aus dieser Datenbank und nicht mehr aus dem lokalen Cache abgerufen werden.

Cloud SQL-Instanz mit PostGres-Datenbank erstellen

Sie können eine Cloud SQL-Instanz über die Google Cloud Console erstellen, aber es ist noch einfacher, das Dienstprogramm gcloud zu verwenden, um eine Instanz über die Befehlszeile zu erstellen. Erstellen Sie in Cloud Shell mit dem folgenden Befehl eine Cloud SQL-Instanz:

gcloud sql instances create locations \
--database-version=POSTGRES_12 \
--tier=db-custom-1-3840 --region=us-central1
  • Das Argument locations ist der Name, den wir dieser Instanz von Cloud SQL zuweisen.
  • Mit dem Flag tier können Sie aus einigen vordefinierten Maschinen auswählen.
  • Der Wert db-custom-1-3840 gibt an, dass die zu erstellende Instanz eine vCPU und ungefähr 3,75 GB Arbeitsspeicher haben sollte.

Die Cloud SQL-Instanz wird erstellt und mit einer PostGresSQL-Datenbank mit dem Standardnutzer postgres initialisiert. Wofür ist dieses Passwort? Gute Frage. Sie haben keins. Sie müssen ein Konto konfigurieren, bevor Sie sich anmelden können.

Legen Sie das Passwort mit dem folgenden Befehl fest:

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

Geben Sie dann Ihr Passwort ein, wenn Sie dazu aufgefordert werden.

PostGIS-Erweiterung aktivieren

PostGIS ist eine Erweiterung für PostGresSQL, mit der Sie standardisierte Arten von raumbezogenen Daten speichern können. Normalerweise müssen wir einen vollständigen Installationsprozess durchlaufen, um PostGIS unserer Datenbank hinzuzufügen. Glücklicherweise ist es eine der von Cloud SQL unterstützten Erweiterungen für PostGresSQL.

Stellen Sie eine Verbindung zur Datenbankinstanz her, indem Sie sich als Nutzer postgres mit dem folgenden Befehl im Cloud Shell-Terminal anmelden.

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

Geben Sie das Passwort ein, das Sie gerade erstellt haben. Fügen Sie nun die PostGIS-Erweiterung bei der Eingabeaufforderung postgres=> hinzu.

CREATE EXTENSION postgis;

Wenn der Vorgang erfolgreich abgeschlossen wurde, sollte in der Ausgabe der Befehl CREATE EXTENSION angezeigt werden.

Beispiel für eine Befehlsausgabe

CREATE EXTENSION

Beenden Sie die Datenbankverbindung, indem Sie den Befehl „quit“ in der postgres=>-Eingabeaufforderung eingeben.

\q

Geografische Daten in Datenbank importieren

Jetzt müssen alle diese Standortdaten aus den GeoJSON-Dateien in unsere neue Datenbank importiert werden.

Glücklicherweise ist dies ein weitverbreitetes Problem. Im Internet finden Sie viele Tools, mit denen sich das automatisieren lässt. Wir verwenden dafür ein Tool namens ogr2ogr, das Konvertierungen zwischen verschiedenen gängigen Formaten für das Speichern von raumbezogenen Daten ausführt. Dazu gehört unter anderem die Konvertierung von GeoJSON in eine SQL-Dump-Datei. Die SQL-Dump-Datei kann dann zum Erstellen Ihrer Tabellen und Spalten für die Datenbank verwendet und mit allen Daten geladen werden, die in Ihren GeoJSON-Dateien vorhanden waren.

SQL-Dumpdatei erstellen

Installieren Sie zuerst ogr2ogr.

sudo apt-get install gdal-bin

Erstellen Sie als Nächstes mit ogr2ogr die SQL-Dump-Datei. Durch diese Datei wird eine Tabelle namens austinrecycling erstellt.

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

Der obige Befehl basiert auf dem Befehl austin-recycling. Wenn Sie es aus einem anderen Verzeichnis ausführen möchten, ersetzen Sie data durch den Pfad zum Verzeichnis, in dem recycling-locations.geojson gespeichert ist.

Datenbank mit Recyclingstellen auffüllen

Nach Abschluss des letzten Befehls befinden sich jetzt die Dateien datadump.sql, im selben Verzeichnis, in dem Sie den Befehl ausgeführt haben. Wenn Sie die Tabelle öffnen, sehen Sie etwas mehr als 100 SQL-Zeilen, in denen eine Tabelle „austinrecycling“ erstellt und mit Standorten gefüllt wird.

Stellen Sie jetzt eine Verbindung zur Datenbank her und führen Sie das Skript mit dem folgenden Befehl aus.

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

Wenn das Skript erfolgreich ausgeführt wird, sehen die folgenden Ausgabezeilen so aus:

Beispielbefehlsausgabe

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

Go-Back-End aktualisieren, um Cloud SQL zu verwenden

Da wir nun alle Daten in unserer Datenbank haben, ist es an der Zeit, unseren Code zu aktualisieren.

Front-End aktualisieren, um Standortinformationen zu senden

Beginnen wir mit einem kleinen Update für das Front-End: Da diese App jetzt für eine Skala geschrieben wird, in der es jeden einzelnen Standort nicht jedes Mal an das Front-End liefern soll, müssen wir einige Front-End-Informationen zum Standort übermitteln, der für den Nutzer wichtig ist.

Öffnen Sie app.js und ersetzen Sie die Funktionsdefinition fetchStores durch diese Version, um den Breiten- und Längengrad in die URL aufzunehmen.

app.js – Abrufen von Geschäften

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

Nachdem Sie diesen Schritt des Codelabs abgeschlossen haben, gibt die Antwort nur die Geschäfte zurück, die den Kartenkoordinaten im Parameter center am nächsten sind. Für den ersten Abruf in der initialize-Funktion werden im Beispielcode in diesem Lab die zentralen Koordinaten für Austin, Texas, verwendet.

Da fetchStores jetzt nur noch einen Teil der Filialen zurückgibt, müssen die Filialen immer noch einmal abgerufen werden, wenn der Nutzer seinen Ausgangspunkt ändert.

Aktualisiere die initAutocompleteWidget-Funktion, um die Standorte zu aktualisieren, wenn ein neuer Ursprung festgelegt wird. Dafür sind zwei Änderungen erforderlich:

  1. Suchen Sie in „itAutocompleteAutocompleteWidget“ den Callback für den „place_changed“-Listener. Entfernen Sie die Kommentare aus der Zeile, aus der bestehende Kreise entfernt werden. So wird sie jetzt jedes Mal ausgeführt, wenn der Nutzer eine Adresse aus der Place Autocomplete-Suche auswählt.

app.js – initAutocompleteWidget

  autocomplete.addListener("place_changed", async () => {
    circles.forEach((c) => c.setMap(null)); // clear existing stores
    // ...
  1. Wenn der ausgewählte Ursprung geändert wird, wird die Variable „originLocation“ aktualisiert. Beenden Sie am Ende des Callbacks „place_changed“ die Zeile über der Zeile „// TODO: Calculate the closest stores“ und kommentieren Sie sie, um diesen neuen Ursprung an einen neuen Aufruf der Funktion fetchAndRenderStores zu übergeben.

app.js – initAutocompleteWidget

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

Back-End aktualisieren, um Cloud SQL anstelle einer flachen JSON-Datei zu verwenden

GeoJSON-Lese- und Caching von Flatfiles entfernen

Ändern Sie zuerst main.go, um den Code zu entfernen, der die flache GeoJSON-Datei lädt und im Cache speichert. Außerdem können wir die Funktion dropoffsHandler loswerden, da wir eine, von Cloud SQL bereitgestellte Suchmaschine in einer anderen Datei speichern.

Deine neue main.go wird wesentlich kürzer sein.

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

Neuen Handler für Standortanfragen erstellen

Jetzt erstellen wir eine weitere Datei, locations.go, im Verzeichnis austin-recycling. Implementieren Sie zuerst den Handler für Standortanfragen neu.

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

Der Handler führt die folgenden wichtigen Aufgaben aus:

  • werden die Längen- und Breitengrade aus dem Anfrageobjekt abgerufen. (denken Sie daran, wie wir sie der URL hinzugefügt haben? )
  • Er wird durch den getGeoJsonFromDatabase-Aufruf ausgelöst, der einen GeoJSON-String zurückgibt. Dieser wird später geschrieben.
  • Verwendet den ResponseWriter, um diesen GeoJSON-String in der Antwort zu drucken.

Als Nächstes erstellen wir einen Verbindungspool, um die Datenbanknutzung im Hinblick auf gleichzeitige Nutzer gut zu skalieren.

Verbindungspool erstellen

Ein Verbindungspool ist eine Sammlung aktiver Datenbankverbindungen, die der Server für Nutzeranfragen verwenden kann. Da die Anzahl der aktiven Nutzer zunimmt, ist der Verwaltungsaufwand sehr hoch, da der Server Zeit für das Erstellen und Löschen von Verbindungen für jeden aktiven Nutzer aufwendet. Vielleicht haben Sie schon bemerkt, dass wir im vorherigen Abschnitt die Bibliothek github.com/jackc/pgx/stdlib. importiert haben. Das ist eine beliebte Bibliothek für die Verwendung mit Verbindungspools in Go.

Erstellen Sie am Ende von locations.go eine Funktion initConnectionPool (von main.go aufgerufen), die einen Verbindungspool initialisiert. Zur Klarstellung: In diesem Snippet werden einige Hilfsmethoden verwendet. configureConnectionPool bietet eine praktische Möglichkeit, um Pooleinstellungen wie die Anzahl der Verbindungen und die Lebensdauer pro Verbindung anzupassen. mustGetEnv umfasst Aufrufe zum Abrufen der erforderlichen Umgebungsvariablen, sodass nützliche Fehlermeldungen ausgegeben werden können, wenn auf der Instanz wichtige Informationen fehlen (z. B. die IP-Adresse oder der Name der Datenbank, zu der eine Verbindung hergestellt werden soll).

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
}

Fragen Sie die Datenbank nach Standorten ab und rufen Sie JSON im Gegenzug ab.

Jetzt schreiben wir eine Datenbankabfrage, mit der Kartenkoordinaten zurückgegeben und die nächstgelegenen 25 Standorte zurückgegeben werden. Das führt nicht nur dazu, sondern dank seiner fortschrittlichen, modernen Datenbankfunktionen gibt es auch diese Daten als GeoJSON zurück. So weit kommt der Front-End-Code zurück, kann sich nichts geändert haben. Bevor eine Anfrage an eine URL ausgelöst wurde und eine Menge GeoJSON abgerufen wurde Jetzt wird eine Anfrage an eine URL ausgelöst und eine Menge GeoJSON zurückgegeben.

Hier ist diese Funktion für dich. Fügen Sie die folgende Funktion nach dem Handler und dem Verbindungspool-Code hinzu, den Sie gerade am Ende von locations.go geschrieben haben.

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
}

Sie wird hauptsächlich nur für die Einrichtung, das Debugging und die Fehlerbehandlung bei der Auslösung einer Anfrage an die Datenbank verwendet. Hier sehen wir uns den SQL-Dialekt an, der auf der Datenbankebene viele interessante Dinge bewirkt. Sie müssen sich also keine Gedanken über die Implementierung von Code machen.

Die unbearbeitete Abfrage, die ausgelöst wird, sobald der String geparst wurde und alle String-Literale an den richtigen Stellen eingefügt wurden, sieht so aus:

geparst.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

Diese Abfrage kann als eine Hauptabfrage und einige JSON-Wrapping-Funktionen betrachtet werden.

Mit SELECT * ... LIMIT 25 werden alle Felder für jeden Standort ausgewählt. Anschließend wird mit der Funktion ST_DISTANCE (Teil der PostGIS-Suite – Messfunktionen für Standorte) die Entfernung zwischen jedem Standort in der Datenbank und dem Breiten- und Längengrad des Nutzers ermittelt, den der Nutzer im Front-End angegeben hat. Im Gegensatz zur Distance Matrix API, mit der du die Strecke vom Auto entfernen kannst, handelt es sich dabei um GeoSpatial-Entfernungen. Zur Steigerung der Effizienz wird diese Entfernung verwendet und die 25 nächsten Standorte werden an den angegebenen Ort zurückgegeben.

**SELECT json_build_object(‘type', ‘F**eature') schließt die vorherige Abfrage um, wobei die Ergebnisse verwendet werden, um ein GeoJSON-Merkmalsobjekt zu erstellen. Unerwartete Anfrage wird auch bei dieser Abfrage der maximale Radius angewendet. Wenn Sie sich fragen, warum diese WHERE-Klausel nicht zur inneren Abfrage hinzugefügt wurde, bei der die Entfernung der einzelnen Standorte ermittelt wird, liegt dies daran, dass dieses Feld bei der Prüfung der WHERE-Klausel nicht berechnet wurde. Wenn Sie versuchen, diese WHERE-Klausel zur inneren Abfrage zu verschieben, wird ein Fehler ausgegeben.

**SELECT json_build_object(‘type', ‘FeatureColl**ection') Diese Abfrage umschließt alle resultierenden Zeilen aus der JSON-Abfrage in einem GeoJSON FeatureCollection-Objekt.

PGX-Bibliothek zu Projekt hinzufügen

Dem Projekt muss eine Abhängigkeit hinzugefügt werden: das PostGres-Treiber &Toolkit, mit dem das Verbindungs-Pooling ermöglicht wird. Am einfachsten geht das mit Go-Modulen. Initialisieren Sie mit dem folgenden Befehl in der Cloud Shell ein Modul:

go mod init my_locator

Führen Sie als Nächstes diesen Befehl aus, um den Code nach Abhängigkeiten zu scannen, eine Liste der Abhängigkeiten der Mod-Datei hinzuzufügen und sie herunterzuladen.

go mod tidy

Führen Sie schließlich diesen Befehl aus, um Abhängigkeiten direkt in Ihr Projektverzeichnis abzurufen, damit der Container für AppEngine Flex problemlos erstellt werden kann.

go mod vendor

Du kannst die Funktion jetzt testen.

Testen

Ok, wir haben gerade viel gemacht. Jetzt ansehen.

Damit sich Ihr Entwicklungscomputer (auch in Cloud Shell) mit der Datenbank verbinden kann, müssen Sie den Cloud SQL-Proxy verwenden, um die Datenbankverbindung zu verwalten. So richten Sie den Cloud SQL-Proxy ein:

  1. Klicken Sie hier, um die Cloud SQL Admin API zu aktivieren
  2. Wenn Sie sich auf einem lokalen Entwicklungsgerät befinden, installieren Sie das Cloud SQL-Proxy-Tool. Wenn Sie Cloud Shell verwenden, können Sie diesen Schritt überspringen. Dann ist er bereits installiert. Hinweis: Die Anleitung bezieht sich auf ein Dienstkonto. Im nächsten Schritt wurde bereits ein Konto für Sie erstellt. Im nächsten Abschnitt fügen Sie diesem Konto die erforderlichen Berechtigungen hinzu.
  3. Erstellen Sie einen neuen Tab in Cloud Shell oder in Ihrem eigenen Terminal, um den Proxy zu starten.

bcca42933bfbd497.png

  1. Öffnen Sie https://console.cloud.google.com/sql/instances/locations/overview und scrollen Sie nach unten bis zum Feld Connection name (Verbindungsname). Kopieren Sie diesen Namen, um ihn im nächsten Befehl zu verwenden.
  2. Führen Sie auf diesem Tab den Cloud SQL-Proxy mit diesem Befehl aus und ersetzen Sie dabei CONNECTION_NAME durch den Namen der Verbindung, der im vorherigen Schritt angezeigt wurde.
cloud_sql_proxy -instances=CONNECTION_NAME=tcp:5432

Kehren Sie zum ersten Tab Ihrer Cloud Shell zurück und definieren Sie die Umgebungsvariablen, die Go benötigt, um mit dem Datenbank-Back-End zu kommunizieren, und führen Sie dann den Server wie zuvor aus:

Gehen Sie zum Stammverzeichnis des Projekts, wenn Sie es noch nicht vorhanden sind.

cd YOUR_PROJECT_ROOT

Erstellen Sie die folgenden fünf Umgebungsvariablen. Ersetzen Sie dabei YOUR_PASSWORD_HERE durch das oben erstellte Passwort.

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

Führen Sie Ihre lokale Instanz aus.

go run *.go

Öffnen Sie das Vorschaufenster. Es sollte funktionieren, als ob sich nichts geändert hätte: Sie können eine Startadresse eingeben, um die Karte herum zoomen und auf Recyclingstellen klicken. Aber jetzt gilt die Datenbank als Datenbank, und ist bereit für die Skalierung!

9. Geschäfte in der Nähe auflisten

Die Directions API funktioniert ähnlich wie das Anfordern von Wegbeschreibungen in der Google Maps App. Sie geben einen Abflugort und ein Ziel ein, um eine Route zwischen diesen Routen zu erhalten. Die Distance Matrix API nutzt dieses Konzept weiter, um die optimalen Kombinationen von mehreren möglichen Abflugorten und mehreren möglichen Zielen auf der Grundlage von Reisezeiten und Entfernungen zu erkennen. In diesem Fall helfen Sie dem Nutzer dabei, das nächstgelegene Geschäft für die ausgewählte Adresse zu finden. Geben Sie dazu einen Ursprung und ein Array von Geschäftsstandorten als Ziel an.

Füge die Entfernung zu jedem Geschäft hinzu.

Ersetzen Sie am Anfang der Funktionsdefinition initMap den Kommentar „// TODO: Start Distance Matrix service“ durch den folgenden Code:

app.js – initMap

distanceMatrixService = new google.maps.DistanceMatrixService();

Füge am Ende von app.js eine neue Funktion mit der Bezeichnung calculateDistances hinzu.

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

Durch die Funktion wird die Distance Matrix API mit dem ursprünglichen Ursprung als einzelne Quelle und den Geschäftsstandorten als Array von Zielen aufgerufen. Dann wird ein Array mit Objekten erstellt, in dem die ID des Geschäfts gespeichert wird, die Entfernung in einem menschenlesbaren String, die Entfernung in Metern als numerischer Wert und das Array sortiert.

Aktualisiere die initAutocompleteWidget-Funktion, um die Entfernungen zu berechnen, sobald eine neue Quelle über die Place Autocomplete-Suchleiste ausgewählt wurde. Ersetzen Sie unten in der initAutocompleteWidget-Funktion den Kommentar // TODO: Calculate the closest stores mit folgendem Code:

app.js - initAutocompleteWidget

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

Listenansicht der nach Entfernung sortierten Geschäfte anzeigen

Der Nutzer erwartet eine Liste der Geschäfte, die von der nächsten bis zur längsten Bestellung bestellt wurden. Füllen Sie eine Seitenleiste für jeden Shop mit der Liste aus, die von der Funktion calculateDistances geändert wurde, um die Anzeigereihenfolge der Geschäfte festzulegen.

Füge am Ende von app.js zwei neue Funktionen namens renderStoresPanel() und storeToPanelRow() hinzu.

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

Starten Sie den Server neu und aktualisieren Sie die Vorschau, indem Sie den folgenden Befehl ausführen.

go run *.go

Geben Sie schließlich eine Austin-TX-Adresse in die Suchleiste für die automatische Vervollständigung ein und klicken Sie auf einen der Vorschläge.

Die Karte sollte auf diese Adresse zentriert sein und in einer Seitenleiste sollten die Filialen in der Reihenfolge ihrer Entfernung zur ausgewählten Adresse aufgeführt sein. Hier ein Beispiel:

96e35794dd0e88c9.png

10. Karte anpassen

Eine wirkungsvolle Möglichkeit, Ihre Karte visuell abzuheben, besteht darin, ihnen Stile hinzuzufügen. Beim cloudbasierten Kartenstil wird die Anpassung Ihrer Karten über die Cloud Console über den cloudbasierten Kartenstil (Beta) gesteuert. Wenn Sie einen Kartenstil mit einer nicht Beta-Funktion erstellen möchten, können Sie die Dokumentation für Kartenstile verwenden, um JSON-Code für den programmatischen Stil der Karte zu generieren. In der Anleitung unten wird die cloudbasierte Kartenstile (Beta) beschrieben.

Karten-ID erstellen

Öffnen Sie zuerst die Cloud Console und das Suchfeld und geben Sie „Kartenverwaltung“ ein. Klicke auf das Ergebnis, das dir angezeigt wird: "Map Management (Google Maps)". 64036tt0ed200200.png

Oben rechts unter dem Suchfeld wird die Schaltfläche Neue Karten-ID erstellen angezeigt. Klicken Sie darauf und geben Sie einen beliebigen Namen ein. Wählen Sie für den Kartentyp JavaScript aus und klicken Sie in der Liste auf Vector, wenn weitere Optionen angezeigt werden. Das Endergebnis sollte in etwa so aussehen:

70f55a759b4c4212

Klicken Sie auf „Weiter“ und Sie erhalten eine brandneue Karten-ID. Du kannst es jetzt kopieren, aber du kannst es später einfach nachschlagen.

Als Nächstes erstellen wir einen Stil, der auf diese Karte angewendet werden soll.

Kartenstil erstellen

Wenn Sie sich noch im Bereich „Maps“ der Cloud Console befinden, klicken Sie unten im Navigationsmenü unten auf „Kartenstile“. Genau wie beim Erstellen einer Karten-ID können Sie auch hier die richtige Seite aufrufen, indem Sie in das Suchfeld „& Kartenstile“ eingeben und aus den Ergebnissen Kartenstile (Google Maps) auswählen (siehe folgende Abbildung).

9284cd200f1a9223

Klicken Sie dann oben auf die Schaltfläche + Neuen Kartenstil erstellen.

  1. Wenn Sie mit dem Stil auf der Karte in diesem Lab übereinstimmen möchten, klicken Sie auf den Tab JSON importieren und fügen Sie das JSON-Blob unten ein. Wenn Sie einen eigenen erstellen möchten, wählen Sie den gewünschten Kartenstil aus. Wählen Sie dann Weiter aus.
  2. Wählen Sie die soeben erstellte Karten-ID aus, um sie mit diesem Stil zu verknüpfen, und klicken Sie dann noch einmal auf Weiter.
  3. An dieser Stelle haben Sie die Möglichkeit, den Stil Ihrer Karte weiter anzupassen. Wenn Sie diesen Stil ausprobieren möchten, klicken Sie auf Im Stileditor anpassen und spielen Sie mit den Farben und Optionen, bis Sie einen Kartenstil haben, der Ihnen gefällt. Andernfalls klicken Sie auf Überspringen.
  4. Geben Sie im nächsten Schritt den Stil und die Beschreibung für den Stil ein und klicken Sie dann auf Speichern und veröffentlichen.

Hier ist ein optionales JSON-Blob, das im ersten Schritt importiert werden kann.

[
  {
    "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"
      }
    ]
  }
]

Karten-ID im Code angeben

Sie wissen jetzt, wie Sie einen Kartenstil erstellen und wie Sie ihn in Ihrer eigenen Karte verwenden. Dazu sind zwei kleine Änderungen erforderlich:

  1. Karten-ID als URL-Parameter zum Skript-Tag in index.html hinzufügen
  2. Add die Karten-ID als Konstruktor-Argument, wenn du die Karte in deiner initMap()-Methode erstellst.

Ersetzen Sie das Skript-Tag, das die Maps JavaScript API in der HTML-Datei durch die Lade-URL unten lädt, und ersetzen Sie die Platzhalter für "YOUR_API_KEY" und "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>
...

Entfernen Sie in der initMap-Methode von app.js, bei der die Konstante map definiert ist, den Kommentar aus der Zeile für die mapId-Eigenschaft und ersetzen Sie &YOUR_MAP_ID_HERE durch die Karten-ID, die Sie gerade erstellt haben:

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',
// ...
});
...

Starten Sie Ihren Server neu.

go run *.go

Nach der Aktualisierung der Vorschau sollte die Karte Ihren Vorstellungen entsprechen. Hier ein Beispiel mit dem JSON-Stil oben.

2ece59c64c06e9da.png

11. In Produktion bereitstellen

Wenn Sie möchten, dass Ihre App von AppEngine Flex (und nicht nur von einem lokalen Webserver auf Ihrem Entwicklungscomputer / Cloud Shell) ausgeführt wird, ist das sehr einfach. Wir müssen nur einige Dinge hinzufügen, damit der Datenbankzugriff in der Produktionsumgebung funktioniert. Dies wird auf der Dokumentationsseite zur Verbindung von App Engine Flex mit Cloud SQL näher erläutert.

Umgebungsvariablen zu App.yaml hinzufügen

All diese Umgebungsvariablen, die du bisher zum Testen vor Ort verwendet hast, müssen unten in die app.yaml-Datei deiner Anwendung aufgenommen werden.

  1. Rufen Sie https://console.cloud.google.com/sql/instances/locations/overview auf, um den Namen der Instanzverbindung zu ermitteln.
  2. Fügen Sie den folgenden Code am Ende von „app.yaml“ ein.
  3. Ersetzen Sie YOUR_DB_PASSWORD_HERE durch das Passwort, das Sie zuvor für den postgres-Nutzernamen erstellt haben.
  4. Ersetzen Sie YOUR_CONNECTION_NAME_HERE durch den Wert aus Schritt 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

Beachten Sie, dass DB_TCP_HOST den Wert 172.17.0.1 haben sollte, da diese App über AppEngine Flex** verbunden wird.** Das liegt daran, dass die Kommunikation über einen Proxy wie mit Cloud SQL erfolgt.

SQL-Clientberechtigungen dem App Engine-Flex-Dienstkonto hinzufügen

Rufen Sie in der Cloud Console die Seite IAM-Verwaltung auf und suchen Sie nach einem Dienstkonto, dessen Name dem Format service-PROJECT_NUMBER@gae-api-prod.google.com.iam.gserviceaccount.com entspricht. Dies ist das Dienstkonto, das App Engine Flex zum Herstellen einer Verbindung zur Datenbank verwendet. Klicken Sie am Ende der Zeile auf Bearbeiten und fügen Sie die Rolle Cloud SQL-Client hinzu.

b04webstore0b4022b905_png

Projektcode in den Go-Pfad kopieren

Damit AppEngine Ihren Code ausführen kann, muss er die relevanten Dateien im Go-Pfad finden können. Sie müssen sich im Projektstammverzeichnis befinden.

cd YOUR_PROJECT_ROOT

Kopieren Sie das Verzeichnis in den go-Pfad.

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

Wechseln Sie zu diesem Verzeichnis.

cd ~/gopath/src/austin-recycling

Anwendung bereitstellen

Verwenden Sie die gcloud-Befehlszeile, um Ihre Anwendung bereitzustellen. Das dauert eine Weile.

gcloud app deploy

Verwenden Sie den Befehl browse, um einen Link zu erhalten, auf den Sie klicken können, um Ihre vollständig bereitgestellte, geschäftsorientierte Filialsuche in Aktion zu sehen.

gcloud app browse

Wenn Sie gcloud außerhalb von Cloud Shell ausgeführt haben, wird ein neuer Browsertab geöffnet, wenn Sie gcloud app browse ausführen.

12. Empfohlen (Bereinigen)

Wenn Sie dieses Codelab durchführen, bleiben Sie im Rahmen der kostenlosen Stufe für die BigQuery-Verarbeitung und die Maps Platform API-Aufrufe innerhalb des Limits. Wenn Sie dies jedoch nur zu Informationszwecken getan haben und zukünftig keine Gebühren mehr dafür zahlen möchten, können Sie die Ressourcen, die mit diesem Projekt verknüpft sind, ganz einfach löschen, indem Sie das Projekt selbst löschen.

Projekt löschen

Rufen Sie in der GCP Console die Seite Cloud Resource Manager auf:

Wählen Sie in der Projektliste das Projekt aus, in dem wir gearbeitet haben, und klicken Sie auf Löschen. Du wirst aufgefordert, die Projekt-ID einzugeben. Geben Sie den Befehl ein und klicken Sie auf Herunterfahren.

Alternativ können Sie das gesamte Projekt direkt aus Cloud Shell mit gcloud löschen. Führen Sie dazu den folgenden Befehl aus und ersetzen Sie den Platzhalter GOOGLE_CLOUD_PROJECT durch Ihre Projekt-ID:

gcloud projects delete GOOGLE_CLOUD_PROJECT

13. Glückwunsch

Glückwunsch! Sie haben das Codelab erfolgreich abgeschlossen.

Oder Sie haben die letzte Seite übersprungen. Glückwunsch! Sie haben auf der letzten Seite überfliegen!

In diesem Codelab haben Sie folgende Technologien eingesetzt:

Weiterführende Literatur

Es gibt noch viel mehr über all diese Technologien zu erfahren. Hier einige nützliche Links zu Themen, die wir in diesem Codelab nicht behandelt haben, aber auf jeden Fall für Sie interessant sein könnten, um eine Filialsuche zu entwickeln, die Ihren speziellen Anforderungen entspricht.