Создайте полноценный локатор магазинов с помощью Google Maps Platform и Google Cloud.

1. Введение

Абстрактный

Представьте, что у вас есть много мест, которые нужно нанести на карту, и вы хотите, чтобы пользователи могли видеть, где находятся эти места, и определять, какое место они хотят посетить. Общие примеры этого включают:

  • локатор магазина на сайте ритейлера
  • карта избирательных участков для предстоящих выборов
  • каталог специализированных мест, таких как емкости для утилизации аккумуляторов

Что вы будете строить

В этой лаборатории кода вы создадите локатор, который будет использовать поток данных в реальном времени для специализированных местоположений и поможет пользователю найти место, ближайшее к его отправной точке. Этот комплексный локатор может обрабатывать гораздо большее количество мест, чем простой локатор магазинов , который ограничен 25 или менее магазинами.

2ece59c64c06e9da.png

Что вы узнаете

В этой лаборатории кода используется открытый набор данных для имитации предварительно заполненных метаданных о большом количестве магазинов, чтобы вы могли сосредоточиться на изучении ключевых технических концепций.

  • Maps JavaScript API: отображение большого количества местоположений на настраиваемой веб-карте.
  • GeoJSON: формат, в котором хранятся метаданные о местоположении.
  • Автозаполнение мест: помогите пользователям указывать начальные местоположения быстрее и точнее
  • Go: язык программирования, используемый для разработки серверной части приложения. Серверная часть будет взаимодействовать с базой данных и отправлять результаты запроса во внешний интерфейс в формате JSON.
  • App Engine: для размещения веб-приложения

Предпосылки

  • Базовые знания HTML и JavaScript
  • Аккаунт Google

2. Настройте

На шаге 3 следующего раздела включите Maps JavaScript API , Places API и Distance Matrix API для этой лаборатории кода.

Начало работы с платформой Google Карт

Если вы еще не использовали платформу Google Maps, следуйте руководству по началу работы с платформой Google Maps или просмотрите список воспроизведения Начало работы с платформой Google Maps , чтобы выполнить следующие шаги:

  1. Создайте платежный аккаунт.
  2. Создайте проект.
  3. Включите API и SDK платформы Google Карт (перечислены в предыдущем разделе).
  4. Сгенерируйте API-ключ.

Активировать облачную оболочку

В этой лаборатории кода вы используете Cloud Shell , среду командной строки, работающую в Google Cloud, которая обеспечивает доступ к продуктам и ресурсам, работающим в Google Cloud, так что вы можете размещать и запускать свой проект полностью из своего веб-браузера.

Чтобы активировать Cloud Shell из Cloud Console, нажмите « Активировать Cloud Shell ». 89665d8d348105cd.png (подготовка и подключение к среде займет всего несколько минут).

5f504766b9b3be17.png

Это открывает новую оболочку в нижней части вашего браузера после возможного показа вступительного межстраничного объявления.

d3bb67d514893d1f.png

Подтвердите свой проект

После подключения к Cloud Shell вы должны увидеть, что вы уже прошли аутентификацию и что для проекта уже задан идентификатор проекта, выбранный вами во время установки.

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

Если по какой-то причине проект не установлен, выполните следующую команду:

gcloud config set project <YOUR_PROJECT_ID>

Включить Flex API AppEngine

API AppEngine Flex необходимо включить вручную из облачной консоли. Это не только активирует API, но и создаст учетную запись AppEngine Flexible Environment Service Account , аутентифицированную учетную запись, которая будет взаимодействовать со службами Google (например, с базами данных SQL) от имени пользователя.

3. Привет, мир

Бэкенд: Hello World в Go

В своем экземпляре Cloud Shell вы начнете с создания приложения Go App Engine Flex, которое будет служить основой для остальной части лаборатории кода.

На панели инструментов Cloud Shell нажмите кнопку « Открыть редактор », чтобы открыть редактор кода в новой вкладке. Этот веб-редактор кода позволяет легко редактировать файлы в экземпляре Cloud Shell.

b63f7baad67b6601.png

Затем щелкните значок « Открыть в новом окне », чтобы переместить редактор и терминал на новую вкладку.

3f6625ff8461c551.png

В терминале внизу новой вкладки создайте новый каталог austin-recycling .

mkdir -p austin-recycling && cd $_

Далее вы создадите небольшое приложение Go App Engine, чтобы убедиться, что все работает. Привет, мир!

Каталог austin-recycling также должен появиться в списке папок редактора слева. В каталоге austin-recycling создайте файл с именем app.yaml . Поместите следующее содержимое в файл app.yaml :

приложение.yaml

runtime: go
env: flex

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

Этот файл конфигурации настраивает приложение App Engine для использования среды выполнения Go Flex. Дополнительные сведения о значении элементов конфигурации в этом файле см. в документации по стандартной среде Google App Engine Go .

Затем создайте файл main.go вместе с файлом 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!")
}

Здесь стоит сделать паузу, чтобы понять, что делает этот код, по крайней мере, на высоком уровне. Вы определили пакет main , который запускает http-сервер, прослушивающий порт 8080, и регистрирует функцию обработчика HTTP-запросов, соответствующих пути "/" .

Функция-обработчик, удобно называемая handler , записывает текстовую строку "Hello, world!" . Этот текст будет передан обратно в ваш браузер, где вы сможете его прочитать. В следующих шагах вы создадите обработчики, которые отвечают данными GeoJSON вместо простых жестко закодированных строк.

После выполнения этих шагов у вас должен появиться редактор, который выглядит следующим образом:

2084fdd5ef594ece.png

Проверьте это

Чтобы протестировать это приложение, вы можете запустить сервер разработки App Engine внутри экземпляра Cloud Shell. Вернитесь в командную строку Cloud Shell и введите следующее:

go run *.go

Вы увидите несколько строк выходных данных журнала, показывающих, что вы действительно используете сервер разработки в экземпляре Cloud Shell, а веб-приложение hello world прослушивает локальный порт 8080. Вы можете открыть вкладку веб-браузера в этом приложении, нажав кнопку Интернет . Кнопка « Предварительный просмотр » и выбор пункта меню « Предварительный просмотр на порту 8080» на панели инструментов Cloud Shell.

4155fc1dc717ac67.png

При нажатии на этот пункт меню откроется новая вкладка в вашем веб-браузере со словами «Привет, мир!» обслуживается сервером разработки App Engine.

На следующем шаге вы добавите в это приложение данные о переработке города Остин и начнете визуализировать их.

4. Получить текущие данные

GeoJSON, лингва-франка в мире ГИС

На предыдущем шаге упоминалось, что вы создадите обработчики в своем коде Go, которые отображают данные GeoJSON в веб-браузере. Но что такое GeoJSON?

В мире географических информационных систем (ГИС) нам необходимо иметь возможность передавать знания о географических объектах между компьютерными системами. Карты отлично подходят для чтения людьми, но компьютеры обычно предпочитают свои данные в более легко усваиваемых форматах.

GeoJSON — это формат для кодирования структур географических данных, таких как координаты пунктов приема вторсырья в Остине, штат Техас. GeoJSON был стандартизирован в стандарте Internet Engineering Task Force под названием RFC7946 . GeoJSON определяется в терминах JSON , JavaScript Object Notation, который сам был стандартизирован в ECMA-404 той же организацией, которая стандартизировала JavaScript, Ecma International .

Важно то, что GeoJSON является широко поддерживаемым проводным форматом для передачи географических знаний. Эта лаборатория кода использует GeoJSON следующими способами:

  • Используйте пакеты Go для преобразования данных Остина во внутреннюю структуру данных ГИС, которую вы будете использовать для фильтрации запрошенных данных.
  • Сериализировать запрошенные данные для передачи между веб-сервером и веб-браузером.
  • Используйте библиотеку JavaScript, чтобы преобразовать ответ в маркеры на карте.

Это избавит вас от значительного объема ввода кода, поскольку вам не нужно писать синтаксические анализаторы и генераторы для преобразования передаваемого по сети потока данных в представления в памяти.

Получить данные

Портал открытых данных города Остин, штат Техас, делает геопространственную информацию об общедоступных ресурсах общедоступной. В этой кодовой лаборатории вы визуализируете набор данных о местах приема отходов.

Вы будете визуализировать данные с помощью маркеров на карте, отображаемых с помощью слоя данных Maps JavaScript API.

Начните с загрузки данных GeoJSON с веб-сайта города Остин в свое приложение.

  1. В окне командной строки вашего экземпляра Cloud Shell выключите сервер, набрав [CTRL] + [C].
  2. Создайте каталог data внутри каталога austin-recycling и перейдите в этот каталог:
mkdir -p data && cd data

Теперь используйте curl для получения мест утилизации:

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

Наконец, вернитесь к родительскому каталогу.

cd ..

5. Нанесите на карту места

Во-первых, обновите файл app.yaml , чтобы он отражал более надежное приложение типа «Hello World», которое вы собираетесь создать.

приложение.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

Эта конфигурация app.yaml направляет запросы / , /*.js , /*.css и /*.html в набор статических файлов. Это означает, что статический HTML-компонент вашего приложения будет обслуживаться непосредственно инфраструктурой обслуживания файлов App Engine, а не вашим приложением Go. Это снижает нагрузку на сервер и увеличивает скорость обслуживания.

Теперь пришло время создать серверную часть вашего приложения на Go!

Создайте заднюю часть

Возможно, вы заметили, что ваш файл app.yaml не предоставляет доступ к файлу GeoJSON. Это потому, что GeoJSON будет обрабатываться и отправляться нашим бэкендом Go, что позволит нам добавить некоторые причудливые функции на более поздних этапах. Измените файл main.go следующим образом:

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

Серверная часть Go уже дает нам ценную функцию: экземпляр AppEngine кэширует все эти местоположения сразу после запуска. Это экономит время, так как бэкенду не нужно будет считывать файл с диска при каждом обновлении от каждого пользователя!

Создайте переднюю часть

Первое, что нам нужно сделать, это создать папку для хранения всех наших статических ресурсов. В родительской папке вашего проекта создайте static папку.

mkdir -p static && cd static

Мы собираемся создать 3 файла в этой папке.

  • index.html будет содержать весь HTML-код для вашего одностраничного приложения для поиска магазинов.
  • style.css , как и следовало ожидать, будет содержать стили
  • app.js будет отвечать за получение GeoJSON, вызовы Maps API и размещение маркеров на вашей пользовательской карте.

Создайте эти 3 файла, поместив их в static/ .

стиль.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>

Обратите особое внимание на URL-адрес src в теге script элемента head .

  • Замените текст-заполнитель « YOUR_API_KEY » ключом API, который вы сгенерировали на этапе настройки. Вы можете посетить страницу APIs & Services -> Credentials в Cloud Console, чтобы получить свой ключ API или сгенерировать новый.
  • Обратите внимание, что URL-адрес содержит параметр callback=initialize. Теперь мы собираемся создать файл javascript, содержащий эту функцию обратного вызова. Именно здесь ваше приложение будет загружать местоположения из серверной части, отправлять их в API Карт и использовать результат для отметки пользовательских местоположений на карте, и все это будет прекрасно отображаться на вашей веб-странице.
  • Параметр libraries=places загружает библиотеку Places, которая необходима для таких функций, как автозаполнение адресов, которые будут добавлены позже.

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

Этот код отображает расположение магазинов на карте. Чтобы проверить, что у нас есть, из командной строки вернитесь в родительский каталог:

cd ..

Теперь снова запустите приложение в режиме разработки, используя:

go run *.go

Предварительно просмотрите его, как вы делали это раньше. Вы должны увидеть карту с такими маленькими зелеными кружками.

58a6680e9c8e7396.png

Вы уже рендерите локации на карте, а мы всего лишь на полпути к лабораторному коду! Удивительно. Теперь давайте добавим немного интерактивности.

6. Показать детали по запросу

Реагировать на клики по маркерам карты

Отображение набора маркеров на карте — отличное начало, но нам действительно нужно, чтобы посетитель мог щелкнуть один из этих маркеров и увидеть информацию об этом местоположении (например, название компании, адрес и т. д.). Имя небольшого информационного окна, которое обычно появляется, когда вы нажимаете на маркер Google Maps, называется « Информационное окно ».

Создайте объект infoWindow. Добавьте следующее в функцию initialize , заменив закомментированную строку, которая гласит: « // TODO: Initialize an info window ».

app.js — инициализировать

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

Замените определение функции fetchAndRenderStores на эту немного другую версию, которая меняет последнюю строку на вызов storeToCircle с дополнительным аргументом, 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));
};

Замените определение storeToCircle этой немного более длинной версией, которая теперь принимает информационное окно в качестве третьего аргумента:

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

Приведенный выше новый код отображает infoWindow с информацией о выбранном магазине при каждом щелчке маркера магазина на карте.

Если ваш сервер все еще работает, остановите его и перезапустите. Обновите страницу карты и попробуйте щелкнуть маркер на карте. Должно появиться небольшое информационное окно с названием и адресом компании, выглядящее примерно так:

1af0ab72ad0eadc5.png

7. Получите начальное местоположение пользователя

Пользователи локаторов магазинов обычно хотят знать, какой магазин находится ближе всего к ним или адрес, с которого они планируют начать свое путешествие. Добавьте панель поиска автозаполнения места, чтобы пользователь мог легко ввести начальный адрес. Автозаполнение мест обеспечивает функциональность опережающего ввода, аналогичную тому, как работает автозаполнение в других панелях поиска Google, за исключением того, что все подсказки относятся к местам на платформе Google Карт.

Создайте поле ввода пользователя

Вернитесь к редактированию style.css , чтобы добавить стиль для панели поиска автозаполнения и соответствующей боковой панели результатов. Пока мы обновляем стили CSS, мы также добавим стили для будущей боковой панели, которая отображает информацию о магазине в виде списка, сопровождающего карту.

Добавьте этот код в конец файла.

стиль.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;
}

И панель поиска автозаполнения, и выдвижная панель изначально скрыты до тех пор, пока они не потребуются.

Подготовьте div для виджета автозаполнения, заменив комментарий в index.html, который гласит: "<!-- Autocomplete div goes here --> » следующим кодом. Внося это редактирование, мы также добавим div для выдвижной панели.

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>

Теперь определите функцию для добавления виджета автозаполнения на карту, добавив следующий код в конец 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
};

Код ограничивает предложения автозаполнения только возвращаемыми адресами (поскольку автозаполнение места может также сопоставлять названия заведений и административные местоположения) и ограничивает возвращаемые адреса только теми, кто находится в США. Добавление этих необязательных спецификаций уменьшит количество символов, которые пользователь должен ввести, чтобы сузить прогнозы, чтобы показать адрес, который они ищут.

Затем он перемещает созданный вами div Autocomplete в правый верхний угол карты и указывает, какие поля должны быть возвращены для каждого места в ответе.

Наконец, вызовите функцию initAutocompleteWidget в конце функции initialize , заменив комментарий, который гласит: « // TODO: Initialize the Autocomplete widget ».

app.js — инициализировать

 // Initialize the Places Autocomplete Widget
 initAutocompleteWidget();

Перезагрузите сервер, выполнив следующую команду, затем обновите предварительный просмотр.

go run *.go

Теперь вы должны увидеть виджет автозаполнения в правом верхнем углу вашей карты, который показывает вам адреса в США, совпадающие с тем, что вы вводите, с уклоном в сторону видимой области карты.

58e9bbbcc4bf18d1.png

Обновление карты, когда пользователь выбирает начальный адрес

Теперь вам нужно обработать, когда пользователь выбирает прогноз из виджета автозаполнения, и использовать это местоположение в качестве основы для расчета расстояний до ваших магазинов.

Добавьте следующий код в конец initAutocompleteWidget в app.js , заменив комментарий « // 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
  });

Код добавляет прослушиватель, поэтому, когда пользователь щелкает одно из предложений, карта центрируется на выбранном адресе и устанавливает исходную точку в качестве основы для расчета расстояния. Вы реализуете расчет расстояния на следующем шаге.

Остановите и перезапустите сервер и обновите предварительный просмотр, чтобы увидеть повторное центрирование карты после ввода адреса в строку поиска автозаполнения.

8. Масштабирование с помощью Cloud SQL

На данный момент у нас есть довольно хороший локатор магазинов. Он использует тот факт, что приложение будет использовать только около сотни местоположений, загружая их в память на серверной части (вместо многократного чтения из файла). Но что, если вашему локатору нужно работать в другом масштабе? Если у вас есть сотни локаций, разбросанных по большой географической области (или тысячи по всему миру), хранить все эти локации в памяти — уже не лучшая идея, а разбиение зон на отдельные файлы создаст свои проблемы.

Пришло время загрузить ваши местоположения из базы данных. На этом шаге мы собираемся перенести все местоположения в вашем файле GeoJSON в базу данных Cloud SQL и обновить серверную часть Go, чтобы получать результаты из этой базы данных, а не из ее локального кеша всякий раз, когда поступает запрос.

Создайте экземпляр Cloud SQL с базой данных PostGres

Вы можете создать экземпляр Cloud SQL через Google Cloud Console, но еще проще использовать утилиту gcloud для создания экземпляра из командной строки. В облачной оболочке создайте экземпляр Cloud SQL с помощью следующей команды:

gcloud sql instances create locations \
--database-version=POSTGRES_12 \
--tier=db-custom-1-3840 --region=us-central1
  • locations аргументов — это имя, которое мы выбираем для этого экземпляра Cloud SQL.
  • Флаг tier — это способ выбрать одну из заранее определенных машин .
  • Значение db-custom-1-3840 указывает, что создаваемый экземпляр должен иметь один виртуальный ЦП и около 3,75 ГБ памяти.

Экземпляр Cloud SQL будет создан и инициализирован с помощью базы данных PostGresSQL с пользователем по умолчанию postgres . Какой у этого пользователя пароль? Отличный вопрос! У них его нет. Вам нужно настроить его, прежде чем вы сможете войти в систему.

Установите пароль с помощью следующей команды:

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

Затем введите выбранный вами пароль, когда будет предложено сделать это.

Включить расширение PostGIS

PostGIS — это расширение для PostGresSQL, упрощающее хранение стандартизированных типов геопространственных данных. В обычных условиях нам пришлось бы пройти через полный процесс установки, чтобы добавить PostGIS в нашу базу данных. К счастью, это одно из поддерживаемых Cloud SQL расширений для PostGresSQL .

Подключитесь к экземпляру базы данных, войдя в систему как пользователь postgres с помощью следующей команды в терминале облачной оболочки.

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

Введите только что созданный пароль. Теперь добавьте расширение PostGIS в командной строке postgres=> .

CREATE EXTENSION postgis;

В случае успеха вывод должен выглядеть как CREATE EXTENSION, как показано ниже.

Пример вывода команды

CREATE EXTENSION

Наконец, завершите соединение с базой данных, введя команду quit в командной строке postgres=> .

\q

Импорт географических данных в базу данных

Теперь нам нужно импортировать все эти данные о местоположении из файлов GeoJSON в нашу новую базу данных.

К счастью, это известная проблема, и в Интернете можно найти несколько инструментов для ее автоматизации. Мы собираемся использовать инструмент под названием ogr2ogr , который выполняет преобразование между несколькими распространенными форматами для хранения геопространственных данных. Среди этих вариантов, да, как вы уже догадались, преобразование формы GeoJSON в файл дампа SQL. Затем файл дампа SQL можно использовать для создания ваших таблиц и столбцов для базы данных и загрузки в него всех данных, которые существовали в ваших файлах GeoJSON.

Создать файл дампа SQL

Сначала установите ogr2ogr.

sudo apt-get install gdal-bin

Затем используйте ogr2ogr для создания файла дампа SQL. Этот файл создаст таблицу с именем austinrecycling .

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

Приведенная выше команда основана на запуске из папки austin-recycling . Если вам нужно запустить его из другого каталога, замените data на путь к каталогу, в котором хранится recycling-locations.geojson .

Заполните свою базу данных местами утилизации

После выполнения этой последней команды у вас должен появиться файл datadump.sql, в том же каталоге, где вы запускали команду. Если вы откроете его, вы увидите чуть более сотни строк SQL, создающих таблицу austinrecycling и заполняющую ее местоположениями.

Теперь откройте соединение с базой данных и запустите этот скрипт с помощью следующей команды.

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

Если сценарий работает успешно, последние несколько строк вывода будут выглядеть так:

Пример вывода команды

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

Обновите серверную часть Go, чтобы использовать Cloud SQL

Теперь, когда у нас есть все эти данные в нашей базе данных, пришло время обновить наш код.

Обновите внешний интерфейс, чтобы отправлять информацию о местоположении

Давайте начнем с одного очень небольшого обновления во внешнем интерфейсе: поскольку сейчас мы пишем это приложение для масштаба, при котором мы не хотим, чтобы каждое отдельное местоположение доставлялось во внешний интерфейс каждый раз, когда выполняется запрос, нам нужно передать некоторую базовую информацию из внешнего интерфейса о местоположении, о котором заботится пользователь.

Откройте app.js и замените определение функции fetchStores на эту версию, чтобы включить интересующие широту и долготу в 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();
};

После завершения этого шага кода в ответе будут возвращены только магазины, ближайшие к координатам карты, указанным в параметре center . Для начальной выборки в функции initialize в примере кода, представленном в этой лабораторной работе, используются центральные координаты Остина, штат Техас.

Поскольку fetchStores теперь будет возвращать только подмножество местоположений магазинов, нам нужно будет повторно получать магазины всякий раз, когда пользователь меняет их начальное местоположение.

Обновите функцию initAutocompleteWidget , чтобы обновлять местоположения всякий раз, когда устанавливается новое происхождение. Это требует двух правок:

  1. В initAutocompleteWidget найдите обратный вызов для слушателя place_changed . Снимите комментарий со строки, которая очищает существующие круги, чтобы эта строка теперь запускалась каждый раз, когда пользователь выбирает адрес в поиске места с автозаполнением abr.

app.js — initAutocompleteWidget

  autocomplete.addListener("place_changed", async () => {
    circles.forEach((c) => c.setMap(null)); // clear existing stores
    // ...
  1. Всякий раз, когда выбранное происхождение изменяется, переменная originLocation обновляется. В конце обратного вызова « place_changed » снимите комментарий со строки над строкой « // TODO: Calculate the closest stores », чтобы передать этот новый источник новому вызову функции fetchAndRenderStores .

app.js — initAutocompleteWidget

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

Обновите серверную часть, чтобы использовать CloudSQL вместо плоского файла JSON.

Удалить чтение и кэширование GeoJSON с плоским файлом

Во-первых, измените main.go , чтобы удалить код, который загружает и кэширует плоский файл GeoJSON. Мы также можем избавиться от функции dropoffsHandler , так как мы будем писать ее на базе Cloud SQL в другом файле.

Ваш новый main.go будет намного короче.

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

Создайте новый обработчик запросов местоположения

Теперь давайте создадим еще один файл, locations.go , также в каталоге austin-recycling. Начните с повторной реализации обработчика запросов местоположения.

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

Обработчик выполняет следующие важные задачи:

  • Он извлекает широту и долготу из объекта запроса (помните, как мы добавляли их в URL-адрес?)
  • Он запускает вызов getGeoJsonFromDatabase , который возвращает строку GeoJSON (мы напишем это позже).
  • Он использует ResponseWriter для печати этой строки GeoJSON в ответ.

Далее мы собираемся создать пул соединений, чтобы помочь масштабировать использование базы данных с одновременными пользователями.

Создать пул соединений

Пул соединений — это набор активных соединений с базой данных, которые сервер может повторно использовать для обслуживания запросов пользователей. Это устраняет много накладных расходов по мере увеличения количества активных пользователей, поскольку серверу не нужно тратить время на создание и удаление соединений для каждого активного пользователя. Вы могли заметить, что в предыдущем разделе мы импортировали библиотеку github.com/jackc/pgx/stdlib. Это популярная библиотека для работы с пулами соединений в Go.

В конце locations.go создайте функцию initConnectionPool (вызываемую из main.go ), которая инициализирует пул соединений. Для ясности в этом фрагменте используется несколько вспомогательных методов. configureConnectionPool предоставляет удобное место для настройки параметров пула, таких как количество подключений и время жизни каждого подключения. mustGetEnv вызовы для получения необходимых переменных среды, поэтому полезные сообщения об ошибках могут быть выданы, если в экземпляре отсутствует важная информация (например, IP-адрес или имя базы данных для подключения).

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
}

Запросите базу данных для местоположений, получите взамен JSON.

Теперь мы собираемся написать запрос к базе данных, который берет координаты карты и возвращает 25 ближайших местоположений. Не только это, но и благодаря некоторым причудливым современным функциям базы данных, он будет возвращать эти данные как GeoJSON. Конечным результатом всего этого является то, что, насколько может судить интерфейсный код, ничего не изменилось. До того, как он отправил запрос к URL-адресу и получил кучу GeoJSON. Теперь он запускает запрос к URL-адресу и... возвращает кучу GeoJSON.

Вот функция для выполнения этой магии. Добавьте следующую функцию после кода обработчика и пула соединений, который вы только что написали в нижней части 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
}

Эта функция в основном представляет собой просто настройку, демонтаж и обработку ошибок для запуска запроса к базе данных. Давайте посмотрим на настоящий SQL, который делает много действительно интересных вещей на уровне базы данных, поэтому вам не нужно беспокоиться о реализации чего-либо из них в коде.

Необработанный запрос, который запускается после анализа строки и вставки всех строковых литералов на свои места, выглядит следующим образом:

синтаксический анализ.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

Этот запрос можно рассматривать как один основной запрос и некоторые функции переноса JSON.

SELECT * ... LIMIT 25 выбирает все поля для каждого местоположения. Затем он использует функцию ST_DISTANCE (часть набора функций измерения географии PostGIS) для определения расстояния между каждым местоположением в базе данных и парой широта/долгота местоположения, предоставленного пользователем во внешнем интерфейсе. Помните, что в отличие от матрицы расстояний, которая может дать вам расстояние до автомобиля, это геопространственные расстояния. Затем для эффективности он использует это расстояние для сортировки и возвращает 25 ближайших местоположений к указанному пользователем местоположению.

** SELECT json_build_object('type', 'F **eature') завершает предыдущий запрос, беря результаты и используя их для построения объекта GeoJSON Feature . Неожиданно в этом запросе также применяется максимальный радиус «16090» — это количество метров в 10 милях, жесткое ограничение, указанное серверной частью Go. Если вам интересно, почему это предложение WHERE не было добавлено во внутренний запрос (где определяется расстояние до каждого местоположения), это связано с тем, как SQL выполняется за кулисами, это поле могло не быть вычислено, когда предложение WHERE был осмотрен. На самом деле, если вы попытаетесь переместить это предложение WHERE во внутренний запрос, это вызовет ошибку.

** SELECT json_build_object('type', 'FeatureColl **ection') Этот запрос упаковывает все результирующие строки из запроса, генерирующего JSON, в объект GeoJSON FeatureCollection .

Добавьте библиотеку PGX в свой проект

Нам нужно добавить в ваш проект одну зависимость: PostGres Driver & Toolkit , которая включает пул соединений. Проще всего это сделать с помощью Go Modules . Инициализируйте модуль с помощью этой команды в облачной оболочке:

go mod init my_locator

Затем запустите эту команду, чтобы отсканировать код на наличие зависимостей, добавить список зависимостей в файл мода и загрузить их.

go mod tidy

Наконец, запустите эту команду, чтобы загрузить зависимости непосредственно в каталог вашего проекта, чтобы можно было легко создать контейнер для AppEngine Flex.

go mod vendor

Хорошо, вы готовы проверить это!

Проверьте это

Хорошо, мы только что сделали МНОГО. Давайте посмотрим, как это работает!

Чтобы ваша машина разработки (да, даже облачная оболочка) могла подключиться к базе данных, нам придется использовать Cloud SQL Proxy для управления подключением к базе данных. Чтобы настроить Cloud SQL Proxy:

  1. Перейдите сюда, чтобы включить Cloud SQL Admin API
  2. Если вы работаете на локальном компьютере для разработки, установите облачный прокси-инструмент SQL. Если вы используете облачную оболочку, вы можете пропустить этот шаг, она уже установлена! Обратите внимание, что инструкции относятся к сервисной учетной записи. Один из них уже создан для вас, и мы рассмотрим добавление необходимых разрешений для этой учетной записи в следующем разделе.
  3. Создайте новую вкладку (в облачной оболочке или собственном терминале), чтобы запустить прокси.

bcca42933bfbd497.png

  1. Посетите https://console.cloud.google.com/sql/instances/locations/overview и прокрутите вниз, чтобы найти поле Имя подключения . Скопируйте это имя для использования в следующей команде.
  2. На этой вкладке запустите прокси-сервер Cloud SQL с помощью этой команды, заменив CONNECTION_NAME именем подключения, показанным на предыдущем шаге.
cloud_sql_proxy -instances=CONNECTION_NAME=tcp:5432

Вернитесь на первую вкладку вашей облачной оболочки и определите переменные среды, которые понадобятся Go для связи с серверной частью базы данных, а затем запустите сервер так же, как вы делали это раньше:

Перейдите в корневой каталог проекта, если вы еще этого не сделали.

cd YOUR_PROJECT_ROOT

Создайте следующие пять переменных среды (замените YOUR_PASSWORD_HERE паролем, который вы создали выше).

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

Запустите свой локальный экземпляр.

go run *.go

Откройте окно предварительного просмотра, и оно должно работать так, как будто ничего не изменилось: вы можете ввести начальный адрес, увеличить карту и щелкнуть места утилизации. Но теперь он поддерживается базой данных и готов к масштабированию!

9. Список ближайших магазинов

Directions API работает так же, как запрос маршрута в приложении Google Maps — ввод одного исходного пункта и одного пункта назначения для получения маршрута между ними. API матрицы расстояний развивает эту концепцию для определения оптимальных пар между несколькими возможными пунктами отправления и несколькими возможными пунктами назначения на основе времени в пути и расстояний. В этом случае, чтобы помочь пользователю найти ближайший магазин к выбранному адресу, вы указываете один источник и массив местоположений магазинов в качестве пунктов назначения.

Добавьте расстояние от источника к каждому магазину

At the beginning of the initMap function definition, replace the comment " // TODO: Start Distance Matrix service " with the following code:

app.js - initMap

distanceMatrixService = new google.maps.DistanceMatrixService();

Add a new function to the end of app.js called 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);
  });
};

The function calls the Distance Matrix API using the origin passed to it as a single origin and the store locations as an array of destinations. Then, it builds an array of objects storing the store's ID, distance expressed in a human-readable string, distance in meters as a numerical value, and sorts the array.

Update the initAutocompleteWidget function to calculate the store distances whenever a new origin is selected from the Place Autocomplete search bar. At the bottom of the initAutocompleteWidget function, replace the comment " // TODO: Calculate the closest stores " with the following code:

app.js - initAutocompleteWidget

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

Display a list view of stores sorted by distance

The user expects to see a list of the stores ordered from nearest to farthest. Populate a side-panel listing for each store using the list that was modified by the calculateDistances function to inform the display order of the stores.

Add a two new functions to the end of app.js called renderStoresPanel() and 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;
};

Restart your server and refresh your preview by running the following command.

go run *.go

Finally, enter an Austin, TX address into the Autocomplete search bar and click on one of the suggestions.

The map should center on that address and a sidebar should appear listing the store locations in order of distance from the selected address. One example is pictured as follows:

96e35794dd0e88c9.png

10. Style the map

One high-impact way to set your map apart visually is to add styling to it. With cloud-based map styling, the customization of your maps is controlled from the Cloud Console using Cloud-based Map Styling (beta). If you'd rather style your map with a non-beta feature, you can use the map styling documentation to help you generate json for programmatically styling the map. The instructions below guide you through Cloud-based Map Styling (beta).

Create a Map ID

First, open up Cloud Console and in the search box, and type in "Map Management" . Click the result that says "Map Management (Google Maps)". 64036dd0ed200200.png

You'll see a button near the top (right under the Search box) that says Create New Map ID . Click that, and fill in whatever name you want. For Map Type, be sure to select JavaScript , and when further options show up, select Vector from the list. The end result should look something like the image below.

70f55a759b4c4212.png

Click "Next" and you'll be graced with a brand new Map ID. You can copy it now if you want, but don't worry, it's easy to look up later.

Next we're going to create a style to apply to that map.

Create a Map Style

If you're still in the Maps section of the Cloud Console, click "Map Styles at the bottom of the navigation menu on the left. Otherwise, just like creating a Map ID, you can find the right page by typing "Map Styles" in the search box and selecting " Map Styles (Google Maps)" from the results, like in the picture below.

9284cd200f1a9223.png

Next click on the button near the top that says " + Create New Map Style "

  1. If you want to match the styling in the map shown in this lab, click the " IMPORT JSON " tab and paste the JSON blob below. Otherwise if you want to create your own, select the Map Style you want to start with. Then click Next .
  2. Select the Map ID you just created to associate that Map ID with this style, and click Next again.
  3. At this point you're given the option of further customizing the styling of your map. If this is something you want to explore, click Customize in Style Editor and play around with the colors & options until you have a map style you like. Otherwise click Skip .
  4. On the next step, enter your style's name and description, and then click Save And Publish .

Here is an optional JSON blob to import in the first step.

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

Add Map ID to your code

Now that you've gone through the trouble of creating this map style, how do you actually USE this map style in your own map? You need to make two small changes:

  1. Add the Map ID as a url parameter to the script tag in index.html
  2. Add the Map ID as a constructor argument when you create the map in your initMap() method.

Replace the script tag that loads the Maps JavaScript API in the HTML file with the loader URL below, replacing the placeholders for " YOUR_API_KEY " and " 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>
...

In the initMap method of app.js where the constant map is defined, uncomment the line for the mapId property and replace " YOUR_MAP_ID_HERE " with the Map ID you just created:

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

Restart your server.

go run *.go

Upon refreshing your preview, the map should look styled according to your preferences. Here is an example using the JSON styling above.

2ece59c64c06e9da.png

11. Deploy to production

If you want to see your app running from AppEngine Flex (and not just a local webserver on your development machine / Cloud Shell, which is what you've been doing), it's very easy. We just need to add a couple things in order for database access to work in the production environment. This is all outlined in the documentation page on Connecting from App Engine Flex to Cloud SQL .

Add Environment Variables to App.yaml

First, all those environment variables you were using to test locally need to be added to the bottom of your application's app.yaml file.

  1. Visit https://console.cloud.google.com/sql/instances/locations/overview to look up the instance connection name.
  2. Paste the following code at the end of app.yaml .
  3. Replace YOUR_DB_PASSWORD_HERE with the password you created for the postgres username earlier.
  4. Replace YOUR_CONNECTION_NAME_HERE with the value from step 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

Note that the DB_TCP_HOST should have the value 172.17.0.1 since this app connects via AppEngine Flex**.** This is because it will be communicating with Cloud SQL via a proxy, similar to the way you were.

Add SQL Client permissions to the AppEngine Flex service account

Go to the IAM-Admin page in Cloud Console and look for a service account whose name matches the format service-PROJECT_NUMBER@gae-api-prod.google.com.iam.gserviceaccount.com . This is the service account App Engine Flex will use to connect to the database. Click the Edit button at the end of the row and add the role " Cloud SQL Client ".

b04ccc0b4022b905.png

Copy your project code to the Go path

In order for AppEngine to run your code, it needs to be able to find relevant files in the Go path. Make sure you are in your project root directory.

cd YOUR_PROJECT_ROOT

Copy the directory to the go path.

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

Change into that directory.

cd ~/gopath/src/austin-recycling

Deploy Your App

Use the gcloud CLI to deploy your app. It will take some time to deploy.

gcloud app deploy

Use the browse command to get a link that you can click on to see your fully deployed, enterprise-grade, aesthetically stunning store locator in action.

gcloud app browse

If you were running gcloud outside the cloud shell, then running gcloud app browse would open a new browser tab.

12. (Recommended) Clean up

Performing this codelab will stay within free tier limits for BigQuery processing and Maps Platform API calls, but if you performed this solely as an educational exercise and want to avoid incurring any future charges, the easiest way to delete the resources associated with this project is to delete the project itself.

Delete the Project

In the GCP Console, go to the Cloud Resource Manager page:

In the project list, select the project we've been working in and click Delete . You'll be prompted to type in the project ID. Enter it and click Shut Down.

Alternatively, you can delete the entire project directly from Cloud Shell with gcloud by running the following command and replacing the placeholder GOOGLE_CLOUD_PROJECT with your project ID:

gcloud projects delete GOOGLE_CLOUD_PROJECT

13. Congratulations

Поздравляем! You have successfully completed the codelab !

Or you skimmed to the last page. Поздравляем! You have skimmed to the last page !

Over the course of this codelab, you have worked with the following technologies:

Further Reading

There's still lots to learn about all of these technologies. Below are some helpful links for topics we didn't have time to cover in this codelab, but could certainly be useful to you in building out a store locator solution that fits your specific needs.