Criar um localizador de lojas de pilha completa com a Plataforma Google Maps e o Google Cloud

1. Introdução

Resumo

Imagine que você tenha muitos lugares para colocar em um mapa e quer permitir que os usuários vejam onde esses lugares estão e decidam qual deles querem visitar. Como exemplos, podemos mencionar:

  • um localizador de lojas no site de um varejista;
  • um mapa dos locais de votação para uma eleição futura;
  • um diretório de locais especializados, como coletores de bateria para reciclagem.

O que você criará

Neste codelab, você criará um localizador que usa um feed de dados em tempo real de locais especializados e ajuda usuários a encontrar o local mais próximo do ponto de partida deles. Esse localizador de pilha completa pode trabalhar com um número muito maior de lugares do que o localizador de lojas simples, que é limitado a 25 locais.

2ece59c64c06e9da.png

O que você aprenderá

Esse codelab usa um conjunto de dados abertos para simular metadados pré-preenchidos sobre um grande número de lojas, permitindo que você se concentre em aprender os conceitos técnicos mais importantes.

  • API Maps JavaScript: exibe um número grande de locais em um mapa da Web personalizado.
  • GeoJSON: um formato que armazena metadados sobre locais.
  • Place Autocomplete: ajuda os usuários a fornecer os locais de partida de forma mais rápida e precisa.
  • Go: a linguagem de programação usada para desenvolver o back-end do aplicativo. O back-end vai interagir com o banco de dados e enviar os resultados da consulta ao front-end no JSON formatado.
  • App Engine: para hospedar o app da Web.

Pré-requisitos

  • Conhecimento básico de HTML e JavaScript
  • Uma Conta do Google

2. Começar a configuração

Na etapa 3 da seção a seguir, ative a API Maps JavaScript, a API Places e a API Distance Matrix para este codelab.

Primeiros passos com a Plataforma Google Maps

Se você nunca usou a Plataforma Google Maps, siga o guia Primeiros passos com a Plataforma Google Maps ou assista à playlist Primeiros passos na Plataforma Google Maps para concluir as seguintes etapas:

  1. Criar uma conta de faturamento
  2. Criar um projeto
  3. Ative as APIs e os SDKs da Plataforma Google Maps (listados na seção anterior).
  4. Gerar uma chave de API

Ativar o Cloud Shell

Neste codelab, você usa o Cloud Shell, um ambiente de linha de comando executado no Google Cloud que oferece acesso a produtos e recursos que funcionam no Google Cloud. Com isso, é possível hospedar e executar seu projeto completamente pelo navegador da Web.

Para ativar o Cloud Shell no Console do Cloud, clique em Ativar o Cloud Shell 89665d8d348105cd.png. O provisionamento e a conexão ao ambiente levarão apenas alguns instantes para serem concluídos.

5f504766b9b3be17.png

Isso abre um novo shell na parte inferior do navegador depois de, talvez, exibir um intersticial de introdução.

d3bb67d514893d1f.png

Confirmar seu projeto

Depois que você se conectar ao Cloud Shell, sua autenticação já terá sido feita, e o projeto estará definido com o ID selecionado durante a configuração.

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

Se, por algum motivo, o projeto não estiver definido, execute o seguinte comando:

gcloud config set project <YOUR_PROJECT_ID>

Ativar a API App Engine Flex

A API App Engine Flex precisa ser ativada manualmente no Console do Cloud. Com isso, não apenas a API será ativada, mas a conta de serviço do ambiente App Engine Flex também será criada, que é a conta autenticada que vai interagir com os Serviços do Google (como os bancos de dados SQL) em nome do usuário.

.

3. Hello World

Back-end: Hello World em Go

Para começar, acesse sua instância no Cloud Shell e crie um aplicativo App Engine Flex em Go, que servirá como base para o restante do codelab.

Na barra de ferramentas do Cloud Shell, clique no botão Abrir editor para abrir um editor de código em uma nova guia. Esse editor de código com base na Web permite que você edite arquivos com facilidade na instância do Cloud Shell.

b63f7baad67b6601.png

Em seguida, clique no ícone Abrir em uma nova janela para mover o editor e o terminal para uma nova guia.

3f6625ff8461c551.png

No terminal na parte inferior da nova guia, crie um diretório austin-recycling.

mkdir -p austin-recycling && cd $_

Depois, crie um pequeno aplicativo App Engine em Go para garantir que tudo está funcionando. Hello World!

O diretório austin-recycling também aparecerá na lista de pastas do editor à esquerda. No diretório austin-recycling, crie um arquivo chamado app.yaml. Coloque o seguinte conteúdo no arquivo app.yaml:

app.yaml

runtime: go
env: flex

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

Esse arquivo configura seu aplicativo App Engine para usar o ambiente de execução do Flex para Go. Para informações contextuais sobre o significado dos itens de configuração nesse arquivo, consulte a documentação do ambiente padrão do Google App Engine para Go.

Agora, crie um arquivo main.go para complementar o 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 a pena entender o que esse código faz, mesmo que você não se aprofunde. Você definiu um pacote main que inicializa um servidor HTTP que usa a porta 8080 e registra uma função de gerenciador para solicitações HTTP que correspondem ao caminho "/".

A função de gerenciador, chamada de handler, grava a string de texto "Hello, world!". Esse texto será redirecionado ao seu navegador, onde será possível ler o conteúdo. Nas próximas etapas, você criará gerenciadores que respondem com dados GeoJSON em vez de strings codificadas simples.

Depois de realizar essas etapas, seu editor terá a seguinte aparência:

2084fdd5ef594ece.png

Testar

Para testar o app, execute o servidor de desenvolvimento do App Engine na instância do Cloud Shell. Volte para a linha de comando do Cloud Shell e digite o seguinte:

go run *.go

Você verá algumas linhas de saída de registro mostrando que o servidor de desenvolvimento está sendo executado na instância do Cloud Shell e que o app da Web Hello World está usando a porta localhost 8080. Se quiser abrir uma guia do navegador da Web neste app, pressione o botão Visualização da Web e selecione o item de menu Visualizar na porta 8080 na barra de ferramentas do Cloud Shell.

4155fc1dc717ac67.png

Ao clicar nesse item de menu, uma nova guia é aberta no navegador da Web com as palavras "Hello World!" exibidas pelo servidor de desenvolvimento do App Engine.

Na próxima etapa, você adicionará ao app os dados de reciclagem de Austin e começará a visualizá-los.

4. Receber dados atuais

GeoJSON, a língua franca do mundo do SIG

Na etapa anterior, dissemos que você criará gerenciadores no código Go que renderizam dados GeoJSON no navegador da Web. Mas o que é GeoJSON?

No mundo do Sistema de Informações Geográficas (SIG), precisamos transmitir informações sobre entidades geográficas entre sistemas de computador. Humanos conseguem ler mapas muito bem, mas os computadores normalmente preferem dados em formatos que podem entender melhor.

O GeoJSON é um formato para codificação de estruturas de dados geográficos, como as coordenadas de locais de descarte para reciclagem em Austin, no Texas. Ele foi regulado na norma RFC7946 da Internet Engineering Task Force. O GeoJSON usa o formato JavaScript Object Notation (JSON), conforme definido na norma ECMA-404 pela mesma organização que normatizou o JavaScript, a Ecma International.

O importante é que o GeoJSON é um formato de transmissão amplamente compatível e usado para comunicar informações geográficas. Neste codelab, utilizamos o GeoJSON das seguintes maneiras:

  • Com pacotes em Go para analisar os dados de Austin em uma estrutura interna de dados de SIG que você usará para filtrar os dados solicitados
  • Na serialização dos dados solicitados para transmissão entre o servidor da Web e o navegador da Web
  • Com uma biblioteca JavaScript para transformar a resposta em marcadores em um mapa

Isso reduzirá bastante o tempo que você passa escrevendo código, já que não será preciso gravar analisadores e geradores para converter o fluxo de dados em representações na memória.

Recuperar os dados

O Open Data Portal de Austin, no Texas, disponibiliza informações geoespaciais sobre recursos públicos para uso da população. Neste codelab, você verá o conjunto de dados dos locais de descarte para reciclagem.

Você poderá ver os dados com marcadores no mapa, renderizados usando a camada de dados da API Maps JavaScript.

Para começar, faça o download dos dados GeoJSON do site de Austin no seu app.

  1. Na janela da linha de comando da instância do Cloud Shell, digite [CTRL] + [C] para desligar o servidor.
  2. Crie um diretório data dentro do diretório austin-recycling e faça a mudança nele:
mkdir -p data && cd data

Use o curl para recuperar os locais de descarte para reciclagem:

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

Por fim, mude de volta para o diretório pai.

cd ..

5. Mapear os locais

Atualize o arquivo app.yaml para representar o aplicativo mais robusto que você quer criar, em vez de "apenas mais um 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

Essa configuração do app.yaml direciona as solicitações de /, /*.js, /*.css e /*.html para um conjunto de arquivos estáticos. Isso significa que o componente HTML estático do seu app será veiculado diretamente pela infraestrutura de exibição de arquivos do App Engine, e não pelo seu aplicativo Go. Isso reduz a carga do servidor e acelera a veiculação.

Agora é hora de criar o back-end do seu aplicativo em Go.

Criar o back-end

Algo interessante que você pode ter notado é que seu arquivo app.yaml não expõe o arquivo GeoJSON. Isso ocorre porque o GeoJSON é processado e enviado pelo back-end em Go, o que nos permite criar recursos sofisticados nas etapas seguintes. Mude seu arquivo main.go para o seguinte:

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

O back-end em Go já oferece um recurso valioso: a instância do App Engine começa a armazenar todos os locais em cache assim que é iniciada. Isso economiza tempo, já que o back-end não precisará ler o arquivo do disco rígido toda vez que um usuário fizer uma atualização.

Criar o front-end

O primeiro passo é criar uma pasta para armazenar todos os nossos recursos estáticos. Na pasta pai do projeto, crie uma pasta static.

mkdir -p static && cd static

Vamos criar três arquivos nesta pasta.

  • index.html, que incluirá todo o HTML do seu app localizador de lojas de uma página.
  • style.css, que, como esperado, incluirá o estilo.
  • app.js, que buscará o GeoJSON, fará chamadas para a API Maps e colocará marcadores no seu mapa personalizado.

Crie esses três arquivos em 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>

Preste bastante atenção ao URL src na tag de script do elemento head.

  • Substitua o texto do marcador YOUR_API_KEY pela chave de API que você gerou durante a configuração. Se precisar recuperar sua chave de API ou gerar uma nova, acesse a página "APIs e Serviços" -> Credenciais no Console do Cloud.
  • O URL contém o parâmetro callback=initialize. Agora, vamos criar o arquivo JavaScript que inclui essa função de callback. É aqui que seu aplicativo carregará os locais do back-end, enviará à API Maps e usará o resultado para marcar locais personalizados no mapa e renderizá-los na sua página da Web.
  • O parâmetro libraries=places carrega a biblioteca do Places, que é necessária para certos recursos, como o preenchimento automático de endereços, que serão adicionados mais tarde.

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

Esse código renderiza os locais das lojas em um mapa. Para testar o que temos até agora, vá até a linha de comando e volte para o diretório pai:

cd ..

Agora, execute novamente o app no modo de desenvolvimento usando:

go run *.go

Visualize o resultado como você fazia antes. Surgirá um mapa com pequenos círculos verdes, como mostrado abaixo.

58a6680e9c8e7396.png

Isso significa que você já está renderizando locais no mapa, e olha que estamos apenas na metade do codelab. Isso é incrível. Agora vamos adicionar um pouco de interatividade.

6. Mostrar detalhes sob demanda

Responder a eventos de clique nos marcadores do mapa

Exibir marcadores no mapa é um ótimo começo, mas o necessário mesmo é que um visitante possa clicar em um desses marcadores e ver informações sobre o respectivo local, como o nome ou endereço da empresa, entre outras coisas. A janela pequena com informações que geralmente aparece quando você clica em um marcador do Google Maps é chamada de janela de informações.

Crie um objeto InfoWindow. Adicione o seguinte à função initialize, substituindo a linha comentada que traz "// TODO: Initialize an info window".

app.js: inicializar

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

Substitua a definição da função fetchAndRenderStores por esta versão um pouco diferente, com uma mudança feita na linha final para chamar storeToCircle com outro argumento, 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));
};

Substitua a definição storeToCircle por esta versão um pouco mais longa, que agora usa uma janela de informações como terceiro argumento:

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

O novo código acima exibe um objeto infoWindow com as informações da loja selecionada sempre que alguém clica no marcador da loja no mapa.

Pare e reinicie seu servidor se ele ainda estiver em execução. Atualize a página do mapa e clique em um marcador. Uma pequena janela de informações, com a seguinte aparência, será exibida com o nome e o endereço da empresa:

1af0ab72ad0eadc5.png

7. Saber o local de partida do usuário

Os usuários dos localizadores de lojas geralmente querem saber que loja está mais próxima ou o endereço de onde planejam iniciar o trajeto. Adicione uma barra de pesquisa do Place Autocomplete para que o usuário possa inserir de forma fácil um endereço de partida. O Place Autocomplete funciona de maneira semelhante ao Autocomplete em outras barras de pesquisa do Google, mas as estimativas vêm do Places na Plataforma Google Maps.

Criar um campo de entrada do usuário

Volte para a edição do style.css e adicione estilo à barra de pesquisa do Autocomplete e ao painel lateral de resultados. Embora estejamos atualizando os estilos de CSS, vamos adicionar também estilos a uma barra lateral que exibe informações de lojas como uma lista para complementar o mapa.

Adicione este código ao fim do arquivo.

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

A barra de pesquisa do Autocomplete e o painel deslizante ficam ocultos, conforme a necessidade.

Substitua o comentário em index.html que traz ""<!-- Autocomplete div goes here -->" pelo código a seguir se quiser preparar um div aplicável para o widget do Autocomplete. Além desta edição, vamos adicionar também o div ao painel deslizante.

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>

Agora, defina uma função para colocar o widget do Autocomplete no mapa adicionando o seguinte código ao fim do 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
};

O código restringe as sugestões do Autocomplete para retornar apenas endereços porque o Place Autocomplete também pode corresponder nomes de estabelecimentos e locais administrativos e retorna apenas endereços dos EUA. Adicionar essas especificações opcionais reduzirá o número de caracteres que o usuário precisa inserir para limitar as estimativas e mostrar o endereço que ele está procurando.

Em seguida, o div do Autocomplete que você criou é movido para o canto superior direito do mapa, e os campos que precisam ser retornados sobre cada lugar são especificados.

Para concluir, chame a função initAutocompleteWidget no fim da função initialize, substituindo o comentário "// TODO: Initialize the Autocomplete widget".

app.js: inicializar

 // Initialize the Places Autocomplete Widget
 initAutocompleteWidget();

Reinicie o servidor executando o comando abaixo e, depois, atualize a visualização.

go run *.go

Você verá um widget do Autocomplete no canto superior direito do mapa, que mostra os endereços dos EUA que correspondem ao valores digitados., aproximando-se à área visível do mapa.

58e9bbbcc4bf18d1.png

Atualizar o mapa quando o usuário selecionar um endereço de partida

Agora você precisa definir quando o usuário seleciona uma estimativa do widget do Autocomplete e usa esse local como base para calcular as distâncias até suas lojas.

Adicione o seguinte código no fim de initAutocompleteWidget no app.js, substituindo o comentário "// 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
  });

O código adiciona um listener para que, quando o usuário clicar em uma das sugestões, o mapa seja atualizado novamente no endereço selecionado e defina a origem como a base dos cálculos de distância. Você implementará esses cálculos mais para frente.

Pare e reinicie o servidor e atualize a visualização. O mapa será atualizado depois de você colocar um endereço na barra de pesquisa do Autocomplete.

8. Escalonar com o Cloud SQL

Até o momento, temos um ótimo localizador de lojas. Ele aproveita que há aproximadamente apenas 100 locais que o app usará, carregando-os na memória do back-end em vez de ler as informações do arquivo repetidamente. Mas e se o localizador precisar operar em uma escala diferente? Se você tem centenas de locais espalhados por uma grande área geográfica (ou milhares, em todo o mundo), não é mais uma boa ideia guardar todos esses locais na memória, e a divisão de zonas em arquivos individuais pode gerar problemas.

É hora de carregar seus locais de um banco de dados. Nesta etapa, migraremos todos os locais do seu arquivo GeoJSON para um banco de dados do Cloud SQL e atualizaremos o back-end em Go para extrair os resultados do banco de dados em vez do cache local sempre que uma solicitação chegar.

Criar uma instância do Cloud SQL com um banco de dados PostGres

É possível criar uma instância do Cloud SQL usando o Console do Google Cloud, mas é ainda mais fácil usar o utilitário gcloud para criar uma instância na linha de comando. No Cloud Shell, crie uma instância do Cloud SQL com o seguinte comando:

gcloud sql instances create locations \
--database-version=POSTGRES_12 \
--tier=db-custom-1-3840 --region=us-central1
  • O argumento locations é o nome que escolhemos para essa instância do Cloud SQL.
  • A sinalização tier é uma forma de selecionar, de maneira simples, algumas máquinas predefinidas.
  • O valor db-custom-1-3840 indica que a instância que está sendo criada precisa ter uma vCPU e cerca de 3,75 GB de memória.

A instância do Cloud SQL será criada e inicializada com um banco de dados PostGresSQL e o usuário padrão postgres. Qual é a senha deste usuário? Boa pergunta. Não há uma senha. É preciso definir uma senha antes de fazer login.

Faça isso com o seguinte comando:

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

Em seguida, digite a senha definida quando solicitado.

Ativar a extensão PostGIS

O PostGIS é uma extensão do PostGresSQL que facilita o armazenamento de tipos padronizados de dados geoespaciais. Em circunstâncias normais, seria preciso executar todo um processo de instalação para adicionar o PostGIS ao nosso banco de dados. Felizmente, ele é uma das extensões compatíveis do Cloud SQL para PostGresSQL.

Conecte-se à instância do banco de dados fazendo login como o usuário postgres, utilizando o seguinte comando no terminal do Cloud Shell:

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

Digite a senha que você acabou de criar. Agora, adicione a extensão PostGIS ao prompt de comando postgres=>.

CREATE EXTENSION postgis;

Se isso funcionar, o resultado será CREATE EXTENSION, conforme mostrado abaixo.

Exemplo de resposta ao comando

CREATE EXTENSION

Para terminar, saia da conexão do banco de dados inserindo o respectivo comando no prompt de comando postgres=>.

\q

Importar dados geográficos para o banco de dados

Agora é preciso importar todos esses dados de local dos arquivos GeoJSON para o novo banco de dados.

Felizmente, este problema já foi enfrentado várias vezes, e há várias ferramentas na Internet que automatizam isso para você. Usaremos uma ferramenta chamada ogr2ogr, que converte vários formatos comuns para o armazenamento de dados geoespaciais. Entre essas opções, está a conversão do formulário GeoJSON em um arquivo dump SQL. Você também pode usar o arquivo dump SQL para criar suas tabelas e colunas para o banco de dados e carregá-lo com todos os dados dos seus arquivos GeoJSON.

Criar arquivo dump SQL

Instale o og2ogr.

sudo apt-get install gdal-bin

Depois, use o ogr2ogr para criar o arquivo dump SQL. Esse arquivo criará uma tabela chamada austinrecycling.

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

O comando acima depende da execução da pasta austin-recycling. Se for necessário executá-lo em outro diretório, substitua data pelo caminho para o diretório em que recycling-locations.geojson está armazenado.

Preencher seu banco de dados com os locais de descarte para reciclagem

Após a conclusão do último comando, haverá um arquivo datadump.sql, no mesmo diretório em que o comando foi executado. Este arquivo, de pouco mais 100 linhas de SQL, cria uma tabela austinrecycling e a preenche com os locais.

Faça uma conexão com o banco de dados e execute o script usando o comando a seguir:

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

Se o script for executado corretamente, as linhas de saída serão assim:

Exemplo de resposta ao comando

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

Atualizar o back-end em Go para usar o Cloud SQL

Com todos esses dados no banco de dados, é hora de atualizar nosso código.

Atualizar o front-end para enviar informações do local

Vamos começar com uma atualização bem pequena no front-end. Como estamos codificando esse app para uma escala em que nem todos os locais serão exibidos no front-end sempre que a consulta for realizada, precisamos transmitir algumas informações básicas do front-end sobre o local de interesse do usuário.

Abra app.js e substitua a definição da função fetchStores por esta versão para incluir a latitude e a longitude que interessam no 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();
};

Depois de concluir essa etapa do codelab, a resposta trará apenas as lojas mais próximas das coordenadas do mapa fornecidas no parâmetro center. Para a busca inicial na função initialize, o exemplo de código fornecido neste codelab usa as coordenadas centrais de Austin, no Texas.

Como fetchStores agora só trará um subconjunto dos locais de lojas, será preciso buscar novamente as lojas sempre que o usuário mudar o local de partida.

Modifique a função initAutocompleteWidget para atualizar os locais sempre que uma nova origem for definida. Para isso, faça duas edições:

  1. No initAutocompleteWidget, encontre a callback para o listener place_changed. Remova o comentário da linha que limpa os círculos para que ela seja executada sempre que o usuário selecionar um endereço na barra de pesquisa do Place Autocomplete.

app.js: initAutocompleteWidget

  autocomplete.addListener("place_changed", async () => {
    circles.forEach((c) => c.setMap(null)); // clear existing stores
    // ...
  1. Sempre que a origem selecionada for alterada, a variável originLocation será atualizada. No fim da callback "place_changed", remova a marca de comentário da linha acima da linha "// TODO: Calculate the closest stores" para transmitir essa nova origem a uma nova chamada para a função fetchAndRenderStores.

app.js: initAutocompleteWidget

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

Atualizar o back-end para usar o CloudSQL em vez de um arquivo JSON simples

Remover a leitura e o armazenamento em cache do arquivo GeoJSON simples

Primeiro, altere main.go para remover o código que carrega e armazena em cache o arquivo GeoJSON simples. Também podemos eliminar a função dropoffsHandler porque vamos criar outra função usando o Cloud SQL em um arquivo diferente.

Seu novo main.go será muito mais curto.

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

Criar um gerenciador para solicitações de localização

Agora, vamos criar outro arquivo, locations.go, também no diretório de reciclagem de Austin. Comece implementando novamente o gerenciador para solicitações de localização.

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

O gerenciador executa as seguintes tarefas importantes:

  • Extrai a latitude e a longitude do objeto de solicitação, que são informações já adicionadas ao URL anteriormente.
  • Dispara a chamada getGeoJsonFromDatabase, que traz uma string GeoJSON. Vamos gravá-la mais tarde.
  • Usa ResponseWriter para imprimir essa string GeoJSON na resposta.

O próximo passo é criar um pool de conexões para ajudar o escalonamento da utilização do banco de dados com usuários simultâneos.

Criar um pool de conexões

Um pool de conexões é uma coleção de conexões ativas de bancos de dados que o servidor pode reutilizar para atender às solicitações dos usuários. Isso reduz bastante a sobrecarga gerada pelo aumento nos usuários ativos, já que o servidor não precisa perder tempo criando e destruindo conexões para cada um deles. Na seção anterior, importamos a biblioteca github.com/jackc/pgx/stdlib.. Esta é uma biblioteca comum para trabalhar com pools de conexão em Go.

No fim do locations.go, crie uma função initConnectionPool (chamada main.go) para inicializar um pool de conexões. Vale dizer que alguns métodos auxiliares são usados neste snippet. O configureConnectionPool é útil para ajustar as configurações do pool, como o número de conexões e vida útil por conexão. O mustGetEnv agrupa as chamadas para obter as variáveis de ambiente necessárias e, assim, exibir mensagens de erro úteis se a instância não tiver informações importantes, como o IP ou o nome do banco de dados a que se conectar.

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
}

Consultar o banco de dados para procurar locais e receber o JSON como retorno

Agora vamos gravar uma consulta no banco de dados que usa coordenadas do mapa e retorna os 25 locais mais próximos. Além disso, graças a alguns recursos avançados do banco de dados, ele retornará esses dados no formato GeoJSON. O resultado final desse processo é que, até onde mostra o código do front-end, não houve mudanças. Antes, era disparada uma solicitação para um URL, que recebia como resposta um número muito grande de arquivos GeoJSON. Agora, dispara-se uma solicitação para um URL, e o retorno são, também, muitos arquivos desse tipo.

Esta é a função que executa todos esses processos. Adicione a seguinte função depois do código do pool de gerenciadores e conexões que você acabou de gravar na parte inferior do 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
}

Essa função praticamente configura, retira e trata dos erros para disparar uma solicitação ao banco de dados. Vejamos o SQL real, que está fazendo muitas coisas interessantes na camada do banco de dados para que você não precise se preocupar com a implementação de qualquer uma delas por código.

A pesquisa bruta que é acionada após a análise da string e a inserção de todos os literais de string nos locais apropriados é semelhante a esta:

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

Essa consulta pode ser visualizada como uma consulta principal, além de algumas funções de união do JSON.

SELECT * ... LIMIT 25 seleciona todos os campos para cada local. Em seguida, ele usa a função ST_DISTANCE (parte do pacote de funções de medição geográfica do PostGIS) para determinar a distância entre cada local no banco de dados e o par de latitude/longitude do lugar que o usuário informou no front-end. Lembre-se de que, diferentemente da matriz de distância, que pode informar uma distância de carro, estas distâncias são geoespaciais. Para mais eficiência, essa distância é usada para classificar e retornar os 25 locais mais próximos à localização especificada pelo usuário.

**SELECT json_build_object(‘type', ‘F**eature') encapsula a consulta anterior, usando os resultados dela para criar um objeto de recurso do GeoJSON. De maneira curiosa, essa consulta também faz uso do raio máximo: "16090" é o número de metros em 16 quilômetros, o limite absoluto especificado pelo back-end em Go. Se você estiver se perguntando por que essa cláusula WHERE não foi adicionada à consulta interna, em que a distância de cada local é determinada, isso ocorreu devido à execução do SQL nos bastidores, o que faz com que esse campo talvez não tenha sido calculado quando a cláusula WHERE foi examinada. Na verdade, se você tentar mover essa cláusula WHERE para a consulta interna, um erro será gerado.

**SELECT json_build_object(‘type', ‘FeatureColl**ection'). Essa consulta une todas as linhas resultantes da consulta de geração do JSON em um objeto FeatureCollection do GeoJSON.

Adicionar a biblioteca PGX ao seu projeto

Precisamos adicionar ao seu projeto o Driver e Toolkit do PostGres, uma dependência que permite a criação do pool de conexões. A maneira mais fácil de fazer isso é usando os módulos Go. Inicialize um módulo com este comando no Cloud Shell:

go mod init my_locator

Em seguida, execute esse comando para buscar dependências no código, adicionar uma lista de dependências ao arquivo de modificação e fazer o download delas.

go mod tidy

Por fim, execute o comando a seguir para colocar as dependências diretamente no diretório do seu projeto, de modo que o contêiner possa ser criado com facilidade para a App Engine Flex.

go mod vendor

Tudo pronto para o teste.

Testar

Fizemos bastante coisa até aqui. Agora vamos ver como isso funciona na prática.

Para que sua máquina de desenvolvimento, até mesmo o Cloud Shell, possa se conectar ao banco de dados, precisaremos usar o proxy do Cloud SQL no gerenciamento da conexão do banco de dados. Para configurar o proxy do Cloud SQL, faça o seguinte:

  1. Clique aqui para ativar a API Cloud SQL Admin.
  2. Se você estiver em uma máquina de desenvolvimento local, instale a ferramenta de proxy do Cloud SQL. Se você estiver usando o Cloud Shell, ele já está instalado, e você pode ignorar esta etapa. As instruções serão relevantes para contas de serviço. Uma conta dessas já foi criada para você, e vamos falar sobre a adição das permissões necessárias a essa conta na seção seguinte.
  3. No Cloud Shell ou seu próprio terminal, crie uma guia para iniciar o proxy.

bcca42933bfbd497.png

  1. Acesse https://console.cloud.google.com/sql/instances/locations/overview e role a tela para baixo até encontrar o campo Nome da conexão. Copie esse nome para usar no próximo comando.
  2. Nessa guia, execute o proxy do Cloud SQL com este comando, substituindo CONNECTION_NAME pelo nome da conexão mostrado na etapa anterior.
cloud_sql_proxy -instances=CONNECTION_NAME=tcp:5432

Volte à primeira guia do Cloud Shell e defina as variáveis de ambiente que a linguagem Go precisará para se comunicar com o back-end do banco de dados. Em seguida, execute o servidor como você fez antes:

Se ainda não tiver feito isso, vá até o diretório raiz do projeto.

cd YOUR_PROJECT_ROOT

Crie as cinco variáveis de ambiente a seguir, substituindo YOUR_PASSWORD_HERE pela senha que você definiu acima.

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

Execute sua instância local.

go run *.go

Abra a janela de visualização, que deve funcionar como se nada tivesse mudado. Você pode inserir um endereço de partida, aplicar zoom no mapa e clicar nos locais de descarte para reciclagem. A diferença é que agora há o suporte de um banco de dados, e a estrutura está preparada para ser escalonada.

9. Listar as lojas mais próximas

A API Directions funciona como a experiência de solicitar rotas no app Google Maps. Insira uma única origem e um único destino para receber um trajeto entre os dois. A API Distance Matrix usa esse conceito para identificar os pares ideais entre várias origens e destinos possíveis com base nos tempos e distâncias do trajeto. Nesse caso, para ajudar o usuário a encontrar a loja mais próxima ao endereço selecionado, indique uma origem e uma matriz de locais de lojas como destino.

Adicionar a distância da origem de cada loja

No início da definição da função initMap, substitua o comentário "// TODO: Start Distance Matrix service" pelo seguinte código:

app.js: initMap

distanceMatrixService = new google.maps.DistanceMatrixService();

Adicione uma nova função no fim do app.js chamada 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);
  });
};

A função ativa a API Distance Matrix usando a origem que recebeu como uma origem simples e os locais da loja como uma matriz de destinos. Em seguida, ela cria uma matriz de objetos que armazena o ID da loja, a distância expressa em uma string legível e a distância em metros como um valor numérico. Depois, ela classifica a matriz.

Atualize a função initAutocompleteWidget para calcular as distâncias das lojas sempre que uma nova origem for selecionada na barra de pesquisa do Place Autocomplete. Na parte inferior da função initAutocompleteWidget, substitua o comentário "// TODO: Calculate the closest stores" pelo seguinte código:

app.js: initAutocompleteWidget

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

Exibir uma visualização em lista das lojas por distância

O usuário quer ver uma lista de lojas ordenadas da mais próxima à mais distante. Preencha uma lista de painel lateral para cada loja usando aquela retornada da função calculateDistances para informar a ordem de exibição das lojas.

Adicione as duas novas funções renderStoresPanel() e storeToPanelRow() ao fim do app.js.

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

Reinicie o servidor e atualize a visualização executando o seguinte comando:

go run *.go

Por fim, digite um endereço de Austin, no Texas, na barra de pesquisa do Autocomplete e clique em uma das sugestões.

O mapa terá esse endereço como centro, e uma barra lateral aparecerá listando as localizações de lojas em ordem de distância do endereço selecionado. Veja um exemplo:

96e35794dd0e88c9.png

10. Personalizar o estilo do mapa

Adicione estilo ao seu mapa para destacá-lo de forma impactante. Com essa funcionalidade, você pode personalizar seus mapas no Console do Cloud usando estilos de mapa baseados na nuvem (Beta). Se quiser aplicar um estilo com um recurso que não esteja em versão Beta, use a documentação de estilo do mapa para gerar o json e estilizar o mapa programaticamente. Siga as instruções abaixo para definir estilos de mapa baseados na nuvem (Beta).

Criar um ID de mapa

Primeiro, abra o Console do Cloud e digite "Gerenciamento de mapas" na caixa de pesquisa". Clique no resultado "Gerenciamento de mapas (Google Maps)". 64036dd0ed200200.png

Você verá um botão perto da parte superior da página, logo abaixo da caixa de pesquisa, com o texto Criar ID do mapa. Clique nele e preencha o nome que você escolher. Em "Tipo de mapa", selecione JavaScript e, quando mais opções aparecerem, selecione Vetor na lista. O resultado final será parecido com a imagem abaixo.

70f55a759b4c4212.png

Clique em "Avançar" e você verá seu novo ID de mapa. Você pode copiar o texto agora se quiser, mas não se preocupe. Não será difícil procurá-lo mais tarde.

A próxima etapa é a criação de um estilo para aplicar ao mapa.

Criar um estilo de mapa

Se você ainda estiver na seção "Mapas" do Console do Cloud, clique em "Mapear estilos na parte inferior do menu de navegação à esquerda. Caso contrário, assim como funciona ao criar um ID de mapa, você pode encontrar a página certa digitando "Estilos de mapa" na caixa de pesquisa e selecionando "Estilos de mapa (Google Maps)" nos resultados, como na imagem abaixo.

9284cd200f1a9223.png

Em seguida, clique no botão "+ Criar novo estilo de mapa" na parte superior.

  1. Se você quiser um estilo igual ao do mapa mostrado neste codelab, clique na guia "IMPORTAR JSON" e cole o blob JSON abaixo. Caso você opte por criar algo que criou, selecione o estilo do mapa a ser usado. Depois, clique em Avançar.
  2. Selecione o ID do mapa que você acabou de criar para associá-lo a este estilo e clique em Avançar mais uma vez.
  3. Neste ponto, você terá a opção de personalizar ainda mais o que já fez até agora. Se quiser tentar outro recurso, clique em Personalizar no editor de estilo e teste as cores e opções até escolher o que mais lhe agrada. Se não quiser fazer isso, Pular.
  4. Na próxima etapa, insira o nome e a descrição do estilo e clique em Salvar e publicar.

Veja um blob JSON opcional que pode ser importado na primeira etapa.

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

Adicionar o ID do mapa ao seu código

Depois de criar um estilo, o que fazer para realmente usá-lo no seu mapa? Você precisa considerar o seguinte:

  1. Adicione o ID do mapa como um parâmetro de URL à tag de script em index.html.
  2. Add é o ID do mapa como um argumento do construtor quando você cria o mapa no método initMap().

Substitua a tag do script que carrega a API Maps JavaScript no arquivo HTML com o URL do carregador abaixo, substituindo os marcadores por "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>
...

No método initMap de app.js, em que a constante map está definida, retire a marca de comentário da linha da propriedade mapId e substitua "YOUR_MAP_ID_HERE" pelo ID do mapa que você acabou de criar:

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

Reinicie o servidor.

go run *.go

Após atualizar a visualização, o mapa aparecerá com o estilo definido conforme suas preferências. Veja um exemplo com o estilo JSON acima.

2ece59c64c06e9da.png

11. Implantar para a produção

Se quiser ver seu app em execução na App Engine Flex, e não apenas em um servidor da web local na sua máquina de desenvolvimento ou no Cloud Shell, que é o que você tem feito, isso é muito fácil. Basta adicionar alguns itens para que o acesso ao banco de dados funcione no ambiente de produção. Isso está descrito na página de documentação Como se conectar no Cloud SQL usando a App Engine Flex.

Adicionar variáveis de ambiente ao app.yaml

Primeiro, todas as variáveis de ambiente que você estava usando para testar localmente precisam ser adicionadas à parte inferior do arquivo app.yaml do app.

  1. Acesse https://console.cloud.google.com/sql/instances/locations/overview para procurar o nome da conexão da instância.
  2. Cole o seguinte código no fim de app.yaml.
  3. Substitua YOUR_DB_PASSWORD_HERE pela senha que você criou anteriormente para o nome de usuário postgres.
  4. Substitua YOUR_CONNECTION_NAME_HERE pelo valor da etapa 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

O DB_TCP_HOST precisa ter o valor 172.17.0.1, já que esse app se conecta por meio da App Engine Flex**.** O motivo é que o app se comunicará com o Cloud SQL usando um proxy, similar ao que você já fazia.

Adicionar permissões do cliente SQL à conta de serviço da App Engine Flex

Acesse a página IAM-Admin no Console do Cloud e procure uma conta de serviço com um nome que corresponda ao formato service-PROJECT_NUMBER@gae-api-prod.google.com.iam.gserviceaccount.com. Essa é a conta de serviço que a App Engine Flex usará para se conectar ao banco de dados. Clique no botão Editar no fim da linha e adicione a função "Cliente do Cloud SQL".

b04ccc0b4022b905.png

Copiar o código do projeto para o caminho da programação em Go

Para executar seu código, a App Engine precisa encontrar arquivos relevantes no caminho em Go. Verifique se você está no diretório raiz do projeto.

cd YOUR_PROJECT_ROOT

Copie o diretório para o caminho da programação em Go.

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

Altere para este diretório.

cd ~/gopath/src/austin-recycling

Implantar seu app

Use a ferramenta gcloud para implantar o app. A implantação levará algum tempo.

gcloud app deploy

Use o comando browse para conseguir um link em que você pode clicar e ver seu belo localizador de lojas, de nível empresarial, totalmente implantado e funcional.

gcloud app browse

Se você estivesse executando gcloud fora do Cloud Shell, a execução de gcloud app browse abriria uma nova guia no navegador.

12. (Recomendado) Limpeza

A execução deste codelab permanecerá nos limites de nível gratuito para o processamento do BigQuery e as chamadas da API Maps Platform, mas se você fizer isso apenas como um exercício de aprendizado, para não incorrer em cobranças futuras, a maneira mais fácil de excluir os recursos associados a este projeto é excluir o projeto.

Excluir o projeto

No Console do GCP, acesse a página Cloud Resource Manager:

Na lista de projetos, selecione o projeto em que estamos trabalhando e clique em Excluir. Você precisará digitar o ID do projeto. Depois de fazer isso, clique em Desligar.

Outra opção é excluir todo o projeto diretamente no Cloud Shell com gcloud, executando, para isso, o seguinte comando e substituindo o marcador GOOGLE_CLOUD_PROJECT pelo ID do seu projeto:

gcloud projects delete GOOGLE_CLOUD_PROJECT

13. Parabéns

Parabéns! Você concluiu o codelab.

(Ou fez uma leitura rápida até chegar ao fim.) Parabéns! Você fez uma leitura rápida até chegar ao fim.

Neste codelab, você trabalhou com as seguintes tecnologias:

Leitura complementar

Ainda há muito a aprender sobre essas tecnologias. Veja abaixo alguns links úteis sobre assuntos que não tivemos tempo de falar neste codelab, mas que podem ser úteis quando você for criar uma solução de localizador de lojas que atenda às suas necessidades específicas.