使用 Google 地圖平台和 Google Cloud 建構完整堆疊店家搜尋器

1. 簡介

摘要

假設您有許多地點可以在地圖上顯示,而您希望讓使用者能夠查看這些地點的位置,並識別他們想前往的地點。常見的情況包括:

  • 零售商網站上的店家搜尋器
  • 某意見調查地點將顯示即將舉行的選舉
  • 專賣場所 (例如電池回收接收) 的目錄

建構項目

在這個程式碼研究室中,您將建立定位器,以收集特定地點的即時資料動態饋給,並協助使用者找到距離起點最近的地點。比起簡易店家搜尋器,這種全堆疊定位器能夠處理的地點數量更是多了 25 個以下。

2ece59c64c06e9da.png

課程內容

本程式碼研究室使用開放式資料集來模擬大量商店位置的預先填入中繼資料,讓您專心瞭解重要的技術概念。

  • Maps JavaScript API:在自訂網頁地圖上顯示大量位置
  • GeoJSON:此格式可儲存位置中繼資料
  • 地點自動完成:讓使用者更快、更準確地提供起點
  • Go:用來開發應用程式後端的程式設計語言。後端會與資料庫互動,並將查詢結果傳回採用 JSON 格式的前端。
  • App Engine:用於代管網頁應用程式

必要條件

  • HTML 和 JavaScript 的基本知識
  • Google 帳戶

2. 做好準備

在下一節的步驟 3 中,為這個程式碼研究室啟用 Maps JavaScript APIPlaces APIDistance Matrix API

開始使用 Google 地圖平台

如果您未曾使用過 Google 地圖平台,請按照開始使用 Google 地圖平台指南或觀看 Google 地圖平台入門指南完成下列步驟:

  1. 建立帳單帳戶。
  2. 建立專案。
  3. 啟用 Google 地圖平台的 API 和 SDK (如上一節所示)。
  4. 產生 API 金鑰。

啟動 Cloud Shell

在這個程式碼研究室中,您可以使用 Cloud Shell,這是一個在 Google Cloud 中執行的指令列環境,可讓您存取在 Google Cloud 中運作的產品與資源,方便您從網路瀏覽器託管及執行專案。

如要透過 Cloud Console 啟用 Cloud Shell,請按一下 [啟用 Cloud Shell] 89665d8d348105cd.png (只需幾分鐘即可佈建並連線至環境)。

5f504766b9b3be17.png

這個選項可能會在瀏覽器開啟後,在瀏覽器下半部開啟新的殼層。

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>

啟用 AppEngine Flex API

您必須在 Cloud Console 中手動啟用 AppEngine Flex API。如此一來,您不僅會啟用 API,還能建立 AppEngine 彈性環境服務帳戶;這個帳戶是通過驗證的使用者,可代表使用者存取 Google 服務 (例如 SQL 資料庫)。

3. 大家好:

後端:Go World 的《Go World》

在您的 Cloud Shell 執行個體中,請先建立 Go App Engine Flex 應用程式,做為應用程式程式碼其餘部分的基礎。

在 Cloud Shell 工具列中,按一下 [Open Editor] (開啟編輯器) 按鈕,在新分頁中開啟程式碼編輯器。這個網頁式程式碼編輯器可讓您輕鬆修改 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 標準環境說明文件

接著,建立 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 套件,會啟動監聽 8080 通訊埠的 http 伺服器,並針對與路徑 "/" 相符的 HTTP 要求註冊處理常式函式。

處理常式函式 (稱為 handler) 會寫入文字字串 "Hello, world!"。系統會將這些文字轉發回您的瀏覽器,讓你能夠讀取。在接下來的步驟中,您將建立使用 GeoJSON 資料回應的處理常式,而不是簡單的硬式編碼字串。

執行這些步驟後,您的編輯器應該如下所示:

2084fdd5ef594ece.png

立即測試

如要測試這個應用程式,您可以在 Cloud Shell 執行個體中執行 App Engine 開發伺服器。返回 Cloud Shell 指令列,然後輸入以下內容:

go run *.go

您會看到一行記錄檔輸出內容,表示您確實是在 Cloud Shell 執行個體上執行開發伺服器,而 Hello World 網路應用程式會監聽 localhost 通訊埠 8080。您可以在這個應用程式中開啟網路瀏覽器分頁,方法是按下 [網頁預覽] 按鈕,然後在 Cloud Shell 工具列中選取 [透過以下埠預覽:8080]

4155fc1dc717ac67.png

按一下這個選單項目,即可在網路瀏覽器中開啟新分頁,其中顯示「Hello, world!」字樣。

您將在下一個步驟中,將奧斯汀市的回收資料加入這個應用程式,並開始以視覺化方式呈現資料。

4. 取得目前的資料

GeoJSON (吉他語) 世界語言法語上的 GeoJSON

在上一個步驟中提到,您只會在 Go 程式碼中建立處理常式,以將 GeoJSON 資料轉譯為網路瀏覽器。但 GeoJSON 是什麼?

地理資訊系統 (GIS) 世界中,我們需要能夠針對電腦系統之間的地理實體傳達相關知識。Google 地圖對一般使用者來說非常實用,不過一般來說,電腦偏好比較容易使用的資料。

GeoJSON 是編碼地理資料結構的格式,例如在德州奧斯汀的回收地點。GeoJSON 已成為網際網路工程任務組 (RFC7946) 的標準標準化。GeoJSON 的定義與 JSON (JavaScript 物件標記法) 相同,而該標準本身是已在 ECMA-404 中標準化的標準,而此組織是由標準化的 JavaScript 組織 (Ecma International)。

重點在於 GeoJSON 是一種廣泛的線線格式,可傳達地理知識。此程式碼研究室以下列方式使用 GeoJSON:

  • 使用 Go 套件剖析 Austin 資料至內部的 GIS 專屬資料結構,以便篩選要求的資料。
  • 將請求的資料序列化在網路伺服器和網路瀏覽器之間傳輸。
  • 使用 JavaScript 程式庫將回應轉換成地圖上的標記。

如此一來,您就不必另外撰寫程式碼,因為您不用撰寫剖析器和產生器,就能將線上的 Datastream 轉換成記憶體內表示法。

擷取資料

德州開放式資料入口網站 (Cust of Austin, Texas Open Data Portal) 則提供可公開使用的公開資源地理空間資訊。在這個程式碼研究室中,您會以視覺化方式呈現「回收地點」資料集

使用 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 將由 Google 的後端處理及傳送,所以我們可以在後續的步驟中使用一些精美的功能。將 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 金鑰。如要查詢 API 金鑰或產生新的金鑰,請前往 Cloud Console 中的 API 與「服務 -> Credentials」(憑證) 頁面
  • 請注意,網址包含 callback=initialize. 參數。我們會開始建立包含該回呼函式的 JavaScript 檔案。在這個位置,您的應用程式會從後端載入位置、將結果傳送至 Maps API,並使用結果來在地圖上標示自訂位置,所有結果都會顯示在網頁上。
  • libraries=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 地圖標記時通常會彈出的資訊視窗,就是資訊視窗

建立 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.

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 搜尋列,方便使用者輕鬆輸入起始地址。「地點自動完成」功能與其他 Google 搜尋列中的功能類似,類似於自動完成功能,但預測功能都是 Google 地圖平台中所有的「地點」功能。

建立使用者輸入欄位

返回編輯 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;
}

「自動完成」搜尋列和投影片面板一開始會隱藏,直到需要為止。

請將以下的 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 移到地圖的右上角,並指定回應中每個地點的相關欄位。

最後,呼叫 initialize 函式結尾的 initAutocompleteWidget 函式,取代含有「// TODO: Initialize the Autocomplete widget」的註解。

app.js - 初始化

 // Initialize the Places Autocomplete Widget
 initAutocompleteWidget();

執行下列指令來重新啟動伺服器,然後重新整理預覽。

go run *.go

現在,您的地圖右上角應該會顯示「自動完成」小工具,並顯示符合您所輸入條件的美國地址,並偏向地圖的顯示區域。

58e9bbbcc4bf18d1.png

在使用者選取起始地址時更新地圖

現在,您必須處理使用者從「自動完成」小工具中選取一個預測,並以該位置做為計算商店距離的基準。

請將下列程式碼加入 app.jsinitAutocompleteWidget 結尾,以取代註解「// 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 調度資源

到目前為止,我們有超棒的店家搜尋器。它能將應用程式中約 100 個位置使用的記憶體載入到記憶體中 (而非反覆讀取檔案),藉此有效運用。但如果定位器需要以不同規模運作,該怎麼辦?如果您擁有的數百個地點遍布全球各地 (或全球各地有數千個),請將這些位置保留在記憶體中,因此最好是將各個區域細分為個別檔案,這樣就會面臨自我問題。

從資料庫載入您的地點了。在這個步驟中,我們會把 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 值表示正在建立執行個體的執行個體應具備一個 vCPU 和約 3.75 GB 的記憶體。

Cloud SQL 執行個體將由 PostGresSQL 資料庫建立及初始化,且預設使用者為 postgres。這位使用者的密碼為何?好問題!他們沒有。您必須先設定一個帳戶,才能登入。

使用下列指令設定密碼:

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

然後在系統提示時輸入您選擇的密碼。

啟用 PostGIS 擴充功能

PostGIS 是 PostGresSQL 的擴充功能,可輕鬆儲存標準化類型的地理空間資料。在正常情況下,我們必須完成完整的安裝程序,才能將 PostGIS 新增至我們的資料庫。幸運的是,Cloud SQL 支援 PostGresSQL 的擴充功能之一。

以使用者 postgres 的身分在 Cloud Shell 終端機中以使用者身分登入,以連線至資料庫執行個體。

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

輸入您剛建立的密碼。接著,在 postgres=> 指令提示中新增 PostGIS 擴充功能。

CREATE EXTENSION postgis;

如果成功,輸出應會顯示「CREATE EXTENSION」(如下所示)。

指令輸出範例

CREATE EXTENSION

最後,在 postgres=> 指令提示中輸入 quit 指令,即可結束資料庫連線。

\q

將地理資料匯入資料庫

現在我們需要從 GeoJSON 檔案將所有位置資料匯入新的資料庫。

別擔心,這個問題相當複雜,而且網路上也有多項工具可幫您自動處理這個問題。我們打算使用稱為 ogr2ogr 的工具在多種地理格式之間儲存資料,以便儲存地理空間資料。選項包括 Yuper,你認為可以把表單中的 GeoJSON 轉換成 SQL 傾印檔案。如此一來,SQL 傾印檔案就可以用來建立資料表 (&p; 資料庫欄),並載入 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 函式定義,以在網址中加入搜尋點的經緯度。

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」。從重新實作位置要求的處理常式開始。

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,也就是在資料庫層執行許多非常有趣的工作,所以您完全不用擔心在程式碼中導入這些項目。

當字串剖析完成,且所有字串文字插入到正確位置後,便會引發的原始查詢,如下所示:

已剖析的引數

SELECT jsonb_build_object(
        'type',
        'FeatureCollection',
        'features',
        jsonb_agg(feature)
    )
FROM (
        SELECT jsonb_build_object(
                'type',
                'Feature',
                'id',
                ogc_fid,
                'geometry',
                ST_AsGeoJSON(wkb_geometry)::jsonb,
                'properties',
                to_jsonb(row) - 'ogc_fid' - 'wkb_geometry'
            ) AS feature
        FROM (
                SELECT *,
                    ST_Distance(
                        ST_GEOGFromWKB(wkb_geometry),
                        -- Los Angeles (LAX)
                        ST_GEOGFromWKB(st_makepoint(-97.7624043, 30.523725))
                    ) as distance
                from austinrecycling
                order by distance
                limit 25
            ) row
        where distance < 16090
    ) features

這個查詢可以用做主要的主要查詢以及某些 JSON 包裝函式。

SELECT * ... LIMIT 25 會選取各個地點的所有欄位。接著,它會使用 ST_DISTANCE 函式 (PostGIS' 一系列的地理測量函式套件) 來判斷資料庫中各個位置與前端所提供位置之間的經緯度組合。請記住,與「距離矩陣」不同,後者雖然可以提供距離,而「地理空間距離」也包含在內。為了提高效率,該範圍會使用該距離進行排序,並將最後 25 個位置傳回到使用者指定的位置。

**SELECT json_build_object(‘type', ‘F**eature') 會納入先前的查詢,擷取結果並使用該結果建立 GeoJSON Feature 物件。出乎意料的是,此查業也是在施加最大半徑“16090”的地方。是 10 英里的米數,由 Go 後端指定的硬限制。如果不想知道為什麼 WHERE 子句不會加到內部查詢 (判斷每個位置的距離),而是因為 SQL 在幕後執行的方式,所以在檢查 WHERE 子句時,可能無法計算該欄位。事實上,如果您嘗試將 WHERE 子句移到內部查詢中,就會發生錯誤。

**SELECT json_build_object(‘type', ‘FeatureColl**ection') 此查詢會納入 GeoJSON FeatureCollection 物件中 JSON 產生查詢的所有結果列。

在專案中新增 PGX 程式庫

我們必須為專案新增一個依附元件:PostGres Driver & Toolkit (可以啟用連線集區)。最簡單的方法是使用 Go 模組。在 Cloud Shell 中,使用這個指令來初始化模組:

go mod init my_locator

接著,執行下列指令,以掃描依附元件的程式碼、將依附元件清單新增至 mod 檔案,然後進行下載。

go mod tidy

最後,請執行下列指令,將依附元件直接提取至您的專案目錄,以便為 App Engine Flex 建構容器。

go mod vendor

好的,那麼您可以開始測試了!

立即測試

好的,我們剛完成 LOT 了。現在就開始吧!

為了讓您的開發機器 (是,甚至 shell) 連線至資料庫,我們必須使用 Cloud SQL Proxy 來管理資料庫連線。如何設定 Cloud SQL Proxy:

  1. 前往這裡啟用 Cloud SQL Admin API
  2. 如果您是使用本機開發機器,請安裝 Cloud SQL Proxy 工具。如果您已經使用 Cloud Shell,則無須進行這個步驟,即可略過這個步驟!請注意,操作說明會參照服務帳戶。系統已為您建立專屬帳戶,下一節將說明如何為該帳戶新增必要的權限。
  3. 請在 Cloud Shell 或您自己的終端機中建立新的分頁來啟動 Proxy。

bcca42933bfbd497.png

  1. 瀏覽 https://console.cloud.google.com/sql/instances/locations/overview 並向下捲動畫面,找出「連線名稱」欄位。複製該名稱,以便在下一個指令中使用。
  2. 在該分頁中,使用下列指令執行 Cloud SQL Proxy,並將 CONNECTION_NAME 替換為前一個步驟中顯示的連線名稱。
cloud_sql_proxy -instances=CONNECTION_NAME=tcp:5432

返回 Cloud Shell 的第一個分頁並定義 Go 所需的環境變數,以便與資料庫後端進行通訊,然後執行與之前相同的步驟執行伺服器:

如果您還沒進入專案的根目錄,請前往專案的根目錄。

cd YOUR_PROJECT_ROOT

建立下列五個環境變數 (將 YOUR_PASSWORD_HERE 替換為您在先前建立的密碼)。

export DB_USER=postgres
export DB_PASS=YOUR_PASSWORD_HERE
export DB_TCP_HOST=127.0.0.1 # Proxy
export DB_PORT=5432 #Default for PostGres
export DB_NAME=postgres

執行您的本機執行個體。

go run *.go

開啟預覽視窗後,系統會照常運作,就像修改內容一樣:您可以輸入起始地址、縮放地圖,然後按一下 [回收地點]。不過現在則由資料庫提供支援,並準備擴充規模!

9. 列出最近的商店

Directions API 的運作方式與在 Google 地圖應用程式中要求路線十分類似,只要輸入一個起點和一個目的地即可獲取兩者之間的路線。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

最後,在 [自動完成] 搜尋列中輸入 Austin 和 TX 地址,然後按一下其中一個建議即可。

地圖應以該地址為中心,側欄應會顯示商店位置,並與選取的地址相距。如下圖所示:

96e35794dd0e88c9.png

10. 設定地圖樣式

將地圖與眾不同的視覺效果,是讓地圖與眾不同的視覺效果。您可以透過雲端式地圖樣式設定功能,在 Cloud Console 中自訂地圖,並使用雲端式地圖樣式設定功能 (測試版)。如果您偏好使用非 Beta 版功能設定地圖樣式,則可使用地圖樣式說明文件,透過程式產生 JSON 格式的地圖樣式。以下操作說明可引導您使用雲端式地圖樣式設定功能 (測試版)。

建立地圖 ID

首先,開啟 Cloud Console 並在搜尋框中輸入「地圖管理」,然後輸入「地圖管理」即可。按一下顯示「地圖管理」 (Google 地圖) 的結果。64036dd0ed200200.png

您會在上方附近 (搜尋框下方) 看到 [建立新地圖 ID] 按鈕。按一下該名稱,然後輸入您要設定的名稱。針對「地圖類型」,請務必選取 [JavaScript],更多選項顯示時,從清單中選取 [向量]。最終結果應如下所示。

70f55a759b4c4212.png

按一下 [下一步],您即可使用全新的「地圖 ID」進行升級。現在,您可以視需要複製資料,但不用擔心,日後就能輕鬆查詢。

接下來,我們要建立要套用到該地圖的樣式。

建立地圖樣式

如果您仍在 Google Cloud Console 的「地圖」部分中,請按一下左側導覽選單底部的 [地圖樣式]。或者,就像建立地圖 ID 一樣,您可以在搜尋框中輸入「地圖樣式」並選取「地圖樣式」 (Google 地圖),即可找到正確的網頁,如下圖所示。

9284cd200f1a9223.png

接著,按一下靠近頂端附近的「+ 建立新地圖樣式」按鈕

  1. 如果您想要比對這個實驗室中顯示的地圖樣式,請點選 [IMPORT 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. index.html 的指令碼標記中加入地圖 ID 做為網址參數
  2. Add 當您在 initMap() 方法中建立地圖時,會有一個地圖 ID 做為建構函式引數。

在 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. 部署至實際工作環境

如果您希望應用程式能夠從 AppEngine Flex 執行 (而不只是在開發機器 / Cloud Shell 上在本機網路伺服器執行,這是您所執行的操作),其實十分簡單。只要加入幾個項目,資料庫即可在實際工作環境中運作。您可以在從 App Engine Flex 連線至 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

請注意,由於這個應用程式是透過 AppEngine Flex** 連線,因此 DB_TCP_HOST 的值應為 172.17.0.1。** 這是因為它是透過 Proxy 與 Cloud SQL 通訊,方式與您類似。

將 SQL Client 權限新增至 App Engine Flex 服務帳戶

前往 Cloud Console 的 IAM 管理頁面,尋找名稱與 service-PROJECT_NUMBER@gae-api-prod.google.com.iam.gserviceaccount.com 格式相符的服務帳戶。這是 App Engine Flex 將用來連線至資料庫的服務帳戶。按一下該列末端的 [編輯] 按鈕,然後新增「Cloud SQL 用戶端」角色。

b04ccc0b4022b905.png

將專案程式碼複製到 Go 路徑

如要讓 AppEngine 執行您的程式碼,它必須能在 Go 路徑中尋找相關檔案。確認您位於專案的根目錄。

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

如果您在 Cloud Shell 以外的地方執行 gcloud,則執行 gcloud app browse 會開啟新的瀏覽器分頁。

12. (建議) 清除

執行這個程式碼研究室可避免超出 BigQuery 處理和 Maps Platform API 呼叫的免費方案限制,但如果您僅為了教育作業而執行,而且不希望日後產生任何費用,請刪除與專案相關聯的資源,最簡單的方式就是刪除專案本身。

刪除專案

前往 GCP Console 的「Cloud Resource Manager」頁面:

在專案清單中,選取我們正在處理的專案,然後按一下 [刪除]。系統會提示您輸入專案 ID。輸入後,按一下 [關機]。

或者,您也可以執行下列指令,將預留位置 gcloud 替換成專案 ID,以直接從 Cloud Shell 中刪除整個專案:

gcloud projects delete GOOGLE_CLOUD_PROJECT

13. 恭喜

恭喜!您已成功完成程式碼研究室

或者,您直接前往了最後一頁。恭喜!您已跳轉到最後一頁

在這個程式碼研究室的開發過程中,您已運用下列技術:

延伸閱讀

我們仍有許多要學習的技巧。下面是一些實用的程式碼連結,但目前還沒有時間在這個程式碼研究室中。不過,如果能根據自己的特定需求打造店家搜尋器解決方案,一定能派上用場。