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

1. Введение

Абстрактный

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

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

Что вы построите

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

2ece59c64c06e9da.png

Чему вы научитесь

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

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

Предпосылки

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

2. Настройте

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

Начните работу с платформой Google Карт

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

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

Активировать Cloud Shell

В этой лабораторной работе вы будете использовать 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>

Включить API AppEngine Flex

API AppEngine Flex необходимо включить вручную в консоли Cloud. Это не только включит API, но и создаст учётную запись службы AppEngine Flexible Environment — аутентифицированную учётную запись, которая будет взаимодействовать со службами 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 следующее содержимое:

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

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

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

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

GeoJSON — язык общения в мире ГИС

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

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

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

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

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

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

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

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

Вы визуализируете данные с помощью маркеров на карте, отображаемых с помощью слоя данных 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».

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

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

Теперь пришло время создать бэкэнд вашего приложения на Go!

Постройте бэкэнд

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

main.go

package main

import (
        "fmt"
        "log"
        "net/http"
        "os"
        "path/filepath"
)

var GeoJSON = make(map[string][]byte)

// cacheGeoJSON loads files under data into `GeoJSON`.
func cacheGeoJSON() {
        filenames, err := filepath.Glob("data/*")
        if err != nil {
                log.Fatal(err)
        }

        for _, f := range filenames {
                name := filepath.Base(f)
                dat, err := os.ReadFile(f)
                if err != nil {
                        log.Fatal(err)
                }
                GeoJSON[name] = dat
        }
}

func main() {
        // Cache the JSON so it doesn't have to be reloaded every time a request is made.
        cacheGeoJSON()


        // Request for data should be handled by Go.  Everything else should be directed
        // to the folder of static files.
        http.HandleFunc("/data/dropoffs", dropoffsHandler)
        http.Handle("/", http.FileServer(http.Dir("./static/")))

        // Open up a port for the webserver.
        port := os.Getenv("PORT")
        if port == "" {
                port = "8080"
        }
        log.Printf("Listening on port %s", port)

        if err := http.ListenAndServe(":"+port, nil); err != nil {
                log.Fatal(err)
        }
}

func helloHandler(w http.ResponseWriter, r *http.Request) {
        // Writes Hello, World! to the user's web browser via `w`
        fmt.Fprint(w, "Hello, world!")
}

func dropoffsHandler(w http.ResponseWriter, r *http.Request) {
        w.Header().Set("Content-type", "application/json")
        w.Write(GeoJSON["recycling-locations.geojson"])
}

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

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

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

mkdir -p static && cd static

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

  • index.html будет содержать весь HTML-код вашего одностраничного приложения для поиска магазинов.
  • style.css , как и ожидалось, будет содержать стили
  • app.js будет отвечать за извлечение GeoJSON, выполнение вызовов 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, сгенерированный вами на этапе настройки. Чтобы получить ключ API или сгенерировать новый, перейдите на страницу «API и сервисы» -> «Учётные данные» в Cloud Console.
  • Обратите внимание, что 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 Карт, называется Info Window .

Создайте объект 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. Получите начальное местоположение пользователя.

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

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

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. Начнём с повторной реализации обработчика запросов местоположения.

location.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-адреса или имени базы данных для подключения).

location.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. В итоге, насколько может судить front-end-код, ничего не изменилось. Раньше он отправлял запрос по URL и получал кучу GeoJSON. Теперь он отправляет запрос по URL и... получает кучу GeoJSON.

Вот функция, которая творит это волшебство. Добавьте следующую функцию после кода обработчика и пула подключений, который вы только что написали в конце файла locations.go .

location.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-код, который выполняет множество интересных функций на уровне базы данных, так что вам не придётся беспокоиться о реализации этих функций в коде.

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

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

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

SELECT * ... LIMIT 25 выбирает все поля для каждого местоположения. Затем функция ST_DISTANCE (входит в набор функций PostGIS для измерения географических данных ) определяет расстояние между каждым местоположением в базе данных и парой координат (широта/долгота) местоположения, указанной пользователем во внешнем интерфейсе. Помните, что в отличие от Distance Matrix, которая может показывать расстояние, которое можно проехать на машине, это геопространственные расстояния. Для эффективности функция использует это расстояние для сортировки и возвращает 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 Shell) мог подключиться к базе данных, нам потребуется использовать Cloud SQL Proxy для управления подключением к базе данных. Чтобы настроить Cloud SQL Proxy:

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

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. Перечислите ближайшие магазины

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

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

В начале определения функции initMap замените комментарий « // TODO: Start Distance Matrix service » со следующим кодом:

app.js - initmap

distanceMatrixService = new google.maps.DistanceMatrixService();

Добавьте новую функцию в конце app.js , называемого 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);
  });
};

Функция вызывает API матрицы расстояния, используя исходное происхождение, переданное ему в качестве единого происхождения, а местоположения хранилища в качестве массива направлений. Затем он строит массив объектов, хранящих идентификатор магазина, расстояние, выраженное в человеческой строке, расстояние в метрах в качестве численного значения и сортирует массив.

Обновите функцию initAutocompleteWidget , чтобы вычислить расстояния в магазине всякий раз, когда выбирается новое происхождение из автозаполнения Place. Внизу функции initAutocompleteWidget , замените комментарий « // TODO: Calculate the closest stores » с помощью следующего кода:

app.js - initautocompletewidget

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

Отобразить список магазинов, отсортированных по расстоянию

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

Добавьте две новые функции в конце app.js , называемого renderStoresPanel() и 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;
};

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

go run *.go

Наконец, введите адрес Austin, TX в строку поиска автозаполнения и нажмите на одно из предложений.

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

96E35794DD0E88C9.png

10. Стиль карты

Один высокопоставленный способ выделения карты визуально-добавить в нее стиль. С помощью облачного стиля карты настройка ваших карт управляется из облачной консоли с использованием облачной карты стиля карты (бета). Если вы предпочитаете создать свою карту с помощью не-бета-функции, вы можете использовать документацию по стилю карты , чтобы помочь вам генерировать JSON для программного стиля карты. Инструкции ниже направляют вас через облачную стиль карты (бета).

Создать идентификатор карты

Во -первых, откройте облачную консоль и в поле поиска и введите «Управление картой». Нажмите на результат, в котором говорится «Управление картой (карты Google)». 64036dd0ed200200.png

Вы увидите кнопку возле верхней части (прямо под окном поиска) с написанием «Создание нового идентификатора карты» . Нажмите на это и заполните любое имя, которое вы хотите. Для типа карты обязательно выберите JavaScript , и, когда появятся дополнительные параметры, выберите Vector в списке. Конечный результат должен выглядеть примерно как изображение ниже.

70F55A759B4C4212.png

Нажмите «Далее», и вы будете украшены совершенно новым идентификатором карты. Вы можете скопировать его сейчас, если хотите, но не волнуйтесь, это легко посмотреть позже.

Далее мы собираемся создать стиль, чтобы применить к этой карте.

Создать стиль карты

Если вы все еще находитесь в разделе «Карты облачной консоли», нажмите «Стили карты в нижней части меню навигации слева. В противном случае, как и в создании идентификатора карты, вы можете найти правильную страницу, набрав« стили карты »в поле поиска и выбрав« Стили карты (карты Google) »из результатов, как на рисунке ниже.

9284CD200F1A9223.png

Затем нажмите на кнопку рядом с верхней частью с надписью « + Создайте новый стиль карты »

  1. Если вы хотите соответствовать стилю на карте, показанной в этой лаборатории, нажмите вкладку « Импорт JSON » и вставьте BLOB JSON ниже. В противном случае, если вы хотите создать свой собственный, выберите стиль карты, с которой вы хотите начать. Затем нажмите Далее .
  2. Выберите только что созданный идентификатор карты, чтобы связать этот идентификатор карты с этим стилем, и нажмите снова .
  3. На этом этапе вам предоставлена возможность дальнейшей настройки стиля вашей карты. Если это то, что вы хотите изучить, нажмите «Настроить в редакторе стилей» и поиграйте с цветами и опциями, пока не получите стиль карты, который вам понравится. В противном случае нажмите Skip .
  4. На следующем шаге введите имя и описание вашего стиля, а затем нажмите «Сохранить и публиковать» .

Вот необязательная Blob для импорта на первом шаге.

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

Добавьте идентификатор карты в свой код

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

  1. Добавьте идентификатор карты в качестве параметра URL в тег скрипта в index.html
  2. Add идентификатор карты в качестве аргумента конструктора при создании карты в методе initMap() .

Замените тег скрипта, который загружает API Maps JavaScript в файле HTML на URL -адрес загрузчика ниже, заменив заполнители для " YOUR_API_KEY " и " 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>
...

В методе initMap от app.js , где определяется постоянная map , расстроен строку для свойства mapId и замените « YOUR_MAP_ID_HERE » на только что созданный идентификатор карты, который вы только что создали:

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

Перезагрузите ваш сервер.

go run *.go

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

2ece59c64c06e9da.png

11. Развертывание на производство

Если вы хотите, чтобы ваше приложение работало от Appengine Flex (и не просто локального веб -сервера на вашем машине разработки / облачной оболочке, что вы делаете), это очень легко. Нам просто нужно добавить пару вещей для доступа к базе данных к работе в производственной среде. Все это изложено на странице документации при подключении от App Engine Flex к Cloud SQL .

Добавить переменные среды в app.yaml

Во -первых, все те переменные среды, которые вы использовали для тестирования локально, необходимо добавить в нижнюю часть файла app.yaml вашего приложения.

  1. Посетите https://console.cloud.google.com/sql/instance/locations/overview , чтобы найти имя соединения экземпляра.
  2. Вставьте следующий код в конце app.yaml .
  3. Замените YOUR_DB_PASSWORD_HERE созданным вами паролем для postgres username ранее.
  4. Замените YOUR_CONNECTION_NAME_HERE со значением с шага 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

Обратите внимание, что DB_TCP_HOST должен иметь значение 172.17.0.1 , поскольку это приложение подключается через Appengine Flex **. ** Это связано с тем, что оно будет общаться с облачным SQL через прокси, аналогичный тому, как вы были.

Добавить разрешения клиента SQL в учетную запись Service Appengine Flex

Перейдите на страницу IAM-ADMIN в облачной консоли и ищите учетную запись службы, чье имя соответствует Format service-PROJECT_NUMBER@gae-api-prod.google.com.iam.gserviceaccount.com . Это приложение Service Account Engine Flex будет использовать для подключения к базе данных. Нажмите кнопку «Редактировать» в конце строки и добавьте роль « Cloud SQL Client ».

B04CCC0B4022B905.png

Скопируйте код проекта на путь Go

Чтобы Appengine запустил ваш код, он должен иметь возможность найти соответствующие файлы в пути GO. Убедитесь, что вы находитесь в вашем проекте Root Directory.

cd YOUR_PROJECT_ROOT

Скопируйте каталог на путь GO.

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

Изменить в этот каталог.

cd ~/gopath/src/austin-recycling

Развернуть ваше приложение

Используйте gcloud CLI, чтобы развернуть ваше приложение. Это займет некоторое время, чтобы развернуть.

gcloud app deploy

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

gcloud app browse

Если бы вы запустили gcloud за пределами облачной оболочки, то запуск gcloud app browse откроет новую вкладку браузера.

12. (рекомендуется) очистить

Выполнение этого CodeLab останется в пределах бесплатных пределов уровня для обработки BigQuery и отображает вызовы API платформы, но если вы выполняете это исключительно в качестве образовательного упражнения и хотите избежать каких -либо будущих обвинений, самый простой способ удалить ресурсы, связанные с этим проектом, - это удалить сам проект.

Удалить проект

В консоли GCP перейдите на страницу Manager Cloud Resource Manager :

В списке проектов выберите проект, над которым мы работали, и нажмите «Удалить» . Вам будет предложено ввести идентификатор проекта. Введите его и нажмите, выключите.

В качестве альтернативы, вы можете удалить весь проект непосредственно из Cloud Shell с gcloud , запустив следующую команду и заменив заполнителя GOOGLE_CLOUD_PROJECT на идентификатор проекта:

gcloud projects delete GOOGLE_CLOUD_PROJECT

13. Поздравляю

Поздравляю! Вы успешно завершили CodeLab !

Или вы проезжали на последнюю страницу. Поздравляю! Вы сняли на последней странице !

В течение этого коделаба вы работали со следующими технологиями:

Дополнительное чтение

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