Crea uno store locator full stack con Google Maps Platform e Google Cloud

1. Introduzione

Abstract

Immagina di avere molti luoghi da inserire su una mappa e vuoi che gli utenti possano vedere dove si trovano e identificare il luogo che vogliono visitare. Alcuni esempi comuni includono:

  • uno Store locator sul sito web di un rivenditore
  • una mappa dei seggi elettorali per le prossime elezioni
  • un elenco di luoghi specializzati come i contenitori per il riciclo delle batterie

Cosa creerai

In questo codelab, creerai un localizzatore che attinge a un feed di dati in tempo reale di località specializzate e aiuta l'utente a trovare la località più vicina al punto di partenza. Questo localizzatore full-stack può gestire un numero molto maggiore di luoghi rispetto al localizzatore di negozi semplice, che è limitato a un massimo di 25 sedi di negozi.

2ece59c64c06e9da.png

Obiettivi didattici

Questo codelab utilizza un set di dati aperti per simulare metadati precompilati su un gran numero di sedi di negozi, in modo che tu possa concentrarti sull'apprendimento dei concetti tecnici chiave.

  • API Maps JavaScript: visualizzare un numero elevato di località su una mappa web personalizzata
  • GeoJSON: un formato che archivia i metadati sulle località
  • Place Autocomplete: aiuta gli utenti a fornire posizioni di partenza più velocemente e con maggiore precisione
  • Go: il linguaggio di programmazione utilizzato per sviluppare il backend dell'applicazione. Il backend interagirà con il database e invierà i risultati della query al frontend in formato JSON.
  • App Engine: per l'hosting dell'app web

Prerequisiti

  • Conoscenza di base di HTML e JavaScript
  • Un Account Google

2. Configurazione

Nel passaggio 3 della sezione seguente, abilita API Maps JavaScript, API Places e API Distance Matrix per questo codelab.

Inizia a utilizzare Google Maps Platform

Se non hai mai utilizzato Google Maps Platform, segui la guida Inizia a utilizzare Google Maps Platform o guarda la playlist Inizia a utilizzare Google Maps Platform per completare i seguenti passaggi:

  1. Crea un account di fatturazione.
  2. Creare un progetto.
  3. Abilita le API e gli SDK di Google Maps Platform (elencati nella sezione precedente).
  4. Genera una chiave API.

Attiva Cloud Shell

In questo codelab utilizzi Cloud Shell, un ambiente a riga di comando in esecuzione in Google Cloud che fornisce l'accesso a prodotti e risorse in esecuzione su Google Cloud, in modo da poter ospitare ed eseguire il progetto completamente dal browser web.

Per attivare Cloud Shell dalla console Google Cloud, fai clic su Attiva Cloud Shell 89665d8d348105cd.png (bastano pochi istanti per eseguire il provisioning e connettersi all'ambiente).

5f504766b9b3be17.png

Si apre una nuova shell nella parte inferiore del browser dopo aver mostrato un eventuale interstitial introduttivo.

d3bb67d514893d1f.png

Conferma il progetto

Una volta eseguita la connessione a Cloud Shell, dovresti vedere che il tuo account è già autenticato e il progetto è già impostato sull'ID progetto selezionato durante la configurazione.

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

Se per qualche motivo il progetto non è impostato, esegui il seguente comando:

gcloud config set project <YOUR_PROJECT_ID>

Abilita l'API App Engine Flex

L'API App Engine Flex deve essere abilitata manualmente dalla console Cloud. In questo modo, non solo verrà attivata l'API, ma verrà creato anche l'account di servizio dell'ambiente flessibile App Engine, l'account autenticato che interagirà con i servizi Google (come i database SQL) per conto dell'utente.

3. Hello, World

Backend: Hello World in Go

Nella tua istanza Cloud Shell, inizierai creando un'app Go App Engine Flex che fungerà da base per il resto del codelab.

Nella barra degli strumenti di Cloud Shell, fai clic sul pulsante Apri editor per aprire un editor di codice in una nuova scheda. Questo editor di codice basato sul web ti consente di modificare facilmente i file nell'istanza di Cloud Shell.

b63f7baad67b6601.png

Poi, fai clic sull'icona Apri in una nuova finestra per spostare l'editor e il terminale in una nuova scheda.

3f6625ff8461c551.png

Nel terminale in fondo alla nuova scheda, crea una nuova directory austin-recycling.

mkdir -p austin-recycling && cd $_

Successivamente, creerai una piccola app Go App Engine per verificare che tutto funzioni. Hello World!

Anche la directory austin-recycling dovrebbe essere visualizzata nell'elenco delle cartelle dell'editor a sinistra. Nella directory austin-recycling, crea un file denominato app.yaml. Inserisci i seguenti contenuti nel file app.yaml:

app.yaml

runtime: go
env: flex

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

Questo file di configurazione configura l'app App Engine per utilizzare il runtime Go Flex. Per informazioni di base sul significato degli elementi di configurazione in questo file, consulta la documentazione dell'ambiente standard di Google App Engine Go.

Successivamente, crea un file main.go insieme al file 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!")
}

Vale la pena fermarsi un attimo per capire cosa fa questo codice, almeno a livello generale. Hai definito un pacchetto main che avvia un server HTTP in ascolto sulla porta 8080 e registra una funzione di gestione per le richieste HTTP corrispondenti al percorso "/".

La funzione del gestore, chiamata opportunamente handler, scrive la stringa di testo "Hello, world!". Questo testo verrà ritrasmesso al browser, dove potrai leggerlo. Nei passaggi successivi creerai gestori che rispondono con dati GeoJSON anziché semplici stringhe hardcoded.

Dopo aver eseguito questi passaggi, dovresti avere un editor simile al seguente:

2084fdd5ef594ece.png

Prova

Per testare questa applicazione, puoi eseguire il server di sviluppo App Engine all'interno dell'istanza Cloud Shell. Torna alla riga di comando di Cloud Shell e digita quanto segue:

go run *.go

Vedrai alcune righe di output del log che mostrano che stai effettivamente eseguendo il server di sviluppo sull'istanza Cloud Shell, con l'app web Hello World in ascolto sulla porta 8080 di localhost. Puoi aprire una scheda del browser web in questa app premendo il pulsante Anteprima web e selezionando la voce di menu Anteprima sulla porta 8080 nella barra degli strumenti di Cloud Shell.

4155fc1dc717ac67.png

Se fai clic su questa voce di menu, si aprirà una nuova scheda nel browser web con le parole "Hello, world!" servite dal server di sviluppo App Engine.

Nel passaggio successivo aggiungerai i dati sul riciclaggio della città di Austin a questa app e inizierai a visualizzarli.

4. Recuperare i dati attuali

GeoJSON, la lingua franca del mondo GIS

Il passaggio precedente menzionava la creazione di gestori nel codice Go che eseguono il rendering dei dati GeoJSON nel browser web. Ma che cos'è GeoJSON?

Nel mondo dei sistemi informativi geografici (GIS), dobbiamo essere in grado di comunicare le conoscenze sulle entità geografiche tra i sistemi informatici. Le mappe sono facili da leggere per gli esseri umani, ma i computer in genere preferiscono i dati in formati più facilmente digeribili.

GeoJSON è un formato per la codifica di strutture di dati geografici, come le coordinate dei punti di raccolta per il riciclaggio ad Austin, in Texas. GeoJSON è stato standardizzato in uno standard dell'Internet Engineering Task Force chiamato RFC7946. GeoJSON è definito in termini di JSON, JavaScript Object Notation, che a sua volta è stato standardizzato in ECMA-404 dalla stessa organizzazione che ha standardizzato JavaScript, Ecma International.

La cosa importante è che GeoJSON è un formato di trasferimento ampiamente supportato per comunicare informazioni geografiche. Questo codelab utilizza GeoJSON nei seguenti modi:

  • Utilizza i pacchetti Go per analizzare i dati di Austin in una struttura di dati interna specifica per il GIS che utilizzerai per filtrare i dati richiesti.
  • Serializza i dati richiesti per il transito tra il server web e il browser web.
  • Utilizza una libreria JavaScript per convertire la risposta in indicatori su una mappa.

In questo modo, dovrai scrivere molto meno codice, perché non è necessario scrivere parser e generatori per convertire il flusso di dati on-the-wire in rappresentazioni in memoria.

Recuperare i dati

Il portale open data della città di Austin, Texas rende disponibili per l'uso pubblico le informazioni geospaziali sulle risorse pubbliche. In questo codelab, visualizzerai il set di dati Punti di raccolta per il riciclo.

Visualizzerai i dati con indicatori sulla mappa, visualizzati utilizzando il livello di dati dell'API Maps JavaScript.

Per iniziare, scarica i dati GeoJSON dal sito web della città di Austin nella tua app.

  1. Nella finestra della riga di comando dell'istanza Cloud Shell, arresta il server digitando [CTRL] + [C].
  2. Crea una directory data all'interno della directory austin-recycling e passa a quella directory:
mkdir -p data && cd data

Ora utilizza curl per recuperare le sedi di riciclaggio:

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

Infine, torna alla directory principale.

cd ..

5. Mappare le posizioni

Innanzitutto, aggiorna il file app.yaml in modo che rifletta l'applicazione più solida che stai per creare, che non è più solo un'app "Hello World".

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

Questa configurazione app.yaml indirizza le richieste per /, /*.js, /*.css e /*.html a un insieme di file statici. Ciò significa che il componente HTML statico della tua app verrà pubblicato direttamente dall'infrastruttura di pubblicazione dei file di App Engine e non dalla tua app Go. In questo modo, il carico del server viene ridotto e la velocità di pubblicazione aumenta.

Ora è il momento di creare il backend dell'applicazione in Go.

Crea il backend

Come avrai notato, una cosa interessante che il tuo file app.yaml non fa è esporre il file GeoJSON. Questo perché il GeoJSON verrà elaborato e inviato dal nostro backend Go, il che ci consente di integrare alcune funzionalità avanzate nei passaggi successivi. Modifica il file main.go in modo che sia simile al seguente:

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

Il backend Go ci offre già una funzionalità preziosa: l'istanza App Engine memorizza nella cache tutte queste località non appena viene avviata. In questo modo si risparmia tempo, poiché il backend non dovrà leggere il file dal disco a ogni aggiornamento di ogni utente.

Crea il front-end

La prima cosa da fare è creare una cartella in cui archiviare tutti gli asset statici. Dalla cartella principale del progetto, crea una cartella static.

mkdir -p static && cd static

Creeremo tre file in questa cartella.

  • index.html conterrà tutto il codice HTML per l'app di ricerca dei negozi di una sola pagina.
  • style.css , come previsto, conterrà lo stile
  • app.js sarà responsabile del recupero del GeoJSON, dell'esecuzione di chiamate all'API Maps e del posizionamento dei marcatori sulla mappa personalizzata.

Crea questi tre file, assicurandoti di inserirli in 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>

Presta particolare attenzione all'URL src nel tag script dell'elemento head.

  • Sostituisci il testo segnaposto "YOUR_API_KEY" con la chiave API che hai generato durante il passaggio di configurazione. Puoi visitare la pagina API e servizi -> Credenziali in Cloud Console per recuperare la chiave API o generarne una nuova.
  • Tieni presente che l'URL contiene il parametro callback=initialize.. Ora creeremo il file JavaScript contenente la funzione di callback. Qui la tua app caricherà le posizioni dal backend, le invierà all'API Maps e utilizzerà il risultato per contrassegnare le posizioni personalizzate sulla mappa, il tutto in modo ottimale per la tua pagina web.
  • Il parametro libraries=places carica la libreria Places, necessaria per funzionalità come il completamento automatico dell'indirizzo che verranno aggiunte in un secondo momento.

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

Questo codice esegue il rendering delle sedi dei negozi su una mappa. Per testare ciò che abbiamo finora, dalla riga di comando torna alla directory principale:

cd ..

Ora esegui di nuovo l'app in modalità di sviluppo utilizzando:

go run *.go

Visualizzala in anteprima come in precedenza. Dovresti vedere una mappa con piccoli cerchi verdi come questi.

58a6680e9c8e7396.png

Stai già eseguendo il rendering delle posizioni sulla mappa e siamo solo a metà del codelab. Fantastico. Ora aggiungiamo un po' di interattività.

6. Mostra i dettagli on demand

Rispondere agli eventi di clic sui segnaposto sulla mappa

Visualizzare una serie di indicatori sulla mappa è un ottimo inizio, ma è necessario che un visitatore possa fare clic su uno di questi indicatori e visualizzare informazioni su quella posizione (come il nome dell'attività, l'indirizzo e così via). Il nome della piccola finestra informativa che di solito viene visualizzata quando fai clic su un indicatore di Google Maps è Finestra informativa.

Crea un oggetto infoWindow. Aggiungi quanto segue alla funzione initialize, sostituendo la riga commentata che riporta "// 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();

Sostituisci la definizione della funzione fetchAndRenderStores con questa versione leggermente diversa, che modifica l'ultima riga per chiamare storeToCircle con un argomento aggiuntivo, 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));
};

Sostituisci la definizione di storeToCircle con questa versione leggermente più lunga, che ora accetta una finestra informativa come terzo argomento:

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

Il nuovo codice riportato sopra mostra un infoWindow con le informazioni del negozio selezionato ogni volta che viene fatto clic su un indicatore del negozio sulla mappa.

Se il server è ancora in esecuzione, arrestalo e riavvialo. Aggiorna la pagina della mappa e prova a fare clic su un indicatore. Dovrebbe aprirsi una piccola finestra informativa con il nome e l'indirizzo dell'attività, simile a questa:

1af0ab72ad0eadc5.png

7. Ottieni la posizione di partenza dell'utente

Gli utenti dei localizzatori di negozi in genere vogliono sapere qual è il negozio più vicino a loro o a un indirizzo da cui intendono iniziare il loro viaggio. Aggiungi una barra di ricerca di Place Autocomplete per consentire all'utente di inserire facilmente un indirizzo di partenza. Place Autocomplete fornisce una funzionalità di completamento automatico simile a quella di altre barre di ricerca di Google, tranne per il fatto che le previsioni sono tutte luoghi in Google Maps Platform.

Creare un campo di input dell'utente

Torna a modificare style.css per aggiungere lo stile alla barra di ricerca del completamento automatico e al riquadro laterale dei risultati associato. Durante l'aggiornamento degli stili CSS, aggiungeremo anche gli stili per una futura barra laterale che mostrerà le informazioni del negozio come elenco per accompagnare la mappa.

Aggiungi questo codice alla fine del file.

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

Sia la barra di ricerca del completamento automatico sia il pannello a comparsa sono inizialmente nascosti finché non sono necessari.

Prepara un div per il widget Completamento automatico sostituendo il commento in index.html che recita "<!-- Autocomplete div goes here -->" con il seguente codice. Durante questa modifica, aggiungeremo anche il div per il pannello estraibile.

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>

Ora, definisci una funzione per aggiungere il widget di completamento automatico alla mappa aggiungendo il seguente codice alla fine di 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
};

Il codice limita i suggerimenti di completamento automatico in modo che restituiscano solo indirizzi (perché il completamento automatico dei luoghi può corrispondere anche a nomi di attività e sedi amministrative) e limita gli indirizzi restituiti solo a quelli negli Stati Uniti. L'aggiunta di queste specifiche facoltative ridurrà il numero di caratteri che l'utente deve inserire per restringere le previsioni e mostrare l'indirizzo che sta cercando.

Poi, sposta il completamento automatico div che hai creato nell'angolo in alto a destra della mappa e specifica quali campi devono essere restituiti su ogni luogo nella risposta.

Infine, chiama la funzione initAutocompleteWidget alla fine della funzione initialize, sostituendo il commento "// TODO: Initialize the Autocomplete widget".

app.js - initialize

 // Initialize the Places Autocomplete Widget
 initAutocompleteWidget();

Riavvia il server eseguendo il seguente comando, poi aggiorna l'anteprima.

go run *.go

Ora nell'angolo in alto a destra della mappa dovresti vedere un widget di completamento automatico che mostra gli indirizzi statunitensi corrispondenti a ciò che digiti, con una preferenza per l'area visibile della mappa.

58e9bbbcc4bf18d1.png

Aggiornare la mappa quando l'utente seleziona un indirizzo di partenza

Ora devi gestire il caso in cui l'utente seleziona una previsione dal widget Place Autocomplete e utilizzare quella posizione come base per calcolare le distanze dai tuoi negozi.

Aggiungi il seguente codice alla fine di initAutocompleteWidget in app.js, sostituendo il commento "// 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
  });

Il codice aggiunge un listener in modo che, quando l'utente fa clic su uno dei suggerimenti, la mappa si ricentri sull'indirizzo selezionato e imposti l'origine come base per i calcoli della distanza. Implementerai i calcoli della distanza in un passaggio successivo.

Arresta e riavvia il server e aggiorna l'anteprima per osservare il ricentramento della mappa dopo aver inserito un indirizzo nella barra di ricerca con completamento automatico.

8. Scalare con Cloud SQL

Finora, abbiamo un ottimo Store Locator. Sfrutta il fatto che l'app utilizzerà solo un centinaio di posizioni, caricandole in memoria nel backend (anziché leggerle ripetutamente dal file). Ma cosa succede se il localizzatore deve operare su una scala diversa? Se hai centinaia di sedi sparse in un'ampia area geografica (o migliaia in tutto il mondo), memorizzarle tutte non è più la soluzione migliore e la suddivisione delle zone in singoli file introdurrà i suoi problemi.

È il momento di caricare le posizioni da un database. Per questo passaggio, eseguiremo la migrazione di tutte le località nel file GeoJSON in un database Cloud SQL e aggiorneremo il backend Go in modo che estragga i risultati da questo database anziché dalla cache locale ogni volta che viene ricevuta una richiesta.

Crea un'istanza Cloud SQL con il database PostgreSQL

Puoi creare un'istanza Cloud SQL tramite la console Google Cloud, ma è ancora più facile utilizzare l'utilità gcloud per crearne una dalla riga di comando. In Cloud Shell, crea un'istanza Cloud SQL con il comando seguente:

gcloud sql instances create locations \
--database-version=POSTGRES_12 \
--tier=db-custom-1-3840 --region=us-central1
  • L'argomento locations è il nome che scegliamo di dare a questa istanza di Cloud SQL.
  • Il flag tier consente di scegliere tra alcune macchine predefinite.
  • Il valore db-custom-1-3840 indica che l'istanza da creare deve avere una vCPU e circa 3,75 GB di memoria.

L'istanza Cloud SQL verrà creata e inizializzata con un database PostgreSQL, con l'utente predefinito postgres. Qual è la password di questo utente? Ottima domanda. Non ne hanno uno. Devi configurarne uno prima di poter accedere.

Imposta la password con il seguente comando:

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

Quindi inserisci la password che hai scelto quando ti viene richiesto.

Attivare l'estensione PostGIS

PostGIS è un'estensione per PostGresSQL che semplifica l'archiviazione di tipi standardizzati di dati geospaziali. In circostanze normali, dovremmo eseguire una procedura di installazione completa per aggiungere PostGIS al nostro database. Fortunatamente, è una delle estensioni supportate da Cloud SQL per PostgreSQL.

Connettiti all'istanza del database eseguendo l'accesso come utente postgres con il seguente comando nel terminale Cloud Shell.

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

Inserisci la password che hai appena creato. Ora aggiungi l'estensione PostGIS al prompt dei comandi postgres=>.

CREATE EXTENSION postgis;

Se l'operazione va a buon fine, l'output dovrebbe essere CREATE EXTENSION, come mostrato di seguito.

Output del comando di esempio

CREATE EXTENSION

Infine, esci dalla connessione al database inserendo il comando quit al prompt dei comandi postgres=>.

\q

Importare dati geografici nel database

Ora dobbiamo importare tutti i dati sulla posizione dai file GeoJSON nel nostro nuovo database.

Fortunatamente, questo è un problema ben noto e su internet si possono trovare diversi strumenti per automatizzare questa operazione. Utilizzeremo uno strumento chiamato ogr2ogr, che esegue la conversione tra più formati comuni per l'archiviazione dei dati geospaziali. Tra queste opzioni c'è, sì, hai indovinato, la conversione da GeoJSON a un file di dump SQL. Il file di dump SQL può quindi essere utilizzato per creare tabelle e colonne per il database e caricarlo con tutti i dati presenti nei file GeoJSON.

Crea il file di dump SQL

Innanzitutto, installa ogr2ogr.

sudo apt-get install gdal-bin

Successivamente, utilizza ogr2ogr per creare il file di dump SQL. Questo file creerà una tabella denominata austinrecycling.

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

Il comando precedente si basa sull'esecuzione dalla cartella austin-recycling. Se devi eseguirlo da un'altra directory, sostituisci data con il percorso della directory in cui è archiviato recycling-locations.geojson.

Compilare il database con le sedi di riciclaggio

Al termine dell'ultimo comando, dovresti avere un file, datadump.sql,, nella stessa directory in cui hai eseguito il comando. Se lo apri, vedrai poco più di un centinaio di righe di SQL, che creano una tabella austinrecycling e la compilano con le località.

Ora apri una connessione al database ed esegui lo script con il seguente comando.

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

Se lo script viene eseguito correttamente, le ultime righe dell'output saranno simili a queste:

Output comando di esempio

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

Aggiorna il backend Go per utilizzare Cloud SQL

Ora che abbiamo tutti questi dati nel nostro database, è il momento di aggiornare il codice.

Aggiornare il front-end per inviare i dati sulla posizione

Iniziamo con un aggiornamento molto piccolo del front-end: poiché ora stiamo scrivendo questa app per una scala in cui non vogliamo che ogni singola posizione venga inviata al front-end ogni volta che viene eseguita la query, dobbiamo passare alcune informazioni di base dal front-end sulla posizione che interessa all'utente.

Apri app.js e sostituisci la definizione della funzione fetchStores con questa versione per includere la latitudine e la longitudine di interesse nell'URL.

app.js - fetchStores

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

Dopo aver completato questo passaggio del codelab, la risposta restituirà solo i negozi più vicini alle coordinate della mappa fornite nel parametro center. Per il recupero iniziale nella funzione initialize, il codice campione fornito in questo lab utilizza le coordinate centrali di Austin, in Texas.

Poiché fetchStores ora restituirà solo un sottoinsieme delle sedi dei negozi, dovremo recuperare nuovamente i negozi ogni volta che l'utente cambia la posizione di partenza.

Aggiorna la funzione initAutocompleteWidget per aggiornare le posizioni ogni volta che viene impostata una nuova origine. Per questo sono necessarie due modifiche:

  1. In initAutocompleteWidget, trova il callback per il listener place_changed. Rimuovi il commento dalla riga che cancella i cerchi esistenti, in modo che venga eseguita ogni volta che l'utente seleziona un indirizzo dalla barra di ricerca di completamento automatico dei luoghi.

app.js - initAutocompleteWidget

  autocomplete.addListener("place_changed", async () => {
    circles.forEach((c) => c.setMap(null)); // clear existing stores
    // ...
  1. Ogni volta che viene modificata l'origine selezionata, la variabile originLocation viene aggiornata. Alla fine del callback "place_changed", rimuovi il commento dalla riga sopra la riga "// TODO: Calculate the closest stores" per passare questa nuova origine a una nuova chiamata alla funzione fetchAndRenderStores.

app.js - initAutocompleteWidget

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

Aggiorna il backend in modo che utilizzi CloudSQL anziché un file JSON piatto

Rimozione della lettura e della memorizzazione nella cache di GeoJSON flat-file

Innanzitutto, modifica main.go per rimuovere il codice che carica e memorizza nella cache il file GeoJSON piatto. Possiamo anche eliminare la funzione dropoffsHandler, in quanto ne scriveremo una basata su Cloud SQL in un file diverso.

Il tuo nuovo main.go sarà molto più breve.

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

Crea un nuovo gestore per le richieste di localizzazione

Ora creiamo un altro file, locations.go, sempre nella directory austin-recycling. Inizia reimplementando il gestore per le richieste di localizzazione.

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

Il gestore esegue le seguenti attività significative:

  • Estrae la latitudine e la longitudine dall'oggetto richiesta (ricordi come le abbiamo aggiunte all'URL? )
  • Attiva la chiamata getGeoJsonFromDatabase, che restituisce una stringa GeoJSON (la scriveremo in un secondo momento).
  • Utilizza ResponseWriter per stampare la stringa GeoJSON nella risposta.

Successivamente, creeremo un pool di connessioni per consentire l'utilizzo del database in modo scalabile con più utenti simultanei.

Crea un pool di connessioni

Un pool di connessioni è una raccolta di connessioni attive al database che il server può riutilizzare per gestire le richieste degli utenti. Elimina molti overhead man mano che aumenta il numero di utenti attivi, poiché il server non deve dedicare tempo alla creazione e all'eliminazione delle connessioni per ogni utente attivo. Nella sezione precedente abbiamo importato la libreria github.com/jackc/pgx/stdlib.. Si tratta di una libreria molto usata per lavorare con i pool di connessioni in Go.

Alla fine di locations.go, crea una funzione initConnectionPool (chiamata da main.go) che inizializza un pool di connessioni. Per chiarezza, in questo snippet vengono utilizzati alcuni metodi helper. configureConnectionPool offre un comodo spazio per modificare le impostazioni del pool, come il numero di connessioni e la durata per connessione. mustGetEnv esegue il wrapping delle chiamate per ottenere le variabili di ambiente richieste, in modo che possano essere generati messaggi di errore utili se l'istanza non dispone di informazioni critiche (come l'IP o il nome del database a cui connettersi).

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
}

Esegui query nel database per le località e ricevi JSON in cambio.

Ora scriveremo una query del database che prende le coordinate della mappa e restituisce le 25 località più vicine. Inoltre, grazie ad alcune moderne funzionalità del database, restituirà i dati in formato GeoJSON. Il risultato finale di tutto ciò è che, per quanto riguarda il codice front-end, non è cambiato nulla. Prima di inviare una richiesta a un URL e ricevere una serie di GeoJSON. Ora invia una richiesta a un URL e riceve un insieme di GeoJSON.

Ecco la funzione per eseguire questa magia. Aggiungi la seguente funzione dopo il gestore e il codice di raggruppamento delle connessioni che hai appena scritto nella parte inferiore di 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
}

Questa funzione è principalmente pensata per la configurazione, l'interruzione e la gestione degli errori per l'invio di una richiesta al database. Diamo un'occhiata all'SQL vero e proprio, che svolge molte operazioni interessanti a livello di database, quindi non devi preoccuparti di implementarne nessuna nel codice.

La query non elaborata che viene attivata, una volta analizzata la stringa e inseriti tutti i valori letterali delle stringhe nelle posizioni corrette, ha il seguente aspetto:

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

Questa query può essere visualizzata come una query principale e alcune funzioni di wrapping JSON.

SELECT * ... LIMIT 25 seleziona tutti i campi per ogni sede. Quindi, utilizza la funzione ST_DISTANCE (parte della suite di funzioni di misurazione geografica di PostGIS) per determinare la distanza tra ogni posizione nel database e la coppia di latitudine/longitudine della posizione fornita dall'utente nel front-end. Tieni presente che, a differenza di Distance Matrix, che può fornire la distanza in auto, queste sono distanze geospaziali. Per efficienza, utilizza questa distanza per ordinare e restituire le 25 località più vicine alla posizione specificata dall'utente.

**SELECT json_build_object(‘type', ‘F**eature') racchiude la query precedente, prendendo i risultati e utilizzandoli per creare un oggetto Feature GeoJSON. Inaspettatamente, questa query è anche il punto in cui viene applicato il raggio massimo "16090", ovvero il numero di metri in 10 miglia, il limite rigido specificato dal backend Go. Se ti stai chiedendo perché questa clausola WHERE non è stata aggiunta alla query interna (dove viene determinata la distanza di ogni località), è perché, a causa del modo in cui SQL viene eseguito in background, questo campo potrebbe non essere stato calcolato quando è stata esaminata la clausola WHERE. Infatti, se provi a spostare questa clausola WHERE nella query interna, verrà generato un errore.

**SELECT json_build_object(‘type', ‘FeatureColl**ection') Questa query racchiude tutte le righe risultanti dalla query di generazione JSON in un oggetto FeatureCollection GeoJSON.

Aggiungere la libreria PGX al progetto

Dobbiamo aggiungere una dipendenza al tuo progetto: il driver e toolkit PostGres, che consente il pooling delle connessioni. Il modo più semplice per farlo è utilizzare i moduli Go. Inizializza un modulo con questo comando in Cloud Shell:

go mod init my_locator

Successivamente, esegui questo comando per analizzare il codice alla ricerca di dipendenze, aggiungi un elenco di dipendenze al file mod e scaricale.

go mod tidy

Infine, esegui questo comando per estrarre le dipendenze direttamente nella directory del progetto in modo che il container possa essere creato facilmente per App Engine Flex.

go mod vendor

Ok, ora puoi provarlo.

Prova

Ok, abbiamo fatto MOLTO. Vediamo come funziona.

Affinché la tua macchina di sviluppo (sì, anche Cloud Shell) si connetta al database, dovremo utilizzare il proxy Cloud SQL per gestire la connessione al database. Per configurare il proxy Cloud SQL:

  1. Vai qui per abilitare l'API Cloud SQL Admin
  2. Se ti trovi su una macchina di sviluppo locale, installa lo strumento proxy Cloud SQL. Se utilizzi Cloud Shell, puoi saltare questo passaggio, perché è già installato. Tieni presente che le istruzioni si riferiranno a un service account. Ne è già stato creato uno per te e nella sezione seguente vedremo come aggiungere le autorizzazioni necessarie a questo account.
  3. Crea una nuova scheda (in Cloud Shell o nel tuo terminale) per avviare il proxy.

bcca42933bfbd497.png

  1. Visita la pagina https://console.cloud.google.com/sql/instances/locations/overview e scorri verso il basso per trovare il campo Nome connessione. Copia il nome da utilizzare nel comando successivo.
  2. In questa scheda, esegui il proxy Cloud SQL con questo comando, sostituendo CONNECTION_NAME con il nome della connessione mostrato nel passaggio precedente.
cloud_sql_proxy -instances=CONNECTION_NAME=tcp:5432

Torna alla prima scheda di Cloud Shell e definisci le variabili di ambiente necessarie a Go per comunicare con il backend del database, quindi esegui il server nello stesso modo di prima:

Se non l'hai già fatto, vai alla directory principale del progetto.

cd YOUR_PROJECT_ROOT

Crea le seguenti cinque variabili di ambiente (sostituisci YOUR_PASSWORD_HERE con la password che hai creato sopra).

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

Esegui l'istanza locale.

go run *.go

Apri la finestra di anteprima e dovrebbe funzionare come se non fosse cambiato nulla: puoi inserire un indirizzo di partenza, ingrandire e ridurre la mappa e fare clic sui centri di riciclaggio. Ora, però, è supportato da un database ed è pronto per la scalabilità.

9. Elenca i negozi più vicini

L'API Directions funziona in modo molto simile all'esperienza di richiesta di indicazioni stradali nell'app Google Maps: inserisci un'unica origine e un'unica destinazione per ricevere un percorso tra le due. L'API Distance Matrix porta questo concetto oltre, identificando gli accoppiamenti ottimali tra più origini possibili e più destinazioni possibili in base ai tempi di percorrenza e alle distanze. In questo caso, per aiutare l'utente a trovare il negozio più vicino all'indirizzo selezionato, fornisci un'origine e un array di sedi dei negozi come destinazioni.

Aggiungi la distanza dall'origine a ogni negozio

All'inizio della definizione della funzione initMap, sostituisci il commento "// TODO: Start Distance Matrix service" con il seguente codice:

app.js - initMap

distanceMatrixService = new google.maps.DistanceMatrixService();

Aggiungi una nuova funzione alla fine di app.js chiamata 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);
  });
};

La funzione chiama l'API Distance Matrix utilizzando l'origine passata come singola origine e le sedi del negozio come array di destinazioni. Poi crea un array di oggetti che memorizzano l'ID del negozio, la distanza espressa in una stringa leggibile, la distanza in metri come valore numerico e ordina l'array.

Aggiorna la funzione initAutocompleteWidget per calcolare le distanze dei negozi ogni volta che viene selezionata una nuova origine dalla barra di ricerca del completamento automatico di Places. Nella parte inferiore della funzione initAutocompleteWidget, sostituisci il commento "// TODO: Calculate the closest stores" con il seguente codice:

app.js - initAutocompleteWidget

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

Visualizzare una visualizzazione elenco dei negozi ordinati per distanza

L'utente si aspetta di vedere un elenco dei negozi ordinati dal più vicino al più lontano. Compila un elenco nel riquadro laterale per ogni negozio utilizzando l'elenco modificato dalla funzione calculateDistances per determinare l'ordine di visualizzazione dei negozi.

Aggiungi due nuove funzioni alla fine di app.js chiamate renderStoresPanel() e 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;
};

Riavvia il server e aggiorna l'anteprima eseguendo il seguente comando.

go run *.go

Infine, inserisci un indirizzo di Austin, Texas, nella barra di ricerca del completamento automatico e fai clic su uno dei suggerimenti.

La mappa dovrebbe centrare l'indirizzo e dovrebbe apparire una barra laterale che elenca le sedi dei negozi in ordine di distanza dall'indirizzo selezionato. Un esempio è illustrato di seguito:

96e35794dd0e88c9.png

10. Applica uno stile alla mappa

Un modo efficace per distinguere visivamente la tua mappa è aggiungere uno stile. Con la personalizzazione delle mappe basata su cloud, la personalizzazione delle mappe viene controllata dalla console Cloud utilizzando la personalizzazione delle mappe basata su cloud (beta). Se preferisci personalizzare lo stile della mappa con una funzionalità non beta, puoi utilizzare la documentazione sullo stile della mappa per generare il codice JSON per la personalizzazione programmatica dello stile della mappa. Le istruzioni riportate di seguito ti guidano nella personalizzazione delle mappe basata su cloud (beta).

Creare un ID mappa

Innanzitutto, apri Cloud Console e digita "Gestione mappe" nella casella di ricerca. Fai clic sul risultato "Map Management (Google Maps)". 64036dd0ed200200.png

Vedrai un pulsante in alto (subito sotto la casella di ricerca) con la dicitura Crea nuovo ID mappa. Fai clic e inserisci il nome che preferisci. Per Tipo di mappa, assicurati di selezionare JavaScript e, quando vengono visualizzate altre opzioni, seleziona Vettoriale dall'elenco. Il risultato finale dovrebbe essere simile a quello dell'immagine seguente.

70f55a759b4c4212.png

Fai clic su "Avanti" e ti verrà assegnato un nuovo ID mappa. Puoi copiarlo ora, se vuoi, ma non preoccuparti, è facile cercarlo in un secondo momento.

Successivamente, creeremo uno stile da applicare alla mappa.

Creare uno stile di mappa

Se ti trovi ancora nella sezione Maps di Cloud Console, fai clic su "Stili mappa " nella parte inferiore del menu di navigazione a sinistra. In alternativa, proprio come per la creazione di un ID mappa, puoi trovare la pagina giusta digitando "Stili della mappa" nella casella di ricerca e selezionando "Stili della mappa (Google Maps)" dai risultati, come nell'immagine seguente.

9284cd200f1a9223.png

Poi fai clic sul pulsante in alto che dice "+ Crea nuovo stile di mappa".

  1. Se vuoi abbinare lo stile della mappa mostrata in questo lab, fai clic sulla scheda "IMPORTA JSON" e incolla il blob JSON riportato di seguito. Altrimenti, se vuoi crearne uno personalizzato, seleziona lo stile mappa da cui vuoi iniziare. Quindi, fai clic su Avanti.
  2. Seleziona l'ID mappa appena creato per associarlo a questo stile e fai di nuovo clic su Avanti.
  3. A questo punto, hai la possibilità di personalizzare ulteriormente lo stile della mappa. Se vuoi esplorare questa opzione, fai clic su Personalizza nell'editor di stili e prova i colori e le opzioni finché non trovi uno stile della mappa che ti piace. Altrimenti, fai clic su Salta.
  4. Nel passaggio successivo, inserisci il nome e la descrizione dello stile, quindi fai clic su Salva e pubblica.

Ecco un blob JSON facoltativo da importare nel primo passaggio.

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

Aggiungere l'ID mappa al codice

Ora che hai creato questo stile di mappa, come puoi utilizzarlo nella tua mappa? Devi apportare due piccole modifiche:

  1. Aggiungi l'ID mappa come parametro URL al tag script in index.html
  2. Add l'ID mappa come argomento del costruttore quando crei la mappa nel metodo initMap().

Sostituisci il tag script che carica l'API Maps JavaScript nel file HTML con l'URL del caricatore riportato di seguito, sostituendo i segnaposto per "YOUR_API_KEY" e "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>
...

Nel metodo initMap di app.js in cui è definita la costante map, rimuovi il commento dalla riga della proprietà mapId e sostituisci "YOUR_MAP_ID_HERE" con l'ID mappa che hai appena creato:

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

Riavvia il server.

go run *.go

Dopo aver aggiornato l'anteprima, lo stile della mappa dovrebbe essere conforme alle tue preferenze. Ecco un esempio che utilizza lo stile JSON riportato sopra.

2ece59c64c06e9da.png

11. Distribuzione in produzione

Se vuoi vedere la tua app in esecuzione da App Engine Flex (e non solo un web server locale sulla tua macchina di sviluppo / Cloud Shell, come hai fatto finora), è molto semplice. Dobbiamo solo aggiungere un paio di cose per far funzionare l'accesso al database nell'ambiente di produzione. Tutto questo è descritto nella pagina della documentazione relativa alla connessione da App Engine Flex a Cloud SQL.

Aggiungi variabili di ambiente ad app.yaml

Innanzitutto, tutte le variabili di ambiente che utilizzavi per i test locali devono essere aggiunte in fondo al file app.yaml dell'applicazione.

  1. Visita https://console.cloud.google.com/sql/instances/locations/overview per cercare il nome della connessione all'istanza.
  2. Incolla il seguente codice alla fine di app.yaml.
  3. Sostituisci YOUR_DB_PASSWORD_HERE con la password che hai creato in precedenza per il nome utente postgres.
  4. Sostituisci YOUR_CONNECTION_NAME_HERE con il valore del passaggio 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

Tieni presente che DB_TCP_HOST deve avere il valore 172.17.0.1, poiché questa app si connette tramite App Engine Flex**.** Questo perché comunicherà con Cloud SQL tramite un proxy, in modo simile a quanto facevi tu.

Aggiungi le autorizzazioni client SQL al service account App Engine Flex

Vai alla pagina IAM e amministrazione in Cloud Console e cerca un service account il cui nome corrisponda al formato service-PROJECT_NUMBER@gae-api-prod.google.com.iam.gserviceaccount.com. Questo è l'account di servizio che App Engine Flex utilizzerà per connettersi al database. Fai clic sul pulsante Modifica alla fine della riga e aggiungi il ruolo "Client Cloud SQL".

b04ccc0b4022b905.png

Copia il codice del progetto nel percorso Go

Affinché App Engine possa eseguire il tuo codice, deve essere in grado di trovare i file pertinenti nel percorso Go. Assicurati di trovarti nella directory principale del progetto.

cd YOUR_PROJECT_ROOT

Copia la directory nel percorso Go.

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

Passa a questa directory.

cd ~/gopath/src/austin-recycling

Eseguire il deployment dell'app

Utilizza l'interfaccia a riga di comando di gcloud per eseguire il deployment dell'app. L'operazione richiederà un po' di tempo.

gcloud app deploy

Utilizza il comando browse per ottenere un link su cui puoi fare clic per vedere in azione il tuo localizzatore di negozi completamente implementato, di livello aziendale e di grande impatto estetico.

gcloud app browse

Se stavi eseguendo gcloud al di fuori di Cloud Shell, l'esecuzione di gcloud app browse aprirà una nuova scheda del browser.

12. (Consigliato) Pulisci

L'esecuzione di questo codelab rientra nei limiti del livello senza costi per l'elaborazione di BigQuery e le chiamate API Maps Platform, ma se l'hai eseguito solo a scopo didattico e vuoi evitare addebiti futuri, il modo più semplice per eliminare le risorse associate a questo progetto è eliminare il progetto stesso.

Eliminare il progetto

Nella console di GCP, vai alla pagina Cloud Resource Manager:

Nell'elenco dei progetti, seleziona il progetto su cui abbiamo lavorato e fai clic su Elimina. Ti verrà chiesto di digitare l'ID progetto. Inseriscilo e fai clic su Chiudi.

In alternativa, puoi eliminare l'intero progetto direttamente da Cloud Shell con gcloud eseguendo il seguente comando e sostituendo il segnaposto GOOGLE_CLOUD_PROJECT con il tuo ID progetto:

gcloud projects delete GOOGLE_CLOUD_PROJECT

13. Complimenti

Complimenti! Hai completato il codelab.

Oppure hai sfogliato fino all'ultima pagina. Complimenti! Hai sfogliato fino all'ultima pagina.

Nel corso di questo codelab, hai lavorato con le seguenti tecnologie:

Per approfondire

C'è ancora molto da imparare su tutte queste tecnologie. Di seguito sono riportati alcuni link utili ad argomenti che non abbiamo avuto il tempo di trattare in questo codelab, ma che potrebbero sicuramente esserti utili per creare una soluzione di localizzazione dei negozi adatta alle tue esigenze specifiche.