Créer un outil complet de localisation de magasins avec Google Maps Platform et Google Cloud

1. Introduction

Résumé

Imaginons que deviez placer de nombreux lieux sur une carte, et que vous souhaitiez que les utilisateurs puissent voir où ces lieux se trouvent et identifier celui auquel ils veulent se rendre. Voici quelques exemples courants :

  • Un outil de localisation de magasins sur le site Web d'un revendeur
  • Une carte des bureaux de vote pour des élections à venir
  • Un annuaire de lieux spécialisés, tels que des récipients pour le recyclage des piles

Objectifs de l'atelier

Dans cet atelier de programmation, vous allez créer un outil de localisation à partir d'un flux de données en direct de lieux spécialisés. Cet outil aidera l'utilisateur à trouver l'établissement le plus proche de son point de départ. Cet outil de localisation complet peut accepter beaucoup plus de lieux que l'outil de localisation de magasins simple, qui est limité à 25 magasins maximum.

2ece59c64c06e9da.png

Points abordés

Cet atelier de programmation utilise un ensemble de données publiques pour simuler des métadonnées préremplies à propos d'un grand nombre de lieux, ce qui vous permet de vous concentrer sur les concepts techniques clés.

  • API Maps JavaScript : affichez un grand nombre de lieux sur une carte Web personnalisée.
  • GeoJSON : format permettant de stocker des métadonnées sur des lieux.
  • Place Autocomplete : aidez les utilisateurs à trouver des points de départ plus rapidement et de façon plus précise.
  • Go : langage de programmation utilisé pour développer le backend de l'application. Le backend interagira avec la base de données et renverra les résultats de la requête au frontal au format JSON.
  • App Engine : pour héberger l'application Web.

Conditions préalables

  • Connaissances de base en HTML et JavaScript
  • Un compte Google

2. Configuration

À l'étape 3 de la section suivante, activez l'API Maps JavaScript, l'API Places et l'API Distance Matrix pour cet atelier de programmation.

Premiers pas avec Google Maps Platform

Si vous n'avez jamais utilisé Google Maps Platform, suivez le guide Premiers pas avec Google Maps Platform ou regardez la playlist de démarrage avec Google Maps Platform pour effectuer les étapes suivantes :

  1. Créer un compte de facturation
  2. Créer un projet
  3. Activer les SDK et les API Google Maps Platform (répertoriés dans la section précédente)
  4. Générer une clé API

Activer Cloud Shell

Dans cet atelier de programmation, vous allez utiliser Cloud Shell, un environnement de ligne de commande fonctionnant dans Google Cloud qui donne accès aux produits et aux ressources exécutés dans ce même service. Vous pourrez ainsi héberger et exécuter votre projet entièrement à partir de votre navigateur Web.

Pour activer Cloud Shell à partir de Cloud Console, cliquez sur Activer Cloud Shell 89665d8d348105cd.png. Le provisionnement de l'environnement et la connexion ne devraient prendre que quelques minutes.

5f504766b9b3be17.png

Une nouvelle interface système s'ouvre au bas de votre navigateur après l'affichage éventuel d'un interstitiel d'introduction.

d3bb67d514893d1f.png

Confirmer votre projet

En principe, une fois que vous êtes connecté à Cloud Shell, vous êtes authentifié, et le projet est défini sur l'ID de projet sélectionné lors de la configuration.

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

Si, pour une raison quelconque, le projet n'est pas défini, exécutez la commande suivante :

gcloud config set project <YOUR_PROJECT_ID>

Activer l'API App Engine Flex

L'API App Engine Flex doit être activée manuellement depuis Cloud Console. Cette opération permet non seulement d'activer l'API, mais aussi de créer le compte de service pour l'environnement flexible App Engine. Il s'agit du compte authentifié qui interagira avec les services Google (les bases de données SQL, par exemple) pour l'utilisateur.

3. Hello World

Backend : Hello World dans Go

Dans votre instance Cloud Shell, commencez par créer une application Go App Engine Flex qui servira de base pour le reste de l'atelier de programmation.

Dans la barre d'outils de Cloud Shell, cliquez sur le bouton Ouvrir l'éditeur pour ouvrir un éditeur de code dans un nouvel onglet. Cet éditeur de code Web vous permet de modifier facilement des fichiers dans l'instance Cloud Shell.

b63f7baad67b6601.png

Cliquez ensuite sur l'icône Ouvrir dans une nouvelle fenêtre pour déplacer l'éditeur et le terminal dans un nouvel onglet.

3f6625ff8461c551.png

Dans le terminal situé au bas du nouvel onglet, créez un répertoire austin-recycling.

mkdir -p austin-recycling && cd $_

Ensuite, créez une petite application Go App Engine pour vous assurer que tout fonctionne correctement. Hello World !

Le répertoire austin-recycling doit également figurer dans la liste de dossiers de l'éditeur sur la gauche. Dans le répertoire austin-recycling, créez un fichier nommé app.yaml. Insérez le contenu suivant dans le fichier app.yaml :

app.yaml

runtime: go
env: flex

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

Ce fichier de configuration configure votre application App Engine pour qu'elle utilise l'environnement d'exécution Go Flex. Pour obtenir des informations générales sur la signification des éléments de configuration de ce fichier, consultez la documentation concernant l'environnement standard Google App Engine Go.

Ensuite, créez un fichier main.go en même temps que le fichier 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!")
}

Il convient de s'arrêter un moment ici pour comprendre le rôle de ce code, au moins dans les grandes lignes. Vous avez défini un package main. Celui-ci démarre un serveur HTTP qui écoute le port 8080 et enregistre une fonction de gestionnaire pour les requêtes HTTP correspondant au chemin "/".

La fonction de gestionnaire, appelée handler, écrit la chaîne de texte "Hello, world!". Ce texte sera renvoyé à votre navigateur, où vous pourrez le lire. Dans les étapes suivantes, vous allez créer des gestionnaires qui renvoient des données GeoJSON au lieu de simples chaînes codées en dur.

Une fois ces étapes effectuées, l'éditeur doit ressembler à ceci :

2084fdd5ef594ece.png

Effectuer un test

Pour tester cette application, vous pouvez exécuter le serveur de développement App Engine à l'intérieur de l'instance Cloud Shell. Revenez à la ligne de commande Cloud Shell et saisissez ce qui suit :

go run *.go

Des lignes de sortie dans le journal vous indiquent que vous exécutez bien le serveur de développement sur l'instance Cloud Shell, avec l'application Web Hello World qui écoute le port localhost 8080. Pour ouvrir un onglet de navigateur Web sur cette application, appuyez sur le bouton Aperçu sur le Web, puis sélectionnez l'élément de menu Prévisualiser sur le port 8080 dans la barre d'outils Cloud Shell.

4155fc1dc717ac67.png

Lorsque vous cliquez sur cet élément de menu, un nouvel onglet s'ouvre dans votre navigateur Web et affiche le message "Hello World" diffusé à partir du serveur de développement App Engine.

À l'étape suivante, vous allez ajouter les données de recyclage de la ville d'Austin à cette application et commencer à les visualiser.

4. Obtenir les données actuelles

Le format GeoJSON, la lingua franca du monde des SIG

À l'étape précédente, il était question de créer des gestionnaires dans votre code Go afin d'afficher des données GeoJSON dans le navigateur Web. Mais qu'est-ce que le format GeoJSON ?

Dans le monde des systèmes d'informations géographiques (SIG), nous devons pouvoir communiquer des informations concernant les entités géographiques entre les systèmes informatiques. Les cartes sont idéales pour les utilisateurs, mais les ordinateurs préfèrent généralement que leurs données soient exprimées dans des formats plus faciles à assimiler.

Le format GeoJSON permet d'encoder des structures de données géographiques telles que les coordonnées des lieux de recyclage de la ville d'Austin au Texas. Le format GeoJSON a été standardisé dans une norme Internet Engineering Task Force appelée RFC7946. Il est défini en termes de JSON (JavaScript Object Notation), qui a lui-même été standardisé dans la norme ECMA-404 par la même organisation qui a standardisé le format JavaScript, à savoir Ecma International.

Le plus important est que le format GeoJSON est largement utilisé pour communiquer des informations géographiques. Cet atelier de programmation utilise le format GeoJSON aux fins suivantes :

  • Utiliser des packages Go pour analyser les données d'Austin dans une structure de données SIG interne que vous utiliserez pour filtrer les données demandées.
  • Sérialiser les données demandées pour le transfert entre le serveur Web et le navigateur Web.
  • Utiliser une bibliothèque JavaScript pour convertir la réponse en repères sur une carte.

Vous éviterez ainsi de devoir rédiger une grande partie de code, car vous ne devrez pas écrire d'analyseurs ni de générateurs pour convertir le flux de données en transit en représentations enregistrées en mémoire.

Récupérer les données

Le portail de données publiques de la ville d'Austin au Texas met à la disposition du public des informations géospatiales sur les ressources publiques. Dans cet atelier de programmation, vous visualiserez l'ensemble de données concernant les lieux de recyclage.

Les données s'afficheront sur la carte avec des repères grâce à la couche de données de l'API Maps JavaScript.

Commencez par télécharger les données GeoJSON sur le site Web de la ville d'Austin dans votre application.

  1. Dans la fenêtre de ligne de commande de votre instance Cloud Shell, arrêtez le serveur en appuyant sur [CTRL]+[C].
  2. Créez un répertoire data à l'intérieur du répertoire austin-recycling, puis accédez au répertoire suivant :
mkdir -p data && cd data

Utilisez maintenant la fonction curl pour récupérer les données des lieux de recyclage :

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

Pour terminer, revenez dans le répertoire parent.

cd ..

5. Ajouter les lieux sur la carte

Tout d'abord, mettez à jour le fichier app.yaml pour qu'il reflète l'application plus robuste, qui ne se limite pas à une simple application "Hello World", que vous êtes sur le point de créer.

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

Cette configuration app.yaml dirige les requêtes concernant /, /*.js, /*.css et /*.html vers un ensemble de fichiers statiques. Cela signifie que le composant HTML statique de votre application sera diffusé directement par l'infrastructure de diffusion de fichiers App Engine, et non par votre application Go. Cela réduit la charge du serveur et augmente la vitesse de diffusion.

Vous devez maintenant créer le backend de votre application dans Go.

Créer le backend

Comme vous l'avez peut-être remarqué, votre fichier app.yaml ne divulgue pas le fichier GeoJSON. En effet, le fichier GeoJSON va être traité et envoyé par notre backend Go, ce qui nous permet d'ajouter des fonctionnalités intéressantes dans les étapes suivantes. Modifiez votre fichier main.go comme suit :

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

Le backend Go offre déjà une fonctionnalité intéressante : l'instance App Engine met en cache tous ces lieux dès son démarrage. Cela permet de gagner du temps, car le backend ne devra pas lire le fichier à partir du disque à chaque actualisation de la part des utilisateurs.

Créer le frontal

Pour commencer, nous devons créer un dossier où stocker tous nos éléments statiques. À partir du dossier parent de votre projet, créez un dossier static.

mkdir -p static && cd static

Nous allons créer trois fichiers dans ce dossier :

  • index.html, qui renfermera tout le code HTML de votre application de localisation de magasins sur une page.
  • style.css, qui, comme son nom l'indique, renfermera le style.
  • app.js, qui sera responsable de la récupération du fichier GeoJSON, des appels à l'API Google Maps et de l'insertion de repères sur votre carte personnalisée.

Créez ces trois fichiers en veillant à les placer dans 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>

Portez une attention particulière à l'URL src dans la balise de script de l'élément head.

  • Remplacez le texte fictif YOUR_API_KEY par la clé API générée lors de l'étape de configuration. Vous pouvez accéder à la page API et services -> Identifiants dans Cloud Console pour récupérer votre clé API ou en générer une.
  • Notez que l'URL contient le paramètre callback=initialize.. Nous allons maintenant créer le fichier JavaScript contenant cette fonction de rappel. C'est à cette étape que votre application charge les lieux à partir du backend, les envoie à l'API Google Maps, puis utilise le résultat pour marquer des lieux personnalisés sur la carte, tous affichés de manière agréable sur votre page Web.
  • Le paramètre libraries=places charge la bibliothèque Places, qui est nécessaire pour des fonctionnalités qui seront ajoutées ultérieurement (comme la saisie semi-automatique des adresses).

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

Ce code permet d'afficher l'emplacement des magasins sur une carte. Pour tester ce que nous avons déjà, retournez dans le répertoire parent à partir de la ligne de commande :

cd ..

Exécutez de nouveau votre application en mode développement à l'aide de la commande suivante :

go run *.go

Prévisualisez-la de la même manière qu'auparavant. Vous devriez voir une carte avec de petits cercles verts comme ce qui suit.

58a6680e9c8e7396.png

Vous affichez déjà des lieux sur une carte, et nous ne sommes qu'à la moitié de cet atelier de programmation. C'est incroyable. Ajoutons maintenant une dose d'interactivité.

6. Afficher des détails à la demande

Répondre aux événements de clic sur des repères sur la carte

L'affichage de plusieurs repères sur la carte est un bon point de départ, mais un visiteur doit vraiment pouvoir cliquer sur l'un de ces repères et afficher des informations sur ce lieu (le nom de l'entreprise, son adresse, etc.). Lorsque vous cliquez sur un repère Google Maps, une fenêtre d'informations s'affiche.

Créez un objet infoWindow. Ajoutez ce qui suit à la fonction initialize, en remplaçant la ligne en commentaire // 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();

Remplacez la définition de la fonction fetchAndRenderStores par cette version légèrement différente, qui change la ligne finale de sorte qu'elle appelle storeToCircle avec un argument supplémentaire, 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));
};

Remplacez la définition storeToCircle par cette version légèrement plus longue, qui utilise désormais une fenêtre d'informations comme troisième argument :

app.js - storeToCircle

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

Le nouveau code ci-dessus affiche une infoWindow comprenant les informations du magasin sélectionné lorsqu'un utilisateur clique sur un repère de magasin sur la carte.

Si votre serveur est toujours en cours d'exécution, arrêtez-le et redémarrez-le. Actualisez la page de votre carte et essayez de cliquer sur un repère. Une petite fenêtre d'informations doit s'afficher et présenter le nom et l'adresse de l'établissement. Le résultat doit se présenter comme suit :

1af0ab72ad0eadc5.png

7. Obtenir le lieu de départ de l'utilisateur

En général, les internautes se servent des outils de localisation de magasins lorsqu'ils veulent savoir quel magasin est situé le plus près de leur position ou lorsqu'ils souhaitent connaître une adresse au moment de planifier un trajet. Ajoutez une barre de recherche Place Autocomplete pour permettre à l'utilisateur de saisir facilement une adresse de départ. Celle-ci offre des fonctions de saisie semblables à celles de la saisie semi-automatique des autres barres de recherche Google, mais les prédictions sont toutes des adresses de Google Maps Platform.

Créer un champ de saisie utilisateur

Revenez au fichier style.css pour ajouter un style à la barre de recherche Autocomplete et au panneau latéral de résultats. Pendant la mise à jour des styles CSS, nous allons également ajouter des styles pour une future barre latérale qui affichera des informations sur les magasins sous forme de liste qui accompagnera la carte.

Ajoutez ce code à la fin du fichier.

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

La barre de recherche Autocomplete et le panneau coulissant sont masqués jusqu'à ce que vous en ayez besoin.

Préparez un élément DIV pour le widget Autocomplete en remplaçant dans le fichier index.html le commentaire "<!-- Autocomplete div goes here --> par le code ci-dessous. En même temps que cette modification, nous allons aussi ajouter l'élément DIV du panneau coulissant.

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>

Ensuite, définissez une fonction permettant d'ajouter le widget Autocomplete à la carte en ajoutant le code suivant à la fin de 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
};

Le code restreint les suggestions Autocomplete pour ne renvoyer que des adresses (car Place Autocomplete peut aussi renvoyer des noms d'établissements et des lieux administratifs), et limite ces adresses aux États-Unis. L'ajout de ces spécifications facultatives réduit le nombre de caractères nécessaires pour affiner les prédictions et afficher l'adresse recherchée.

Elles permettent aussi de déplacer le conteneur Autocomplete div que vous avez créé en haut à droite de la carte et spécifient les champs à renvoyer pour chaque adresse dans la réponse.

Pour terminer, appelez la fonction initAutocompleteWidget à la fin de la fonction initialize, et remplacez le commentaire // TODO: Initialize the Autocomplete widget.

app.js - initialize

 // Initialize the Places Autocomplete Widget
 initAutocompleteWidget();

Redémarrez votre serveur en exécutant la commande suivante, puis actualisez l'aperçu.

go run *.go

Un widget Autocomplete devrait maintenant s'afficher en haut à droite de votre carte. Il indique les adresses qui, aux États-Unis, correspondent à votre saisie et les hiérarchise en fonction de la zone visible de la carte.

58e9bbbcc4bf18d1.png

Mettre à jour la carte lorsque l'utilisateur sélectionne une adresse de départ

À présent, vous devez gérer ce qui doit se passer quand l'utilisateur sélectionne une prédiction à partir du widget Autocomplete et utiliser l'adresse choisie comme base pour calculer la distance entre lui et vos magasins.

Ajoutez le code suivant à la fin de initAutocompleteWidget dans le fichier app.js pour remplacer le commentaire // 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
  });

Le code ajoute un écouteur, de sorte que lorsque l'utilisateur clique sur l'une des suggestions, la carte se recentre sur l'adresse sélectionnée et définit le point de départ comme base pour votre calcul de distance. Une étape ultérieure vous permettra de mettre en œuvre ce calcul de distance.

Arrêtez et redémarrez votre serveur, puis actualisez l'aperçu. Vous pourrez voir que la carte se recentre après que vous avez saisi une adresse dans la barre de recherche avec saisie automatique.

8. Mise à l'échelle avec Cloud SQL

Nous disposons maintenant d'un excellent outil de localisation de magasins. Il profite du fait que l'application n'utilisera jamais qu'environ une centaine de lieux et les charge dans la mémoire au niveau du backend (au lieu de les lire depuis le fichier de façon répétée). Et si votre outil de localisation devait fonctionner à une autre échelle ? Si vous disposez de centaines d'établissements éparpillés dans une zone géographique étendue (voire des milliers d'établissements situés aux quatre coins du monde), il n'est plus judicieux de placer tous ces lieux dans la mémoire. Répartir les zones dans des fichiers individuels pose également problème.

Il est temps de charger vos établissements à partir d'une base de données. Pour cette étape, nous allons migrer tous les lieux de votre fichier GeoJSON dans une base de données Cloud SQL, puis modifier le backend Go pour extraire les résultats de cette base de données plutôt que de son cache local chaque fois qu'une requête arrive.

Créer une instance Cloud SQL avec la base de données PostGres

Vous pouvez créer une instance Cloud SQL via Google Cloud Console, mais il est encore plus simple d'utiliser l'utilitaire gcloud pour en créer une à partir de la ligne de commande. Dans Cloud Shell, créez une instance Cloud SQL à l'aide de la commande suivante :

gcloud sql instances create locations \
--database-version=POSTGRES_12 \
--tier=db-custom-1-3840 --region=us-central1
  • L'argument locations correspond au nom que nous choisissons d'attribuer à cette instance de Cloud SQL.
  • L'indicateur tier permet de faire un choix parmi des machines prédéfinies pratiques.
  • La valeur db-custom-1-3840 indique que l'instance en cours de création doit présenter un processeur virtuel et environ 3,75 Go de mémoire.

L'instance Cloud SQL est créée et initialisée avec une base de données PostGresSQL, avec l'utilisateur par défaut postgres. Quel est le mot de passe de cet utilisateur ? Excellente question. Il n'y en a pas. Vous devez en configurer un avant de pouvoir vous connecter.

Définissez le mot de passe à l'aide de la commande suivante :

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

Par la suite, saisissez le mot de passe choisi lorsque vous y êtes invité.

Activer l'extension PostGIS

PostGIS est une extension de PostGresSQL qui facilite le stockage des types standardisés de données géospatiales. Normalement, il faudrait suivre un processus d'installation complet pour ajouter PostGIS à notre base de données. Heureusement, il s'agit de l'une des extensions compatibles avec Cloud SQL pour PostGresSQL.

Connectez-vous en tant qu'utilisateur postgres à l'instance de la base de données à l'aide de la commande ci-dessous dans le terminal Cloud Shell.

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

Saisissez le mot de passe que vous venez de créer. Ajoutez ensuite l'extension PostGIS à l'invite de commande postgres=>.

CREATE EXTENSION postgis;

Si l'opération réussit, "CREATE EXTENSION" doit s'afficher, comme illustré ci-dessous.

Exemple de résultat de la commande

CREATE EXTENSION

Pour terminer, quittez la connexion à la base de données en saisissant la commande de fermeture dans l'invite de commande postgres=>.

\q

Importer des données géographiques dans une base de données

Il convient maintenant d'importer toutes ces données de localisation à partir des fichiers GeoJSON dans la nouvelle base de données.

Il s'agit heureusement d'une opération connue, et plusieurs outils disponibles sur Internet permettent de l'automatiser pour vous. Nous allons utiliser l'outil ogr2ogr, qui permet de convertir différents formats courants de stockage de données géospatiales. Comme vous l'aurez probablement deviné, il est possible grâce à cet outil de convertir un fichier GeoJSON en fichier de vidage SQL. Vous pouvez ensuite utiliser le fichier de vidage SQL pour créer vos tableaux et colonnes pour la base de données. Intégrez-y aussi toutes les données que contenaient vos fichiers GeoJSON.

Créer un fichier de vidage SQL

Commencez par installer ogr2ogr.

sudo apt-get install gdal-bin

Ensuite, utilisez ogr2ogr pour créer le fichier de vidage SQL. Ce fichier crée un tableau intitulé austinrecycling.

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

La commande ci-dessus est basée sur l'exécution du dossier austin-recycling. Si vous devez l'exécuter à partir d'un autre répertoire, remplacez data par le chemin d'accès au répertoire dans lequel recycling-locations.geojson est stocké.

Renseigner les lieux de recyclage dans votre base de données

Une fois cette dernière commande exécutée, le répertoire dans lequel vous avez effectué cette opération doit contenir un fichier datadump.sql,. Si vous l'ouvrez, vous verrez un peu plus d'une centaine de lignes SQL, ce qui crée un tableau austinrecycling qui répertorie les lieux.

Ouvrez ensuite une connexion à la base de données et exécutez ce script avec la commande suivante :

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

Si le script s'exécute correctement, voici à quoi doivent ressembler les dernières lignes de résultats :

Exemple de résultat de la commande

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

Modifier le backend Go pour utiliser Cloud SQL

Maintenant que la base de données contient toutes ces données, il est temps de mettre à jour le code.

Modifier le frontal pour envoyer les informations sur les lieux

Commençons par apporter une petite modification au frontal : étant donné que nous développons cette application de sorte que chaque lieu ne s'affiche pas chaque fois qu'une requête est envoyée, il convient de transmettre des informations de base à partir du frontal concernant le lieu qui intéresse l'utilisateur.

Ouvrez app.js et remplacez la définition de la fonction fetchStores par cette version afin d'inclure la latitude et la longitude du point d'intérêt dans l'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();
};

Une fois cette étape de l'atelier de programmation terminée, la réponse ne renverra que les magasins les plus proches des coordonnées fournies dans le paramètre center. Pour la récupération initiale de la fonction initialize, l'exemple de code fourni dans cet atelier utilise les coordonnées centrales d'Austin, au Texas.

Étant donné que fetchStores ne renvoie maintenant qu'un sous-ensemble des magasins, nous devons de nouveau extraire les magasins chaque fois que l'utilisateur change de point de départ.

Mettez à jour la fonction initAutocompleteWidget pour actualiser les lieux chaque fois qu'un nouveau point de départ est défini. Cela nécessite deux modifications :

  1. Dans initAutocompleteWidget, recherchez le rappel correspondant à l'écouteur place_changed. Annulez la mise en commentaire de la ligne qui efface les cercles existants. Cette ligne sera ainsi exécutée à chaque fois que l'utilisateur sélectionnera une adresse dans la barre de recherche Place Autocomplete.

app.js - initAutocompleteWidget

  autocomplete.addListener("place_changed", async () => {
    circles.forEach((c) => c.setMap(null)); // clear existing stores
    // ...
  1. À chaque modification du point de départ, la variable originLocation est mise à jour. À la fin du rappel place_changed, annulez la mise en commentaire de la ligne au-dessus de la ligne // TODO: Calculate the closest stores pour transmettre ce nouveau point de départ à un nouvel appel de la fonction fetchAndRenderStores.

app.js - initAutocompleteWidget

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

Modifier le backend pour utiliser Cloud SQL au lieu d'un fichier JSON plat

Supprimer la lecture et la mise en cache des fichiers GeoJSON plats

Tout d'abord, modifiez main.go pour supprimer le code qui charge et met en cache le fichier GeoJSON plat. Nous pouvons également supprimer la fonction dropoffsHandler, car nous allons en écrire une fournie par Cloud SQL dans un autre fichier.

Votre nouveau main.go sera beaucoup plus court.

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

Créer un gestionnaire pour les requêtes de position

Nous allons maintenant créer un autre fichier, locations.go, qui se trouve également dans le répertoire dédié au recyclage à Austin. Commencez par implémenter de nouveau le gestionnaire pour les requêtes de position.

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

Le gestionnaire effectue les tâches importantes suivantes :

  • Il extrait la latitude et la longitude de l'objet de la requête (vous souvenez-vous de la façon dont nous avons ajouté ces informations dans l'URL ? ).
  • Il déclenche l'appel getGeoJsonFromDatabase, qui renvoie une chaîne GeoJSON (nous l'écrirons plus tard).
  • Il utilise la fonction ResponseWriter pour imprimer cette chaîne GeoJSON dans la réponse.

Nous allons ensuite créer un pool de connexions afin de faciliter l'évolutivité de l'utilisation de la base de données avec des utilisateurs simultanés.

Créer un pool de connexions

Un pool de connexions est un ensemble de connexions actives à la base de données que le serveur peut réutiliser pour traiter les requêtes des utilisateurs. Il permet de vous simplifier grandement la tâche à mesure que le nombre d'utilisateurs actifs évolue, car le serveur ne perd pas de temps à créer et détruire des connexions pour chaque utilisateur actif. Vous avez peut-être remarqué dans la section précédente que nous avons importé la bibliothèque github.com/jackc/pgx/stdlib.. Il s'agit d'une bibliothèque populaire pour travailler avec des pools de connexions dans Go.

À la fin de locations.go, créez une fonction initConnectionPool (appelée à partir de main.go) qui initialise un pool de connexions. Par souci de clarté, plusieurs méthodes d'assistance sont utilisées dans cet extrait. configureConnectionPool permet d'ajuster facilement les paramètres du pool, comme le nombre de connexions et la durée de vie de chaque connexion. mustGetEnv encapsule les appels pour obtenir les variables d'environnement requises. Des messages d'erreur utiles peuvent donc être générés si l'instance ne dispose pas d'informations essentielles (telles que l'adresse IP ou le nom de la base de données à laquelle se connecter).

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
}

Interroger la base de données à propos de lieux et obtenir un fichier JSON en retour

Nous allons maintenant créer une requête à la base de données qui utilise des coordonnées géographiques et renvoie les 25 lieux les plus proches. Grâce à une fonctionnalité de base de données sophistiquée, elle renverra ces données sous la forme d'un fichier GeoJSON. Au final, en ce qui concerne le code du frontal, rien n'a changé. Auparavant, une requête était envoyée à une URL, et un ensemble de fichiers GeoJSON était renvoyé. Maintenant, une requête est envoyée à une URL, et… un ensemble de fichiers GeoJSON est renvoyé.

La fonction sur laquelle repose cette magie est décrite ci-dessous. Ajoutez la fonction suivante après le code du gestionnaire et du pool de connexions que vous venez d'écrire en bas de 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
}

Cette fonction consiste essentiellement en la gestion de la configuration, de la suppression et des erreurs pour envoyer une requête à la base de données. Intéressons-nous au SQL réel, qui effectue de nombreuses opérations vraiment intéressantes au niveau de la base de données. Vous n'avez donc pas à vous soucier de leur implémentation dans le code.

Une fois la chaîne analysée et tous les littéraux de chaîne insérés à leur emplacement approprié, la requête brute qui est déclenchée ressemble à ceci :

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

Cette requête peut être considérée comme une requête principale accompagnée de quelques fonctions d'encapsulation JSON.

SELECT * ... LIMIT 25 sélectionne tous les champs pour chaque lieu. Ce paramètre utilise ensuite la fonction ST_DISTANCE (qui fait partie de la suite de fonctions de mesure géographique de PostGIS) pour déterminer la distance entre chaque lieu dans la base de données et les coordonnées de latitude et longitude du lieu indiqué par l'utilisateur dans le frontal. N'oubliez pas que, contrairement à Distance Matrix qui peut vous fournir la distance à parcourir, il s'agit de distances GeoSpatial. Pour plus d'efficacité, cette distance est ensuite utilisée à des fins de tri, puis les 25 lieux les plus proches de celui spécifié par l'utilisateur sont renvoyés.

**SELECT json_build_object(‘type', ‘F**eature') encapsule la requête précédente. Cette fonction utilise alors les résultats pour créer un objet "Feature" GeoJSON. Étonnamment, cette requête correspond également à l'emplacement où le rayon maximal est appliqué. "16090" correspond au nombre de mètres dans 10 miles, c'est-à-dire la limite stricte spécifiée par le backend Go. Si vous vous demandez pourquoi cette clause WHERE n'a pas été ajoutée à la requête interne (où la distance de chaque lieu est déterminée), cela est dû au fait qu'étant donné que SQL s'exécute en arrière-plan, ce champ n'a peut-être pas été calculé lors de l'examen de la clause WHERE. Si vous essayez de déplacer cette clause WHERE vers la requête interne, vous obtiendrez une erreur.

**SELECT json_build_object(‘type', ‘FeatureColl**ection') Cette requête encapsule toutes les lignes obtenues à partir de la requête qui génère des fichiers JSON dans un objet "FeatureCollection" GeoJSON.

Ajouter une bibliothèque PGX à votre projet

Nous devons ajouter une dépendance à votre projet : le pilote et kit PostGres, qui donne accès au pool de connexions. Pour ce faire, le moyen le plus simple consiste à utiliser des modules Go. Initialisez un module à l'aide de la commande suivante dans Cloud Shell :

go mod init my_locator

Exécutez ensuite cette commande pour rechercher des dépendances dans le code, ajouter une liste de dépendances au fichier mod et les télécharger.

go mod tidy

Enfin, exécutez cette commande pour extraire les dépendances directement dans le répertoire de votre projet, afin de faciliter la création du conteneur pour App Engine Flex.

go mod vendor

Vous êtes prêt à effectuer un test !

Effectuer un test

Nous avons abattu BEAUCOUP de travail. Voyons le résultat !

Afin que votre ordinateur de développement (même Cloud Shell, oui) puisse se connecter à la base de données, nous allons devoir utiliser le proxy Cloud SQL pour gérer la connexion à la base de données. Pour configurer le proxy Cloud SQL :

  1. Accédez à cette page pour activer l'API Cloud SQL Admin.
  2. Sur un ordinateur de développement local, installez l'outil de proxy Cloud SQL. Si vous utilisez Cloud Shell, vous pouvez ignorer cette étape, car elle est déjà installée. Notez que les instructions impliquent l'utilisation d'un compte de service. Un compte a déjà été créé pour vous, et nous verrons comment ajouter les autorisations nécessaires à ce compte dans la section suivante.
  3. Créez un onglet (dans Cloud Shell ou votre propre terminal) pour démarrer le proxy.

bcca42933bfbd497.png

  1. Accédez à https://console.cloud.google.com/sql/instances/locations/overview, puis faites défiler l'écran jusqu'au champ Nom de la connexion. Copiez ce nom pour l'utiliser dans la commande suivante.
  2. Dans cet onglet, exécutez le proxy Cloud SQL avec cette commande en remplaçant CONNECTION_NAME par le nom de la connexion affiché à l'étape précédente.
cloud_sql_proxy -instances=CONNECTION_NAME=tcp:5432

Revenez dans le premier onglet de Cloud Shell et définissez les variables d'environnement Go nécessaires pour communiquer avec le backend de la base de données, puis exécutez le serveur comme vous l'avez fait auparavant :

Si ce n'est pas déjà fait, accédez au répertoire racine du projet.

cd YOUR_PROJECT_ROOT

Créez les cinq variables d'environnement suivantes (remplacez YOUR_PASSWORD_HERE par le mot de passe que vous avez créé ci-dessus).

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

Exécutez votre instance locale.

go run *.go

Ouvrez la fenêtre d'aperçu. Elle devrait fonctionner comme si rien n'avait changé : vous pouvez saisir une adresse de départ, effectuer des zooms avant et arrière sur la carte et cliquer sur les lieux de recyclage. Elle repose toutefois maintenant sur une base de données et est évolutive.

9. Répertorier les magasins les plus proches

L'API Directions fonctionne de la même manière que le traitement des requêtes d'itinéraire dans l'application Google Maps : vous pouvez saisir un point de départ et une destination pour connaître l'itinéraire entre les deux. L'API Distance Matrix pousse le concept plus loin et identifie les meilleures combinaisons entre plusieurs points de départ et plusieurs destinations possibles, en fonction du temps de trajet et de la distance. Dans ce cas, pour aider l'utilisateur à trouver le magasin le plus proche de l'adresse sélectionnée, vous allez indiquer un point de départ unique et plusieurs adresses de destination.

Ajouter la distance entre le point de départ et chaque magasin

Au début de la définition de la fonction initMap, remplacez le commentaire // TODO: Start Distance Matrix service par le code suivant :

app.js - initMap

distanceMatrixService = new google.maps.DistanceMatrixService();

Ajoutez une fonction à la fin de la propriété app.js appelée 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 fonction appelle l'API Distance Matrix en se basant sur l'adresse qui lui est transmise en tant que point de départ unique ainsi que sur les emplacements des magasins qui sont donnés en tant que destinations. Elle crée ensuite un tableau d'objets incluant l'ID du magasin, la distance exprimée sous la forme d'une chaîne lisible ainsi que la distance exprimée sous la forme d'une valeur numérique (en mètres), et elle organise ce tableau.

Mettez à jour la fonction initAutocompleteWidget pour calculer la distance jusqu'au magasin chaque fois qu'un nouveau point de départ est sélectionné dans la barre de recherche Place Autocomplete. En bas de la fonction initAutocompleteWidget, remplacez le commentaire // TODO: Calculate the closest stores par le code suivant :

app.js - initAutocompleteWidget

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

Afficher une liste de magasins triés en fonction de la distance

L'utilisateur s'attend à voir une liste de magasins, des plus proches aux plus éloignés. Dans le panneau latéral, complétez la fiche pour chaque magasin à l'aide de la liste modifiée par la fonction calculateDistances, qui indique l'ordre d'affichage des magasins.

Ajoutez deux fonctions à la fin de app.js, nommées renderStoresPanel() et 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;
};

Redémarrez votre serveur et actualisez l'aperçu en exécutant la commande suivante :

go run *.go

Pour finir, saisissez une adresse à Austin (Texas) dans la barre de recherche Autocomplete, puis cliquez sur l'une des suggestions.

La carte doit être centrée sur cette adresse, et la barre latérale doit s'afficher pour indiquer l'emplacement des magasins, en commençant par celui qui est le plus proche de l'adresse sélectionnée. Voici un exemple :

96e35794dd0e88c9.png

10. Ajouter un style à la carte

Si vous souhaitez que votre carte se démarque visuellement, l'un des meilleurs moyens est de lui ajouter un style. Avec le style de carte dans le cloud, la personnalisation de vos cartes est contrôlée depuis Cloud Console à l'aide de la fonctionnalité en version bêta de style de carte dans le cloud. Si vous préférez styliser votre carte à l'aide d'une fonctionnalité qui n'est pas en version bêta, consultez la documentation sur la stylisation des cartes pour savoir comment générer un fichier JSON permettant de styliser la carte via le programmatique. Les instructions ci-dessous vous expliquent comment utiliser la fonctionnalité en version bêta de style de carte dans le cloud.

Créer un identifiant de carte

Commencez par ouvrir Cloud Console, puis saisissez "Gestion des cartes" dans le champ de recherche. Cliquez sur le résultat "Gestion des cartes (Google Maps)". 64036dd0ed200200.png

Le bouton Créer un ID de carte s'affiche en haut de l'écran (juste en dessous du champ de recherche). Cliquez dessus, puis saisissez le nom de votre choix. Dans "Type de carte", veillez à sélectionner JavaScript. Ensuite, lorsque d'autres options s'affichent, sélectionnez Vecteur dans la liste. Le résultat final doit ressembler à l'image ci-dessous.

70f55a759b4c4212.png

Cliquez sur "Suivant" pour recevoir un nouvel identifiant de carte. Vous pouvez le copier maintenant si vous le souhaitez, mais ne vous inquiétez pas, vous pourrez le retrouver facilement plus tard.

Nous allons ensuite créer un style à appliquer à cette carte.

Créer un style de carte

Dans la section "Maps" de Cloud Console, cliquez sur "Styles de carte" en bas du menu de navigation de gauche. Comme pour la création d'un identifiant de carte, vous pouvez également trouver la bonne page en saisissant "Styles de carte" dans le champ de recherche et en sélectionnant "Styles de carte (Google Maps)" dans les résultats, comme indiqué sur l'image ci-dessous.

9284cd200f1a9223.png

Cliquez ensuite sur le bouton + Créer un style de carte en haut de l'écran.

  1. Si vous souhaitez utiliser le même style que celui de la carte présentée dans cet atelier, cliquez sur l'onglet IMPORTER LES DONNÉES AU FORMAT JSON, puis collez le blob JSON ci-dessous. Si vous souhaitez créer votre propre style, sélectionnez le style de carte avec lequel vous souhaitez commencer. Cliquez ensuite sur Suivant.
  2. Sélectionnez l'identifiant de carte que vous venez de créer pour l'associer à ce style, puis cliquez de nouveau sur Suivant.
  3. À ce stade, vous avez la possibilité de personnaliser davantage le style de votre carte. Si cela vous intéresse, cliquez sur Personnaliser dans l'éditeur de style, puis jouez avec les couleurs et les options jusqu'à ce que le style de carte vous plaise. Sinon, cliquez sur Ignorer.
  4. À l'étape suivante, saisissez un nom et une description pour votre style, puis cliquez sur Enregistrer et publier.

Vous trouverez ci-dessous un blob JSON facultatif à importer lors de la première étape.

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

Ajouter un identifiant de carte dans votre code

Maintenant que vous êtes parvenu à créer un style de carte, comment faire pour l'UTILISER dans votre propre carte ? Il suffit d'apporter deux petites modifications :

  1. Ajoutez l'identifiant de carte en tant que paramètre d'URL au tag de script dans le fichier index.html.
  2. Add l'identifiant de carte en tant qu'argument constructeur lorsque vous créez la carte dans votre méthode initMap().

Remplacez le tag de script qui charge l'API Maps JavaScript dans le fichier HTML par l'URL du chargeur ci-dessous, en remplaçant les espaces réservés pour YOUR_API_KEY et 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>
...

Dans la méthode initMap du fichier app.js, où la map constante est définie, annulez la mise en commentaire de la ligne de la propriété mapId et remplacez YOUR_MAP_ID_HERE par l'identifiant de carte que vous venez de créer :

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

Redémarrez votre serveur.

go run *.go

Après avoir actualisé l'aperçu, le style de votre choix doit avoir été appliqué à la carte. Voici un exemple d'utilisation du style JSON ci-dessus.

2ece59c64c06e9da.png

11. Déployer vers l'environnement de production

Si vous souhaitez que votre application s'exécute à partir d'AppEngine Flex (et pas seulement d'un serveur Web local sur votre ordinateur de développement/Cloud Shell, comme vous l'avez déjà fait), c'est très simple. Il suffit d'ajouter quelques éléments pour que l'accès à la base de données fonctionne dans l'environnement de production. Toutes ces informations sont décrites sur la page de documentation concernant la connexion d'App Engine Flex à Cloud SQL.

Ajouter des variables d'environnement au fichier App.yaml

Tout d'abord, toutes les variables d'environnement que vous utilisiez pour effectuer des tests en local doivent être ajoutées en bas du fichier app.yaml de votre application.

  1. Accédez à la page https://console.cloud.google.com/sql/instances/locations/overview pour connaître le nom de la connexion de l'instance.
  2. Collez le code suivant à la fin de app.yaml.
  3. Remplacez YOUR_DB_PASSWORD_HERE par le mot de passe que vous avez créé pour le nom d'utilisateur postgres.
  4. Remplacez YOUR_CONNECTION_NAME_HERE par la valeur obtenue à l'étape 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

Notez que le paramètre DB_TCP_HOST doit être associé à la valeur 172.17.0.1, car cette application se connecte via App Engine Flex**.** En effet, il va communiquer avec Cloud SQL via un proxy, comme vous l'avez fait.

Ajouter des autorisations de client SQL au compte de service App Engine Flex

Accédez à la page Administration IAM de Cloud Console, puis recherchez un compte de service dont le nom correspond au format service-PROJECT_NUMBER@gae-api-prod.google.com.iam.gserviceaccount.com. Il s'agit du compte de service qu'App Engine Flex utilisera pour se connecter à la base de données. Cliquez sur le bouton Modifier au bout de la ligne, puis ajoutez le rôle Client Cloud SQL.

b04ccc0b4022b905.png

Copier le code de votre projet dans le chemin d'accès Go

Pour qu'App Engine puisse exécuter votre code, il doit pouvoir trouver les fichiers appropriés dans le chemin Go. Assurez-vous que vous vous trouvez dans le répertoire racine de votre projet.

cd YOUR_PROJECT_ROOT

Copiez le répertoire dans le chemin d'accès Go.

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

Accédez à ce répertoire.

cd ~/gopath/src/austin-recycling

Déployer votre application

Utilisez l'outil gcloud pour déployer votre application. Cette opération prend un certain temps.

gcloud app deploy

Utilisez la commande browse pour obtenir un lien sur lequel vous pouvez cliquer afin de voir à l'œuvre votre magnifique outil professionnel de localisation de magasins complètement déployé.

gcloud app browse

Si vous exécutiez gcloud en dehors de Cloud Shell, l'exécution de gcloud app browse ouvre un nouvel onglet de navigateur.

12. (Recommandé) Effectuer un nettoyage

Cet atelier de programmation ne restera pas dans les limites de la version gratuite pour le traitement BigQuery et les appels de l'API Maps Platform. Toutefois, si vous l'avez suivi à des fins purement informatives et que vous ne souhaitez pas payer de frais à l'avenir, le moyen le plus simple de supprimer les ressources associées à ce projet est de supprimer le projet lui-même.

Supprimer le projet

Dans la console GCP, accédez à la page Cloud Resource Manager :

Dans la liste des projets, sélectionnez celui dans lequel vous avez travaillé, puis cliquez sur Supprimer. Vous serez alors invité à saisir l'ID du projet. Saisissez-le, puis cliquez sur Arrêter.

Vous pouvez également supprimer le projet dans son intégralité directement dans Cloud Shell avec gcloud en exécutant la commande suivante et en remplaçant l'espace réservé GOOGLE_CLOUD_PROJECT par l'identifiant de votre projet :

gcloud projects delete GOOGLE_CLOUD_PROJECT

13. Félicitations

Félicitations ! Vous avez terminé l'atelier de programmation !

Ou vous l'avez parcouru jusqu'à la dernière page. Félicitations ! Vous avez parcouru l'atelier jusqu'à la dernière page !

Au cours de cet atelier de programmation, vous avez travaillé avec les technologies suivantes :

Documentation complémentaire

Il reste encore beaucoup à apprendre sur ces technologies. Vous trouverez ci-dessous des liens utiles sur des sujets que nous n'avons pas eu le temps de couvrir dans cet atelier de programmation. Ils vous seront certainement utiles pour créer une solution de localisation de magasins adaptée à vos besoins.