1. 簡介
摘要
假設您要在地圖上標示許多地點,並希望使用者能查看這些地點的位置,以及找出想去的地點。常見範例如下:
- 零售商網站上的店家搜尋器
- 即將舉行的選舉投票所地圖
- 電池回收箱等特殊地點的目錄
建構項目
在本程式碼研究室中,您將建立定位器,從特定地點的即時資料動態饋給中提取資訊,協助使用者找到離起點最近的地點。這個全方位定位器可處理的場所數量遠大於簡易商店定位器,後者最多只能處理 25 個商店位置。
課程內容
本程式碼研究室會使用開放資料集,模擬大量商店位置的預先填入中繼資料,讓您專心學習重要技術概念。
- Maps JavaScript API:在自訂網頁地圖上顯示大量地點
- GeoJSON:儲存地點中繼資料的格式
- 地點自動完成:協助使用者更快更準確地提供出發地點
- Go:用於開發應用程式後端的程式設計語言。後端會與資料庫互動,並以格式化的 JSON 形式將查詢結果傳回前端。
- App Engine:用於託管網頁應用程式
必要條件
- 具備 HTML 和 JavaScript 的基礎知識
- Google 帳戶
2. 做好準備
在下一節的步驟 3 中,請為本程式碼研究室啟用 Maps JavaScript API、Places API 和 Distance Matrix API。
開始使用 Google 地圖平台
如果您從未使用過 Google 地圖平台,請按照「開始使用 Google 地圖平台」指南或「開始使用 Google 地圖平台」播放清單中的操作說明,完成下列步驟:
- 建立帳單帳戶。
- 建立專案。
- 啟用 Google 地圖平台 API 和 SDK (如上一節所列)。
- 產生 API 金鑰。
啟用 Cloud Shell
在本程式碼研究室中,您會使用 Cloud Shell。Cloud Shell 是在 Google Cloud 中執行的指令列環境,可存取 Google Cloud 上執行的產品和資源,因此您完全可以透過網路瀏覽器代管及執行專案。
如要從 Cloud Shell 啟動 Cloud Shell,請按一下「啟用 Cloud Shell」圖示 (系統應會在幾分鐘內完成佈建作業並連線至環境)。
這會在瀏覽器底部開啟新的殼層,並可能顯示簡介插頁式廣告。
確認專案
連線至 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 控制台手動啟用 AppEngine Flex API。這樣做不僅會啟用 API,還會建立 App Engine 彈性環境服務帳戶,這個經過驗證的帳戶會代表使用者與 Google 服務 (例如 SQL 資料庫) 互動。
3. Hello, World
後端:以 Go 撰寫的 Hello World
在 Cloud Shell 執行個體中,您首先要建立 Go App Engine 彈性應用程式,做為本程式碼研究室其餘部分的基礎。
在 Cloud Shell 的工具列中,按一下「開啟編輯器」按鈕,即可在新分頁中開啟程式碼編輯器。這個網頁版程式碼編輯器可讓您輕鬆編輯 Cloud Shell 執行個體中的檔案。
接著,點選「在新視窗中開啟」圖示,將編輯器和終端機移至新分頁。
在新分頁底部的終端機中,建立新的 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 彈性執行階段。如要瞭解這個檔案中設定項目的意義,請參閱 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 資料 (而非簡單的硬式編碼字串) 回應。
完成這些步驟後,您應該會看到如下所示的編輯器:
立即試用
如要測試這個應用程式,您可以在 Cloud Shell 執行個體中執行 App Engine 開發伺服器。返回 Cloud Shell 指令列,然後輸入下列指令:
go run *.go
您會看到幾行記錄輸出內容,顯示您確實是在 Cloud Shell 執行個體上執行開發伺服器,且 Hello World 網頁應用程式正在接聽 localhost 通訊埠 8080。如要開啟這個應用程式的網頁瀏覽器分頁,請按下「網頁預覽」按鈕,然後在 Cloud Shell 工具列中選取「透過以下通訊埠預覽:8080」選單項目。
按一下這個選單項目後,網路瀏覽器會開啟新分頁,並顯示 App Engine 開發伺服器提供的「Hello, world!」字串。
在下一個步驟中,您將把奧斯丁市的回收資料新增至這個應用程式,並開始以圖表呈現。
4. 取得目前資料
GeoJSON:GIS 領域的通用語言
上一個步驟提到,您會在 Go 程式碼中建立處理常式,將 GeoJSON 資料轉譯至網頁瀏覽器。但什麼是 GeoJSON?
在地理資訊系統 (GIS) 領域中,我們需要在電腦系統之間傳達地理實體的相關知識。地圖很適合人類閱讀,但電腦通常偏好更容易消化的資料格式。
GeoJSON 是一種格式,可將地理資料結構編碼,例如德州奧斯汀的回收地點座標。GeoJSON 已在 網際網路工程任務小組標準中標準化,稱為 RFC7946。GeoJSON 是以 JSON (JavaScript 物件標記法) 定義,而 JSON 本身是由 JavaScript 標準化機構 Ecma International,在 ECMA-404 中標準化。
重點是,GeoJSON 是一種廣泛支援的線路格式,可傳達地理知識。本程式碼研究室會以以下方式使用 GeoJSON:
- 使用 Go 套件將 Austin 資料剖析為內部 GIS 專屬資料結構,用於篩選所要求的資料。
- 將要求的資料序列化,以便在網頁伺服器和網頁瀏覽器之間傳輸。
- 使用 JavaScript 程式庫將回應轉換為地圖上的標記。
這樣一來,您就不必編寫剖析器和產生器,將連線資料串轉換為記憶體內表示法,因此可省下大量程式碼輸入作業。
擷取資料
德州奧斯汀市開放資料入口網站提供公用資源的地理空間資訊,供大眾使用。在本程式碼研究室中,您將視覺化呈現回收地點資料集。
您將使用 Maps JavaScript API 的資料層,在地圖上以標記顯示資料。
首先,請從奧斯汀市網站將 GeoJSON 資料下載到應用程式中。
- 在 Cloud Shell 執行個體的指令列視窗中,輸入 [CTRL] + [C] 關閉伺服器。
- 在
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"
"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 後端已提供實用功能: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 控制台的「APIs & Services -> Credentials」(API 和服務 -> 憑證) 頁面,擷取或產生新的 API 金鑰。 - 請注意,網址包含
callback=initialize.
參數。現在我們要建立包含該回呼函式的 JavaScript 檔案。應用程式會從後端載入位置資訊,然後傳送至 Maps 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;
};
這段程式碼會在 Google 地圖上顯示商店位置。如要測試目前為止的內容,請從指令列返回父項目錄:
cd ..
現在,請使用下列指令,再次以開發模式執行應用程式:
go run *.go
預覽方式與先前相同。地圖上會顯示類似下方的綠色小圓圈。
您已經算繪地圖位置,而我們才完成一半的程式碼研究室!太棒了!現在,我們來新增一些互動元素。
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
函式定義替換為這個略有不同的版本,其中最後一行會呼叫 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
,並附上所選商店的資訊。
如果伺服器仍在執行中,請停止並重新啟動。重新整理地圖頁面,然後按一下地圖標記。系統會彈出一個資訊視窗,顯示商家名稱和地址,如下所示:
7. 取得使用者的起始位置
商店定位器使用者通常想知道哪間商店離自己最近,或是離他們預計出發的地點最近。新增 Place Autocomplete 搜尋列,方便使用者輕鬆輸入起始地址。Place Autocomplete 提供預測查詢字串功能,與其他 Google 搜尋列的 Autocomplete 功能類似,但預測結果都是 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;
}
自動完成搜尋列和滑出式面板一開始都會隱藏,直到需要時才會顯示。
將 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
結尾加入下列程式碼,定義函式以在地圖中加入 Autocomplete 小工具。
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
};
這段程式碼會限制自動完成建議只傳回地址 (因為 Place Autocomplete 也能比對地點名稱和行政區位置),並將傳回的地址限制在美國境內。新增這些選用規格後,使用者只需輸入較少的字元,即可縮小預測範圍,顯示他們要尋找的地址。
接著,它會將您建立的 Autocomplete div
移至地圖的右上角,並指定回應中應傳回每個地點的哪些欄位。
最後,在 initialize
函式的結尾呼叫 initAutocompleteWidget
函式,並取代「// TODO: Initialize the Autocomplete widget
」註解。
app.js - 初始化
// Initialize the Places Autocomplete Widget
initAutocompleteWidget();
執行下列指令重新啟動伺服器,然後重新整理預覽畫面。
go run *.go
現在地圖的右上角應該會顯示 Autocomplete 小工具,其中會根據您輸入的內容,顯示與地圖可見區域相符的美國地址。
使用者選取起始地址時更新地圖
現在,您需要處理使用者從自動完成小工具選取預測結果的情況,並以該地點做為計算商店距離的依據。
將下列程式碼新增至 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 後端,以便在收到要求時,從該資料庫 (而非本機快取) 提取結果。
建立含有 PostgreSQL 資料庫的 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 執行個體,並使用預設使用者 postgres
初始化 PostgreSQL 資料庫。這位使用者的密碼是什麼?好問題!他們沒有帳戶。您必須先設定一個,才能登入。
使用下列指令設定密碼:
gcloud sql users set-password postgres \ --instance=locations --prompt-for-password
然後按照系統提示輸入所選密碼。
啟用 PostGIS 擴充功能
PostGIS 是 PostgreSQL 的擴充功能,可讓您更輕鬆地儲存標準類型的地理空間資料。在正常情況下,我們必須完成完整的安裝程序,才能將 PostGIS 新增至資料庫。幸好,這是 Cloud SQL 支援的 PostgreSQL 擴充功能之一。
在 Cloud Shell 終端機中執行下列指令,以使用者 postgres
的身分登入資料庫執行個體。
gcloud sql connect locations --user=postgres --quiet
輸入您剛建立的密碼。現在請在 postgres=>
命令提示字元中新增 PostGIS 擴充功能。
CREATE EXTENSION postgis;
如果成功,輸出內容應會顯示 CREATE EXTENSION,如下所示。
指令輸出範例
CREATE EXTENSION
最後,在 postgres=>
命令提示字元輸入 quit 指令,結束資料庫連線。
\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
函式定義替換為這個版本,以便在網址中加入感興趣的經緯度。
app.js - fetchStores
const fetchStores = async (center) => {
const url = `/data/dropoffs?centerLat=${center.lat}¢erLng=${center.lng}`;
const response = await fetch(url);
return response.json();
};
完成本程式碼研究室的這項步驟後,回應只會傳回最靠近 center
參數中提供地圖座標的商店。在 initialize
函式中進行初始擷取時,本實驗室提供的範例程式碼會使用德州奧斯汀的中心座標。
由於 fetchStores
現在只會傳回部分商店位置,因此每當使用者變更起始位置時,我們都需要重新擷取商店。
更新 initAutocompleteWidget
函式,在設定新來源時重新整理位置。這需要進行兩項編輯:
- 在 initAutocompleteWidget 中,找出
place_changed
監聽器的回呼。取消註解清除現有圓圈的程式碼行,這樣一來,每當使用者從 Place Autocomplete 搜尋列選取地址時,該行程式碼就會執行。
app.js - initAutocompleteWidget
autocomplete.addListener("place_changed", async () => {
circles.forEach((c) => c.setMap(null)); // clear existing stores
// ...
- 每當選取的來源變更時,系統就會更新變數 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,這會在資料庫層執行許多有趣的操作,因此您不必擔心在程式碼中實作任何操作。
字串剖析完畢,且所有字串常值都插入適當位置後,觸發的原始查詢如下所示:
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 Proxy 管理資料庫連線。如要設定 Cloud SQL Proxy,請按照下列步驟操作:
- 按這裡啟用 Cloud SQL Admin API
- 如果您使用本機開發電腦,請安裝 Cloud SQL Proxy 工具。如果您使用 Cloud Shell,可以略過這個步驟,因為 Cloud Shell 已安裝此工具!請注意,操作說明會提及服務帳戶。系統已為您建立一個帳戶,我們將在下一節說明如何為該帳戶新增必要權限。
- 建立新分頁 (在 Cloud Shell 或您自己的終端機中),啟動 Proxy。
- 前往
https://console.cloud.google.com/sql/instances/locations/overview
,然後向下捲動找出「連線名稱」欄位。複製該名稱,以便在下一個指令中使用。 - 在該分頁中執行 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 地圖應用程式中要求路線規劃的體驗非常相似,只要輸入單一起點和單一目的地,即可取得兩者之間的路線。距離矩陣 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
函式,每當從 Place Autocomplete 搜尋列選取新的來源時,就計算商店距離。在 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
最後,在「自動完成」搜尋列中輸入德州奧斯汀的地址,然後按一下其中一項建議。
地圖應會以該地址為中心,並顯示側欄,列出商店位置,順序是依據與所選地址的距離。範例如下圖所示:
10. 設定地圖樣式
如要讓地圖在視覺上與眾不同,最有效的方式就是為地圖新增樣式。您可以利用雲端地圖樣式功能,在 Cloud 控制台中使用雲端地圖樣式 (Beta 版) 來自訂地圖。如果想使用非 Beta 版功能設定地圖樣式,請參閱地圖樣式說明文件,瞭解如何產生 JSON,以程式輔助方式設定地圖樣式。下列操作說明將引導您使用雲端地圖樣式設定功能 (Beta 版)。
建立地圖 ID
首先,開啟 Cloud 控制台,然後在搜尋方塊中輸入「地圖管理」。按一下顯示「地圖管理 (Google 地圖)」的結果。
您會在頂端附近 (搜尋方塊正下方) 看到「建立新地圖 ID」按鈕。按一下該名稱,然後填入所需名稱。請務必選取「JavaScript」做為地圖類型,然後在顯示更多選項時,從清單中選取「向量」。最終結果應如下圖所示。
按一下「下一步」,系統就會顯示新的地圖 ID。你可以現在複製,但別擔心,之後也很容易查閱。
接著,我們要建立套用至該地圖的樣式。
建立地圖樣式
如果您仍在 Cloud Console 的「地圖」部分,請按一下左側導覽選單底部的「地圖樣式」。否則,就像建立地圖 ID 一樣,您可以在搜尋框中輸入「地圖樣式」,然後從結果中選取「地圖樣式 (Google 地圖)」,即可找到正確的頁面,如下圖所示。
接著,按一下頂端附近的「+ 建立新的地圖樣式」按鈕
- 如要比照本實驗室中顯示的地圖樣式,請按一下「匯入 JSON」分頁,然後貼上以下 JSON Blob。如要自行建立,請選取要使用的地圖樣式。然後點選「下一步」。
- 選取剛建立的地圖 ID,將該 ID 與這個樣式建立關聯,然後再次點按「下一步」。
- 此時,您可以選擇進一步自訂地圖樣式。如要探索這項功能,請按一下「在樣式編輯器中進行自訂」,然後調整顏色和選項,直到找到喜歡的地圖樣式為止。否則請按一下「略過」。
- 在下一個步驟中,輸入樣式名稱和說明,然後按一下「儲存並發布」。
以下是選用的 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
您已費盡心思建立這個地圖樣式,現在要如何在自己的地圖中實際使用這個地圖樣式呢?您需要進行兩項小變更:
- 在
index.html
的指令碼標記中,將地圖 ID 新增為網址參數 - 在
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>
...
在定義常數 map
的 app.js
initMap
方法中,取消註解 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 樣式的範例。
11. 部署至正式環境
如要查看從 App Engine 彈性環境執行的應用程式 (而不只是開發機器 / Cloud Shell 上的本機網頁伺服器,也就是您目前執行的應用程式),做法非常簡單。我們只需要新增幾項內容,就能在正式環境中存取資料庫。如需完整說明,請參閱「從 App Engine 彈性環境連線至 Cloud SQL」說明文件頁面。
在 app.yaml 中新增環境變數
首先,您需要將用於在本機測試的所有環境變數,新增至應用程式 app.yaml
檔案的底部。
- 前往 https://console.cloud.google.com/sql/instances/locations/overview 查詢執行個體連線名稱。
- 將下列程式碼貼到
app.yaml
結尾。 - 將
YOUR_DB_PASSWORD_HERE
改成您先前為postgres
使用者名稱建立的密碼。 - 將
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 值。這是因為它會透過 Proxy 與 Cloud SQL 通訊,與您之前的方式類似。
將 SQL 用戶端權限新增至 App Engine 彈性環境服務帳戶
前往 Cloud Console 的 IAM 管理頁面,然後找出名稱符合 service-PROJECT_NUMBER@gae-api-prod.google.com.iam.gserviceaccount.com
格式的服務帳戶。這是 App Engine 彈性環境用來連線至資料庫的服務帳戶。按一下資料列尾端的「編輯」按鈕,然後新增「Cloud SQL 用戶端」角色。
將專案程式碼複製到 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 主控台的「Cloud Resource Manager」頁面:
在專案清單中,選取我們一直在處理的專案,然後按一下「刪除」。系統會提示您輸入專案 ID。輸入後,按一下「Shut Down」(關機)。
或者,您也可以直接在 Cloud Shell 中使用 gcloud
刪除整個專案,方法是執行下列指令,並將預留位置 GOOGLE_CLOUD_PROJECT
替換為您的專案 ID:
gcloud projects delete GOOGLE_CLOUD_PROJECT
13. 恭喜
恭喜!您已成功完成程式碼研究室!
或是直接跳到最後一頁。恭喜!你已快速瀏覽至最後一頁!
在本程式碼研究室中,您已使用下列技術:
- Maps JavaScript API
- Maps JavaScript API 的距離矩陣服務 (另有 Distance Matrix API)
- Maps JavaScript API 的 Places Library (也稱為 Places API)
- App Engine 彈性環境 (Go)
- Cloud SQL API
延伸閱讀
我們還有很多關於這些技術的知識需要學習。以下連結提供一些實用資訊,內容涵蓋我們在這個程式碼研究室中沒有時間介紹的主題,但肯定有助於您建構符合特定需求的商店定位器解決方案。