使用 Google Maps Platform 和 Google Cloud 构建全栈店铺定位工具

1. 简介

摘要

假设您要在地图上展示多个地点,并且希望用户可以看到这些地点的位置并确定他们想要访问的地点。这种情况的常见示例包括:

  • 零售商网站上的店铺定位工具
  • 即将举行的选举的投票点地图
  • 特殊位置目录,例如电池回收站

要构建的内容

在此 Codelab 中,您将创建一个定位工具,该定位工具会根据特殊位置的实时数据 Feed 进行绘制,帮助用户找到距离他们的出发地最近的位置。这种全栈定位工具能够处理的地点远远多于简单的店铺定位工具,后者最多只能处理 25 个店铺位置。

2ece59c64c06e9da.png

要学习的内容

此 Codelab 使用开放数据集模拟与大量店铺位置相关的预填充元数据,以便您可以集中精力学习关键的技术概念。

  • Maps JavaScript API:在自定义的网页地图上显示大量位置
  • GeoJSON:用于存储与位置相关的元数据的格式
  • 地点自动补全功能:帮助用户更快速、更准确地提供出发地点
  • Go:用于开发应用后端的编程语言。后端将与数据库进行交互,并以格式经过设定的 JSON 将查询结果发送回前端。
  • App Engine:用于托管 Web 应用

前提条件

  • 具备 HTML 和 JavaScript 方面的基础知识
  • Google 帐号

2. 进行设置

在下文的第 3 步中,为此 Codelab 启用 Maps JavaScript APIPlaces APIDistance Matrix API

开始使用 Google Maps Platform

如果您之前从未使用过 Google Maps Platform,请参阅 Google Maps Platform 使用入门指南或观看 Google Maps Platform 使用入门播放列表中的视频,完成以下步骤:

  1. 创建结算帐号。
  2. 创建项目。
  3. 启用 Google Maps Platform API 和 SDK(已在上一部分中列出)。
  4. 生成 API 密钥。

激活 Cloud Shell

在此 Codelab 中,您需要使用 Cloud Shell,这是一种在 Google Cloud 中运行的命令行环境,可用于访问 Google Cloud 上运行的产品和资源,这样您就可以完全在网络浏览器上托管和运行项目了。

如需在 Cloud Console 中激活 Cloud Shell,请点击激活 Cloud Shell 89665d8d348105cd.png(配置和连接到环境应该只需要片刻时间)。

5f504766b9b3be17.png

此时,系统可能会先显示一个介绍性的插页,然后在浏览器的下半部分打开一个新的 shell。

d3bb67d514893d1f.png

确认您的项目

连接到 Cloud Shell 后,您应该会看到自己已通过身份验证,且相关项目已设置为您在设置过程中选择的项目 ID。

$ 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>

启用 App Engine Flex API

您需要在 Cloud Console 中手动启用 App Engine Flex API。这样做不仅可以启用 API,还会创建 App Engine 柔性环境服务帐号,该经过身份验证的帐号将代表用户与 Google 服务(如 SQL 数据库)进行交互。

3. Hello, World

后端:Go 版 Hello World

在 Cloud Shell 实例中,您首先要创建一个 Go App Engine Flex 应用,为此 Codelab 的其余部分奠定基础。

在 Cloud Shell 的工具栏中,点击打开编辑器按钮,以在新的标签页中打开代码编辑器。通过这种基于网络的代码编辑器,您可以轻松地在 Cloud Shell 实例中修改文件。

b63f7baad67b6601.png

然后,点击在新窗口中打开图标,将编辑器和终端移至新的标签页。

3f6625ff8461c551.png

在新标签页底部的终端中,创建一个新的 austin-recycling 目录。

mkdir -p austin-recycling && cd $_

接下来,为确保一切正常运行,您需要创建一个 Go App Engine 小应用:Hello World!

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 标准环境文档

接下来,除了 app.yaml 文件之外,再创建一个 main.go 文件:

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 软件包,用于启动监听端口 8080 的 HTTP 服务器,并针对与路径 "/" 匹配的 HTTP 请求注册处理程序函数。

处理程序函数(简称 handler)会输出文本字符串 "Hello, world!"。系统会将此文本中继回您的浏览器,供您读取。在后续步骤中,您将构建使用 GeoJSON 数据(而非简单的硬编码字符串)进行响应的处理程序。

完成上述步骤后,您的编辑器现在应如下所示:

2084fdd5ef594ece.png

开始测试

如需测试此应用,您可以在 Cloud Shell 实例中运行 App Engine 开发服务器。返回到 Cloud Shell 命令行,然后输入以下内容:

go run *.go

您会看到几行日志输出,表明您确实在 Cloud Shell 实例上运行此开发服务器,并且 Hello World Web 应用在监听 localhost 端口 8080。您可以在此应用中打开网络浏览器标签页,具体方法是,在 Cloud Shell 工具栏中,按下网页预览按钮,然后选择在端口 8080 上预览菜单项。

4155fc1dc717ac67.png

当您点击此菜单项后,系统会在您的网络浏览器中打开一个新标签页,其中包含 App Engine 开发服务器传送的“Hello, world!”字样。

在下一步中,您将向此应用添加奥斯汀市的回收数据,然后开始直观呈现此类数据。

4. 获取当前数据

GeoJSON,GIS 领域的通用语言

我们在上一步中提到,您将使用 Go 代码构建处理程序,用于在网络浏览器中呈现 GeoJSON 数据。但什么是 GeoJSON 呢?

地理信息系统 (GIS) 领域中,我们需要能够在计算机系统之间传达地理实体的相关信息。地图非常适合人类阅读,但计算机通常更希望数据采用更易理解的格式。

GeoJSON 是一种用于对地理数据结构进行编码的格式,例如德克萨斯州奥斯汀市废品回收点的坐标。GeoJSON 已在名为 RFC7946互联网工程任务组标准中进行了标准化。GeoJSON 是根据 JSON(JavaScript 对象表示法)定义的,而 JSON 本身已由实现了 JavaScript 标准化的同一组织(即 Ecma International)在 ECMA-404 中进行了标准化。

重要的是,GeoJSON 是一种受到广泛支持的传输格式,可用于传达地理信息。此 Codelab 会通过以下方式使用 GeoJSON:

  • 使用 Go 软件包将奥斯汀市的相关数据解析为 GIS 专用的内部数据结构,供您用于过滤请求的数据。
  • 序列化请求的数据,以便在网络服务器和网络浏览器之间进行传输。
  • 使用 JavaScript 库将响应转换为地图上的标记。

这样一来,您无需编写解析器和生成器即可将传输中的数据流转换为内存中的表示形式,因而大大减少了需要输入的代码量。

检索数据

德克萨斯州奥斯汀市开放数据门户提供了有关公共资源的地理空间信息,以供公众使用。在此 Codelab 中,您将直观呈现废品回收点数据集。

您将使用地图上的标记直观呈现数据,具体方法是使用 Maps JavaScript API 的数据层加以呈现。

首先,将奥斯汀市政府网站中的 GeoJSON 数据下载到您的应用中。

  1. 在 Cloud Shell 实例的命令行窗口中,输入 [CTRL] + [C] 以关闭服务器。
  2. austin-recycling 目录中创建一个 data 目录,然后切换到该目录:
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"
        "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 后端已经为我们提供了一项重要功能:App Engine 实例会在启动后立即缓存所有这些位置。这样一来,后端就无需在每位用户每次刷新时从磁盘读取文件,因此节省了时间!

构建前端

首先,我们需要创建一个文件夹来保存我们的所有静态资源。在项目的父级文件夹中,创建一个 static 文件夹。

mkdir -p static && cd static

我们将在此文件夹中创建 3 个文件。

  • index.html 将包含单页店铺定位应用的所有 HTML。
  • style.css 将包含样式,正如您所期望的那样
  • app.js 将负责检索 GeoJSON、调用 Maps API 以及在您的自定义地图上放置标记。

创建这 3 个文件,确保将其放入 static/ 中。

style.css

html,
body {
  height: 100%;
  margin: 0;
  padding: 0;
}

body {
  display: flex;
}

#map {
  height: 100%;
  flex-grow: 4;
  flex-basis: auto;
}

index.html

<html>
  <head>
    <title>Austin recycling drop-off locations</title>
    <link rel="stylesheet" type="text/css" href="style.css" />
    <script src="app.js"></script>

    <script
      defer
    src="https://maps.googleapis.com/maps/api/js?key=YOUR_API_KEY&v=weekly&libraries=places&callback=initialize&solution_channel=GMP_codelabs_fullstackstorelocator_v1_a"
    ></script>
  </head>

  <body>
    <div id="map"></div>
    <!-- Autocomplete div goes here -->
  </body>
</html>

请特别注意 head 元素的脚本标记中的 src 网址。

  • 将占位符文本“YOUR_API_KEY”替换为您在设置步骤中生成的 API 密钥。在 Cloud Console 中,依次转到“API 和服务”->“凭据”页面,即可检索您的 API 密钥或生成新的 API 密钥。
  • 请注意,该网址中包含参数 callback=initialize.。接下来我们要创建包含该回调函数的 JavaScript 文件。在此文件中,您的应用将从后端加载位置、将这些位置发送到 Maps API,然后使用返回的结果在地图上标记自定义位置,所有这些位置都将美观地呈现在您的网页上。
  • 参数 libraries=places 可用于加载 Places Library,这对稍后要添加的地址自动补全等功能是必需的。

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

您已经在地图上呈现相关位置,此 Codelab 只剩一半就能完成了!太棒了。现在,我们来添加一些互动功能。

6. 按需显示详细信息

响应地图标记上的点击事件

在地图上显示一些标记是一个不错的开始,但我们真正需要的是访问者能够点击其中一个标记,然后查看相应位置的相关信息(例如,商家名称、地址等)。当您点击 Google 地图标记时,系统通常会弹出一个小小的信息窗口,该窗口的名称为信息窗口

创建一个 infoWindow 对象。将以下内容添加到 initialize 函数中,以替换显示“// TODO: Initialize an info window”的注释行。

app.js - initialize

  // Add an info window that pops up when user clicks on an individual
  // location. Content of info window is entirely up to us.
  infowindow = new google.maps.InfoWindow();

fetchAndRenderStores 函数定义替换为这个略有不同的版本,这会提供一个额外的参数 infowindow,将最后一行代码更改为调用 storeToCircle

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 Maps Platform 中的地点。

创建用户输入字段

返回修改 style.css,以便为自动补全搜索栏和相关的结果侧边栏添加样式。更新 CSS 样式时,我们还会为日后随地图一起显示的边栏添加样式,该边栏会以列表形式显示店铺信息。

将此代码添加到文件末尾。

style.css

#panel {
  height: 100%;
  flex-basis: 0;
  flex-grow: 0;
  overflow: auto;
  transition: all 0.2s ease-out;
}

#panel.open {
  flex-basis: auto;
}

#panel .place {
  font-family: "open sans", arial, sans-serif;
  font-size: 1.2em;
  font-weight: 500;
  margin-block-end: 0px;
  padding-left: 18px;
  padding-right: 18px;
}

#panel .distanceText {
  color: silver;
  font-family: "open sans", arial, sans-serif;
  font-size: 1em;
  font-weight: 400;
  margin-block-start: 0.25em;
  padding-left: 18px;
  padding-right: 18px;
}

/* Styling for Autocomplete search bar */
#pac-card {
  background-color: #fff;
  border-radius: 2px 0 0 2px;
  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3);
  box-sizing: border-box;
  font-family: Roboto;
  margin: 10px 10px 0 0;
  -moz-box-sizing: border-box;
  outline: none;
}

#pac-container {
  padding-top: 12px;
  padding-bottom: 12px;
  margin-right: 12px;
}

#pac-input {
  background-color: #fff;
  font-family: Roboto;
  font-size: 15px;
  font-weight: 300;
  margin-left: 12px;
  padding: 0 11px 0 13px;
  text-overflow: ellipsis;
  width: 400px;
}

#pac-input:focus {
  border-color: #4d90fe;
}

#pac-title {
  color: #fff;
  background-color: #acbcc9;
  font-size: 18px;
  font-weight: 400;
  padding: 6px 12px;
}

.hidden {
  display: none;
}

自动补全搜索栏和滑出式侧边栏最初都处于隐藏状态,仅在需要时才会显示出来。

将 index.html 中显示"<!-- Autocomplete div goes here -->”的注释替换为以下代码,以便为自动补全微件准备 div。进行此项修改时,我们还会为滑出式侧边栏添加 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 移到地图的右上角,并指定应在响应中返回各个地点的哪些字段。

最后,在 initialize 函数结束时调用 initAutocompleteWidget 函数,以替换显示“// TODO: Initialize the Autocomplete widget”的注释。

app.js - initialize

 // Initialize the Places Autocomplete Widget
 initAutocompleteWidget();

请运行以下命令重启服务器,然后刷新预览。

go run *.go

现在,您应该会在地图右上角看到自动补全微件,其中显示了与您输入的内容匹配的美国地址,偏向于地图的可见区域。

58e9bbbcc4bf18d1.png

在用户选择出发地址时更新地图

现在,您需要处理以下情况:用户从自动补全微件中选择预测位置后,以该位置为基础计算到您店铺的距离。

将以下代码添加到 app.js 中的 initAutocompleteWidget 末尾,以替换注释“// 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 后端,以便从该数据库(而不是其本地缓存)中提取结果。

使用 PostGres 数据库创建 Cloud SQL 实例

您可以通过 Google Cloud Console 创建 Cloud SQL 实例,但更简单的方法是使用 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 表示要创建的实例应具有 1 个 vCPU 且内存大约为 3.75GB。

Cloud SQL 实例将使用 PostGresSQL 数据库进行创建和初始化,其中默认用户为 postgres。此用户的密码是什么?这个问题问得好!他们没有密码。您需要先配置一个密码,然后才能登录。

请使用以下命令设置密码:

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

然后,在系统提示您输入密码时,输入您选择的密码。

启用 PostGIS 扩展程序

PostGIS 是 PostGresSQL 的一款扩展程序,可让您更轻松地存储标准化类型的地理空间数据。在正常情况下,我们必须完成整个安装流程才能将 PostGIS 添加到数据库中。幸运的是,这是 Cloud SQL 支持的 PostGresSQL 扩展程序之一。

连接到数据库实例,只需在 Cloud Shell 终端中使用以下命令以用户 postgres 的身份登录即可。

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

输入您刚刚创建的密码。现在,在 postgres=> 命令提示符处添加 PostGIS 扩展程序。

CREATE EXTENSION postgis;

如果成功,输出应显示“CREATE EXTENSION”,如下所示。

命令输出示例

CREATE EXTENSION

最后,在 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,。打开该文件后,您将看到 100 多行 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 函数定义替换为以下版本,以在网址中包含地图注点的纬度和经度。

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

完成 Codelab 的此步骤后,响应将仅返回距离 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)
        }
}

为位置请求创建新的处理程序

接下来在 austin-recycling 目录中再创建另一个文件 locations.go。首先,重新为位置请求实现处理程序。

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

该处理程序会执行以下重要任务:

  • 从请求对象中提取纬度和经度(还记得是如何将这些内容添加到网址中的吗?)
  • 触发 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 的形式返回数据。而这一切的最终结果就是,只要前端代码可以识别出来,就没有发生任何变化。之前,它触发了对网址的请求,然后获取大量 GeoJSON。现在,它会触发对网址的请求,然后…还是获取大量 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 会在数据库层执行许多非常有趣的操作,因此您不必担心通过代码实现其中任何一项。

在字符串经过解析并且所有字符串字面量插入各自合适的位置后,原始查询就会触发,如下所示:

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 的一系列地理位置测量函数中的一种)来确定数据库中每个位置之间的距离,以及用户在前端提供的位置的纬度/经度对。请注意,与可提供行车距离的距离矩阵不同,这些距离是指理空间距离。为了提高效率,随后它会使用该距离进行排序,然后返回距离用户的指定位置最近的 25 个位置。

**SELECT json_build_object(‘type', ‘F**eature') 会封装上一个查询,以获取相关结果并使用这些结果构建 GeoJSON 特征对象。出乎意料的是,系统还会在此查询中应用最大半径“16090”(10 英里对应的米数,这是由 Go 后端指定的硬性限制)。如果您想知道为什么没有改为将此 WHERE 子句添加到内部查询(其中确定了每个位置之间的距离)中,这是由于 SQL 在后台执行的方式,系统在检查此 WHERE 子句时可能没有计算该字段。实际上,如果您尝试将此 WHERE 子句移至内部查询,则会抛出错误。

**SELECT json_build_object(‘type', ‘FeatureColl**ection') 此查询会将 JSON 生成的查询中的所有结果行封装在 GeoJSON FeatureCollection 对象中。

将 PGX 库添加到您的项目中

我们需要向您的项目添加一个用于启用连接池的依赖项:PostGres 驱动程序和工具包。最简单的方法是使用 Go 模块。在 Cloud Shell 中使用以下命令初始化模块:

go mod init my_locator

接下来,运行此命令,以扫描代码,查找依赖项、将依赖项列表添加到 mod 文件中,然后下载这些依赖项。

go mod tidy

最后,运行此命令,将依赖项直接提取到您的项目目录中,以便轻松针对 App Engine 柔性环境构建容器。

go mod vendor

现在,您已准备好开始测试了!

开始测试

我们刚刚完成了许多工作。接下来看看它如何发挥作用吧!

为了让您的开发机器(即使是 Cloud Shell)能够连接到数据库,我们必须使用 Cloud SQL 代理来管理数据库连接。如需设置 Cloud SQL 代理,请执行以下操作:

  1. 转到此处启用 Cloud SQL Admin API
  2. 如果您使用的是本地开发机器,请安装 Cloud SQL 代理工具。如果您使用的是 Cloud Shell,则可以跳过此步骤,Cloud SQL 代理已安装完毕!请注意,具体操作说明将涉及服务帐号。我们已经为您创建了一个服务帐号,并且在下面的部分中,我们将介绍如何为该帐号添加必要的权限。
  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

返回到 Cloud Shell 的第一个标签页,定义 Go 与数据库后端通信所需的环境变量,然后按照以前的方式运行服务器:

转到项目的根目录(如果您尚未进入该目录的话)。

cd YOUR_PROJECT_ROOT

创建以下 5 个环境变量(将 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 地图应用中请求路线,即输入一个出发地和一个目的地,然后获取两地之间的路线。Distance Matrix API 进一步深化了此概念,力求根据行程时间和距离,确定多个可能的出发地与多个可能的目的地之间的最优配对。在这种情况下,为帮助用户找到距离所选地址最近的店铺,您需要提供一个出发地以及一组作为目的地的店铺位置。

添加从出发地到每个店铺的距离

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

该函数会调用 Distance Matrix API,将其收到的出发地用作一个出发地,并将店铺位置用作一组目的地。然后,它会构建一个对象数组,用于存储店铺的 ID、以用户可理解的字符串表示的距离、数值形式的距离(以米为单位),然后对该数组进行排序。

更新 initAutocompleteWidget 函数,以在用户每次从地点自动补全搜索栏中选择新出发地时计算出发地到店铺的距离。在 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

最后,在自动补全搜索栏中输入德克萨斯州奥斯汀市的一个地址,然后点击其中一个建议的地点。

地图应以该地址为中心,且应显示一个边栏,其中会按照与选定地址的距离顺序列出店铺位置。下图显示了一个示例:

96e35794dd0e88c9.png

10. 设置地图样式

在视觉上突出显示地图的一种高效方法是为地图添加样式。借助云端地图样式设置,您可以在 Cloud Console 中使用云端地图样式设置(Beta 版)来控制地图的自定义设置。如果您希望使用非 Beta 版功能设置地图样式,则可以参阅地图样式设置文档,生成用于以编程方式设置地图样式的 JSON。以下说明将引导您完成云端地图样式设置(Beta 版)。

创建地图 ID

首先,打开 Cloud Console,然后在搜索框中输入“地图管理”。点击显示“地图管理(Google 地图)”的结果。64036dd0ed200200.png

您将在顶部附近(搜索框正下方)看到一个显示创建新的地图 ID 的按钮。点击该按钮,然后填写任意名称。对于“地图类型”,请务必选择 JavaScript,如果系统显示更多选项,请从列表中选择矢量。最终结果应如下图所示。

70f55a759b4c4212.png

点击“下一步”,您便会获得一个全新的地图 ID。如果需要的话,您可以立即复制此 ID,不过别担心,以后查找起来也很轻松。

接下来,我们将创建应用于该地图的样式。

创建地图样式

如果您仍然位于 Cloud Console 的“地图”部分,请点击左侧导航菜单底部的“地图样式”。另外,您也可以像创建地图 ID 一样,找到正确的页面,具体方法是,在搜索框中输入“地图样式”,然后从结果中选择“地图样式(Google 地图)”,如下图所示。

9284cd200f1a9223.png

接下来,点击顶部附近显示“+ 新建地图样式”的按钮

  1. 如果您希望匹配此实验中显示的地图样式,请点击“导入 JSON”标签页,然后粘贴下面的 JSON blob。或者,如果您想自行创建,请选择您想要开始使用的地图样式。然后,点击下一步
  2. 选择您刚刚创建的地图 ID,将该地图 ID 与此样式相关联,然后再次点击下一步
  3. 此时,您可以选择进一步自定义地图样式。如果您想要探索这部分内容,请点击在样式编辑器中自定义,然后尝试各种颜色和选项,直到获得令您满意的地图样式。否则,请点击跳过
  4. 在下一步中,输入样式的名称和说明,然后点击保存并发布

以下是要在第一步中导入的可选 JSON 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"
      }
    ]
  }
]

将地图 ID 添加到您的代码中

现在,您已费尽心力创建完此地图样式,那么如何在您自己的地图中实际使用此地图样式呢?您需要进行以下两项细微更改:

  1. 将地图 ID 作为网址参数添加到 index.html 中的脚本标记
  2. initMap() 方法中创建地图时,将地图 ID Add 为构造函数参数。

将 HTML 文件中用于加载 Maps JavaScript API 的脚本标记替换为下面的加载器网址,注意需要替换“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>
...

app.jsinitMap 方法(其中定义了常量 map)中,取消对 mapId 属性所在行的注释,并将“YOUR_MAP_ID_HERE”替换为您刚刚创建的地图 ID:

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. 部署到生产环境

如果您希望看到自己的应用从 App Engine 柔性环境中运行,而不仅仅是从开发机器/Cloud Shell 上的本地网络服务器中运行(这正是您一直以来采取的做法),这是非常简单的。只需添加几项内容,即可在生产环境中访问数据库。如需了解所有这些信息,请访问从 App Engine 柔性环境连接到 Cloud SQL 文档页面。

向 App.yaml 添加环境变量

首先,您在本地测试所用的所有这些环境变量都需要添加到应用的 app.yaml 文件底部。

  1. 访问 https://console.cloud.google.com/sql/instances/locations/overview,查找实例连接名称。
  2. 将以下代码粘贴到 app.yaml 末尾。
  3. YOUR_DB_PASSWORD_HERE 替换为您之前为用户名 postgres 创建的密码。
  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

请注意,由于此应用通过 App Engine 柔性环境进行连接,因此 DB_TCP_HOST 的值应为 172.17.0.1**。**这是因为它将通过代理与 Cloud SQL 进行通信,这与您之前使用的方式类似。

向 App Engine 柔性环境服务帐号添加 SQL Client 权限

转到 Cloud Console 中的“IAM 管理”页面,查找名称与 service-PROJECT_NUMBER@gae-api-prod.google.com.iam.gserviceaccount.com 格式匹配的服务帐号。App Engine 柔性环境将使用此服务帐号连接到数据库。点击行末尾的修改按钮,然后添加“Cloud SQL Client”角色。

b04ccc0b4022b905.png

将项目代码复制到 Go 路径

为了运行您的代码,App Engine 需要能够在 Go 路径中找到相关文件。请确保您目前位于项目的根目录中。

cd YOUR_PROJECT_ROOT

将该目录复制到 Go 路径。

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

切换到该目录。

cd ~/gopath/src/austin-recycling

部署您的应用

使用 gcloud 工具部署您的应用。部署需要一些时间才能完成。

gcloud app deploy

使用 browse 命令获取一个链接,点击该链接,即可查看您的已完全部署且精致美观的企业级店铺定位工具的实际运行情况。

gcloud app browse

如果您在 Cloud Shell 之外运行 gcloud,则运行 gcloud app browse 会打开新的浏览器标签页。

12. (推荐)清理

完成此 Codelab 将不会超出 BigQuery 处理和 Maps Platform API 调用的免费层级限制,但如果您只是将此 Codelab 作为培训练习来完成,并且希望避免产生任何额外费用,那么删除与此项目相关联的资源的最简单方法就是删除项目本身。

删除项目

在 GCP Console 中,转到 Cloud Resource Manager 页面:

在项目列表中,选择我们一直使用的项目,然后点击删除。此时,系统会提示您输入项目 ID。输入项目 ID,然后点击关停

此外,您也可以运行以下命令并将占位符 GOOGLE_CLOUD_PROJECT 替换为您的项目 ID,以使用 gcloud 直接从 Cloud Shell 中删除整个项目:

gcloud projects delete GOOGLE_CLOUD_PROJECT

13. 恭喜

恭喜!您已成功完成此 Codelab

或者您已浏览至最后一页。恭喜!您已浏览至最后一页

在此 Codelab 中,您使用了以下技术:

深入阅读

关于所有这些技术,还有许多内容需要我们了解。下面提供了一些实用链接,其中涵盖了我们因时间关系而没有在此 Codelab 中加以介绍的主题,但毫无疑问,这些主题可帮助您制定满足您具体需求的店铺定位工具解决方案。