1. 總覽
如果資料集中的模式與位置資訊有關,地圖就是非常實用的工具,可將這些模式視覺化。這項關係可以是地點名稱、特定經緯度值,或是具有特定界線的區域名稱,例如人口普查區或郵遞區號。
如果這些資料集非常龐大,使用傳統工具查詢及視覺化資料可能會很困難。使用 Google BigQuery 查詢資料,並透過 Google Maps API 建構查詢及將輸出內容視覺化,您就能快速探索資料中的地理模式,不必進行太多設定或編碼,也不必管理儲存大型資料集的系統。
建構項目
在本程式碼研究室中,您將編寫及執行一些查詢,示範如何使用 BigQuery,針對非常龐大的公開資料集提供以位置為準的洞察資料。您也會建構網頁,使用 Google 地圖平台 JavaScript API 載入地圖,然後使用 適用於 JavaScript 的 Google API 用戶端程式庫和 BigQuery API,對相同的大型公開資料集執行空間查詢並以視覺化方式呈現。
課程內容
- 如何使用 BigQuery,透過 SQL 查詢、使用者定義函式 和 BigQuery API,在幾秒內查詢 PB 級別的位置資料集
- 如何使用 Google 地圖平台在網頁中新增 Google 地圖,並讓使用者在地圖上繪製形狀
- 如何在地圖上呈現針對大型資料集執行的查詢,如下方範例圖片所示,這張地圖顯示 2016 年從帝國大廈附近街區出發的行程,以及計程車下車地點的密度。
軟硬體需求
- HTML、CSS、JavaScript、SQL 和 Chrome 開發人員工具的基本知識
- 新式網路瀏覽器,例如最新版的 Chrome、Firefox、Safari 或 Edge。
- 您選擇的文字編輯器或 IDE
技術
BigQuery
BigQuery 是 Google 的資料分析服務,適用於非常龐大的資料集。這項服務提供 RESTful API,並支援以 SQL 編寫的查詢。如果資料包含經緯度值,即可依位置查詢資料。優點是您可以透過視覺化方式探索非常龐大的資料集,找出模式,不必管理任何伺服器或資料庫基礎架構。無論資料表有多大,您都能透過 BigQuery 的大規模擴充性和代管基礎架構,在幾秒內獲得問題解答。
Google 地圖平台
Google 地圖平台提供 Google 地圖、地點和路線資料的程式輔助存取權。目前已有超過 200 萬個網站和應用程式採用這項服務,為使用者提供嵌入式地圖和位置查詢功能。
Google 地圖平台 JavaScript API 繪圖圖層可讓您在地圖上繪製形狀。這些值可以轉換為輸入內容,針對 BigQuery 資料表執行查詢,這些資料表的資料欄中儲存了經緯度值。
如要開始使用,您需要啟用 BigQuery 和 Maps API 的 Google Cloud Platform 專案。
2. 開始設定
Google 帳戶
如果您還沒有 Google 帳戶 (Gmail 或 Google 應用程式),請先建立帳戶。
建立專案
登入 Google Cloud Platform 主控台 ( console.cloud.google.com),然後建立新專案。畫面頂端會顯示「專案」下拉式選單:
點選這個專案下拉式選單後,會出現可建立新專案的選單項目:
在「輸入專案的新名稱」方塊中,輸入新專案的名稱,例如「BigQuery Codelab」:
系統會為您產生專案 ID。所有 Google Cloud 專案的專案 ID 都是不重複的名稱。記下專案 ID,後續步驟將會用到。上述名稱已有人使用,因此不適用於你。在本程式碼研究室中,凡是看到 YOUR_PROJECT_ID,請一律插入您自己的專案 ID。
啟用計費功能
如要註冊 BigQuery,請使用上一個步驟中選取或建立的專案。您必須為這項專案啟用計費功能。啟用計費功能後,即可啟用 BigQuery API。
計費功能的啟用方式取決於您的目的是建立新專案,還是重新啟用現有專案的計費功能。
Google 提供為期 12 個月的免費試用方案,最多可使用價值 $300 美元的 Google Cloud Platform 服務,您或許能將這項優惠用於本 Codelab。如要瞭解詳情,請前往 https://cloud.google.com/free/。
新專案
建立新專案時,系統會提示您選擇要與該專案連結的帳單帳戶。如果您只有一個帳單帳戶,系統會自動將該帳戶與專案連結。
如果您沒有帳單帳戶,就必須先建立一個帳單帳戶並啟用專案的計費功能,才能使用 Google Cloud Platform 的許多功能。如要建立新的帳單帳戶,以及啟用專案的計費功能,請按照「建立新的計費帳戶」中的操作說明。
現有專案
如果您有暫時停用計費功能的專案,可以重新啟用該專案的計費功能:
- 前往 Cloud Platform Console。
- 在專案清單中,選取您要重新啟用計費功能的專案。
- 開啟主控台左側的選單,然後選取「帳單」
。系統提示您選取帳單帳戶。
- 按一下 [設定帳戶]。
建立新的帳單帳戶
建立新帳單帳戶的方式如下:
- 前往 Cloud Platform Console 並登入,或如果您還沒有帳戶,請註冊一個新帳戶。
- 開啟主控台左側的選單,然後選取「帳單」
- 按一下 [新增帳單帳戶] 選項。(請注意,如果這不是您的第一個帳單帳戶,請先按一下頁面頂端附近現有帳單帳戶的名稱,開啟帳單帳戶清單,然後按一下「管理帳單帳戶」。)
- 輸入帳單帳戶名稱,並輸入您的帳單資訊。您看到的選項會因帳單地址的國家/地區而有所不同。請注意,如果是美國帳戶,建立帳戶後就不能更改稅籍。
- 按一下 [提交資訊並啟用計費功能]。
根據預設,建立帳單帳戶的使用者就是該帳戶的帳單管理員。
如要瞭解如何驗證銀行帳戶及新增付款備用方法,請參閱「新增、移除或更新付款方式」。
啟用 BigQuery API
如要在專案中啟用 BigQuery API,請前往控制台中的 BigQuery API 頁面 Marketplace,然後按一下藍色的「啟用」按鈕。
3. 在 BigQuery 中查詢位置資料
如要查詢以經緯度值儲存在 BigQuery 中的位置資料,有三種方法。
- 矩形查詢:將感興趣的區域指定為查詢,選取最小和最大經緯度範圍內的所有列。
- 半徑查詢:使用 Haversine 公式和數學函式計算某個點周圍的圓形,模擬地球形狀,藉此指定感興趣的區域。
- 多邊形查詢:指定自訂形狀,並使用使用者定義函式表示點在多邊形內的邏輯,測試每列的緯度和經度是否位於形狀內。
如要開始使用,請前往 Google Cloud Platform 主控台的「BigQuery」專區,使用「查詢編輯器」對紐約市計程車資料執行下列查詢。
標準 SQL 與舊版 SQL 的比較
BigQuery 支援兩種 SQL 版本:舊版 SQL 和標準 SQL。後者是 2011 年的 ANSI 標準。在本教學課程中,我們會使用標準 SQL,因為標準 SQL 遵循標準的程度較高。
如要在 BigQuery 編輯器中執行舊版 SQL,請按照下列步驟操作:
- 按一下「更多」按鈕
- 從下拉式選單中選取「查詢設定」
- 在「SQL 方言」下方,選取「舊版」圓形按鈕
- 按一下「儲存」按鈕
矩形查詢
在 BigQuery 中建構矩形查詢相當簡單。您只需要新增 WHERE
子句,將傳回的結果限制在緯度和經度的最小值與最大值之間。
請在 BigQuery 控制台中試用下列範例。這項查詢會針對在矩形區域內 (包含中城和曼哈頓下城) 開始的行程,查詢一些平均行程統計資料。您可以嘗試兩個不同的位置,取消第二個 WHERE
子句的註解,即可對從 JFK 機場出發的行程執行查詢。
SELECT
ROUND(AVG(tip_amount),2) as avg_tip,
ROUND(AVG(fare_amount),2) as avg_fare,
ROUND(AVG(trip_distance),2) as avg_distance,
ROUND(AVG(tip_proportion),2) as avg_tip_pc,
ROUND(AVG(fare_per_mile),2) as avg_fare_mile FROM
(SELECT
pickup_latitude, pickup_longitude, tip_amount, fare_amount, trip_distance, (tip_amount / fare_amount)*100.0 as tip_proportion, fare_amount / trip_distance as fare_per_mile
FROM `bigquery-public-data.new_york_taxi_trips.tlc_yellow_trips_2015`
WHERE trip_distance > 0.01 AND fare_amount <100 AND payment_type = "1" AND fare_amount > 0
)
--Manhattan
WHERE pickup_latitude < 40.7679 AND pickup_latitude > 40.7000 AND pickup_longitude < -73.97 and pickup_longitude > -74.01
--JFK
--WHERE pickup_latitude < 40.654626 AND pickup_latitude > 40.639547 AND pickup_longitude < -73.771497 and pickup_longitude > -73.793755
這兩項查詢的結果顯示,這兩個地點的平均行程距離、車資和小費有很大的差異。
曼哈頓
avg_tip | avg_fare | avg_distance | avg_tip_pc | avg_fare_mile |
2.52 | 12.03 | 9.97 | 22.39 | 5.97 |
JFK
avg_tip | avg_fare | avg_distance | avg_tip_pc | avg_fare_mile |
9.22 | 48.49 | 41.19 | 22.48 | 4.36 |
半徑查詢
如果您具備一些數學知識,也可以輕鬆在 SQL 中建構半徑查詢。您可以使用 BigQuery 的舊版 SQL 數學函式,透過 Haversine 公式建構 SQL 查詢,估算地球表面的圓形區域或球面帽。
以下是 BigQuery SQL 陳述式範例,用於以 40.73943, -73.99585
為圓心,半徑為 0.1 公里的圓形查詢。
系統會使用 111.045 公里的常數值,估算一度代表的距離。
這是根據 http://www.plumislandmedia.net/mysql/haversine-mysql-nearest-loc/ 上的範例所建立:
SELECT pickup_latitude, pickup_longitude,
(111.045 * DEGREES(
ACOS(
COS( RADIANS(40.73943) ) *
COS( RADIANS( pickup_latitude ) ) *
COS(
RADIANS( -73.99585 ) -
RADIANS( pickup_longitude )
) +
SIN( RADIANS(40.73943) ) *
SIN( RADIANS( pickup_latitude ) )
)
)
) AS distance FROM `project.dataset.tableName`
HAVING distance < 0.1
Haversine 公式的 SQL 看起來很複雜,但您只需要插入圓心座標、半徑,以及 BigQuery 的專案、資料集和資料表名稱。
以下查詢範例會計算帝國大廈 100 公尺內上車的平均行程統計資料。複製並貼到 BigQuery 網頁版控制台,即可查看結果。變更經緯度,與其他區域 (例如布朗克斯) 的位置進行比較。
#standardSQL
CREATE TEMPORARY FUNCTION Degrees(radians FLOAT64) RETURNS FLOAT64 AS
(
(radians*180)/(22/7)
);
CREATE TEMPORARY FUNCTION Radians(degrees FLOAT64) AS (
(degrees*(22/7))/180
);
CREATE TEMPORARY FUNCTION DistanceKm(lat FLOAT64, lon FLOAT64, lat1 FLOAT64, lon1 FLOAT64) AS (
Degrees(
ACOS(
COS( Radians(lat1) ) *
COS( Radians(lat) ) *
COS( Radians(lon1 ) -
Radians( lon ) ) +
SIN( Radians(lat1) ) *
SIN( Radians( lat ) )
)
) * 111.045
);
SELECT
ROUND(AVG(tip_amount),2) as avg_tip,
ROUND(AVG(fare_amount),2) as avg_fare,
ROUND(AVG(trip_distance),2) as avg_distance,
ROUND(AVG(tip_proportion), 2) as avg_tip_pc,
ROUND(AVG(fare_per_mile),2) as avg_fare_mile
FROM
-- EMPIRE STATE BLDG 40.748459, -73.985731
-- BRONX 40.895597, -73.856085
(SELECT pickup_latitude, pickup_longitude, tip_amount, fare_amount, trip_distance, tip_amount/fare_amount*100 as tip_proportion, fare_amount / trip_distance as fare_per_mile, DistanceKm(pickup_latitude, pickup_longitude, 40.748459, -73.985731)
FROM `bigquery-public-data.new_york_taxi_trips.tlc_yellow_trips_2015`
WHERE
DistanceKm(pickup_latitude, pickup_longitude, 40.748459, -73.985731) < 0.1
AND fare_amount > 0 and trip_distance > 0
)
WHERE fare_amount < 100
查詢結果如下。你可以看到平均小費、車資、行程距離、小費與車資的比例,以及每英里行駛的平均車資,這些都有很大的差異。
帝國大廈:
avg_tip | avg_fare | avg_distance | avg_tip_pc | avg_fare_mile |
1.17 | 11.08 | 45.28 | 10.53 | 6.42 |
布朗克斯
avg_tip | avg_fare | avg_distance | avg_tip_pc | avg_fare_mile |
0.52 | 17.63 | 4.75 | 4.74 | 10.9 |
多邊形查詢
SQL 不支援使用矩形和圓形以外的任意形狀進行查詢。BigQuery 沒有任何原生幾何資料型別或空間索引,因此如要使用多邊形形狀執行查詢,您需要採用不同於簡單 SQL 查詢的方法。其中一種方法是在 JavaScript 中定義幾何函式,並在 BigQuery 中以使用者定義函式 (UDF) 執行。
許多幾何運算都可以用 JavaScript 編寫,因此您可以輕鬆擷取一個運算,並針對包含經緯度值的 BigQuery 資料表執行。您需要透過 UDF 傳遞自訂多邊形,並針對每個資料列執行測試,只傳回經緯度位於多邊形內的資料列。如要進一步瞭解 UDF,請參閱 BigQuery 參考資料。
多邊形內點演算法
在 JavaScript 中,有很多方法可以計算點是否位於多邊形內。以下是從 C 移植的知名實作,使用光線追蹤演算法判斷點是否位於多邊形內外,方法是計算無限長線穿過形狀邊界的次數。只需幾行程式碼即可完成:
function pointInPoly(nvert, vertx, verty, testx, testy){
var i, j, c = 0;
for (i = 0, j = nvert-1; i < nvert; j = i++) {
if ( ((verty[i]>testy) != (verty[j]>testy)) &&
(testx < (vertx[j]-vertx[i]) * (testy-verty[i]) / (verty[j]-verty[i]) + vertx[i]) )
c = !c;
}
return c;
}
移植到 JavaScript
這個演算法的 JavaScript 版本如下所示:
/* This function includes a port of C code to calculate point in polygon
* see http://www.ecse.rpi.edu/Homepages/wrf/Research/Short_Notes/pnpoly.html for license
*/
function pointInPoly(polygon, point){
// Convert a JSON poly into two arrays and a vertex count.
let vertx = [],
verty = [],
nvert = 0,
testx = point[0],
testy = point[1];
for (let coord of polygon){
vertx[nvert] = coord[0];
verty[nvert] = coord[1];
nvert ++;
}
// The rest of this function is the ported implementation.
for (let i = 0, let j = nvert - 1; i < nvert; j = i++) {
if ( ((verty[i] > testy) != (verty[j] > testy)) &&
(testx < (vertx[j] - vertx[i]) * (testy - verty[i]) / (verty[j] - verty[i]) + vertx[i]) )
c = !c;
}
return c;
}
在 BigQuery 中使用標準 SQL 時,UDF 方法只需要單一陳述式,但 UDF 必須在陳述式中定義為暫時性函式。舉例來說,將下列 SQL 陳述式貼到「查詢編輯器」視窗。
CREATE TEMPORARY FUNCTION pointInPolygon(latitude FLOAT64, longitude FLOAT64)
RETURNS BOOL LANGUAGE js AS """
let polygon=[[-73.98925602436066,40.743249676056955],[-73.98836016654968,40.74280666503313],[-73.98915946483612,40.741676770346295],[-73.98967981338501,40.74191656974406]];
let vertx = [],
verty = [],
nvert = 0,
testx = longitude,
testy = latitude,
c = false,
j = nvert - 1;
for (let coord of polygon){
vertx[nvert] = coord[0];
verty[nvert] = coord[1];
nvert ++;
}
// The rest of this function is the ported implementation.
for (let i = 0; i < nvert; j = i++) {
if ( ((verty[i] > testy) != (verty[j] > testy)) &&
(testx < (vertx[j] - vertx[i]) * (testy - verty[i]) / (verty[j] - verty[i]) + vertx[i]) ) {
c = !c;
}
}
return c;
""";
SELECT pickup_latitude, pickup_longitude, dropoff_latitude, dropoff_longitude, pickup_datetime
FROM `bigquery-public-data.new_york_taxi_trips.tlc_yellow_trips_2016`
WHERE pointInPolygon(pickup_latitude, pickup_longitude) = TRUE
AND (pickup_datetime BETWEEN CAST("2016-01-01 00:00:01" AS DATETIME) AND CAST("2016-02-28 23:59:59" AS DATETIME))
LIMIT 1000
恭喜!
您現在已使用 BigQuery 執行三種空間查詢。如您所見,位置資訊對這個資料集的查詢結果資料影響很大,但除非您猜測要執行查詢的位置,否則很難只使用 SQL 查詢臨時探索空間模式。
如果我們能在地圖上將資料視覺化,並透過定義任意感興趣的區域來探索資料,那就太棒了!您可以使用 Google Maps API 達成這個目標。首先,您需要啟用 Maps API,在本機電腦上設定執行中的簡單網頁,然後開始使用 BigQuery API 從網頁傳送查詢。
4. 使用 Google Maps API
執行一些簡單的空間查詢後,下一步是將輸出內容視覺化,以便查看模式。為此,您需要啟用 Maps API、建立網頁,將查詢從地圖傳送至 BigQuery,然後在地圖上繪製結果。
啟用 Maps JavaScript API
在本程式碼研究室中,您需要在專案中啟用 Google 地圖平台的 Maps JavaScript API。若要這麼做,請執行下列作業:
- 在 Google Cloud Platform 控制台中,前往 Marketplace
- 在 Marketplace 中搜尋「Maps JavaScript API」
- 在搜尋結果中,按一下「Maps JavaScript API」圖塊
- 按一下「啟用」按鈕
產生 API 金鑰
如要向 Google 地圖平台提出要求,您必須產生 API 金鑰,並隨所有要求一併傳送。如要產生 API 金鑰,請按照下列步驟操作:
- 在 Google Cloud Platform 主控台中,按一下漢堡選單開啟左側導覽列
- 選取「APIs & Service」(API 和服務) >「Credentials」(憑證)
- 按一下「建立憑證」按鈕,然後選取「API 金鑰」
- 複製新的 API 金鑰
下載程式碼並設定網路伺服器
點選下方按鈕,即可下載這個程式碼研究室的所有程式碼:
將下載的 ZIP 檔案解壓縮。這會解壓縮根資料夾 (bigquery
),其中包含本程式碼研究室每個步驟的資料夾,以及您需要的所有資源。
stepN
資料夾包含本程式碼研究室每個步驟的最終狀態。僅供參考。我們會在名為 work
的目錄中完成所有程式碼編寫工作。
設定本機網路伺服器
您可以自由使用自己的網頁伺服器,但本程式碼研究室的設計可與 Chrome 網頁伺服器完美搭配。如果尚未安裝該應用程式,可以從 Chrome 線上應用程式商店安裝。
安裝完成後,請開啟應用程式。在 Chrome 中,你可以按照下列步驟操作:
- 開啟 Chrome
- 在頂端的網址列中輸入 chrome://apps
- 按下 Enter 鍵。
- 在開啟的視窗中,按一下「Web Server」圖示。您也可以在應用程式上按一下滑鼠右鍵,在一般或固定分頁、全螢幕或新視窗中開啟應用程式
接著會看到這個對話方塊,可供您設定本機網頁伺服器:
- 按一下「選擇資料夾」,然後選取您下載程式碼研究室範例檔案的位置
- 在「選項」部分,勾選「自動顯示 index.html」旁的方塊:
- 將標示為「Web Server: STARTED」的切換鈕向左滑動,然後再向右滑動,停止並重新啟動網路伺服器
5. 載入地圖和繪圖工具
建立基本地圖頁面
首先,請建立簡單的 HTML 網頁,使用 Maps JavaScript API 和幾行 JavaScript 載入 Google 地圖。Google 地圖平台「簡易地圖範例」中的程式碼是絕佳的起點。我們在此重現該檔案,方便您複製並貼到所選的文字編輯器或 IDE,您也可以開啟下載的存放區中的 index.html
檔案來查看。
- 將
index.html
複製到存放區本機副本的work
資料夾 - 將 img/ 資料夾複製到存放區本機副本的 work/ 資料夾
- 在文字編輯器或 IDE 中開啟 work/
index.html
- 將
YOUR_API_KEY
替換為您先前建立的 API 金鑰
<script src="https://maps.googleapis.com/maps/api/js?key=YOUR_API_KEY&callback=initMap"
async defer></script>
- 在瀏覽器中開啟
localhost:<port>/work
,其中port
是在本機網頁伺服器設定中指定的通訊埠號碼。預設通訊埠為8887
。您應該會看到顯示的第一張地圖。
如果在瀏覽器中收到錯誤訊息,請檢查 API 金鑰是否正確,以及本機網路伺服器是否處於啟用狀態。
變更預設位置和縮放等級
設定位置和縮放等級的程式碼位於 index.html 的第 27 和 28 行,目前以澳洲雪梨為中心:
<script>
let map;
function initMap() {
map = new google.maps.Map(document.getElementById('map'), {
center: {lat: -34.397, lng: 150.644},
zoom: 8
});
}
</script>
本教學課程會使用紐約市的 BigQuery 計程車載客資料,因此接下來請變更地圖初始化程式碼,將地圖中心設在紐約市的某個位置,並將縮放層級設為適當的值 (13 或 14 應該都可以)。
如要這麼做,請將上述程式碼區塊更新為下列程式碼,將地圖中心點設在帝國大廈,並將縮放等級調整為 14:
<script>
let map;
function initMap() {
map = new google.maps.Map(document.getElementById('map'), {
center: {lat: 40.7484405, lng: -73.9878531},
zoom: 14
});
}
</script>
接著,重新載入瀏覽器中的地圖,即可查看結果。
載入繪圖和視覺化程式庫
如要在地圖中新增繪圖功能,請修改載入 Maps JavaScript API 的指令碼,加入選用參數,告知 Google Maps Platform 啟用繪圖程式庫。
本程式碼研究室也會使用 HeatmapLayer
,因此您也需要更新指令碼,要求使用視覺化程式庫。如要這麼做,請新增 libraries
參數,並以逗號分隔指定 visualization
和 drawing
程式庫,例如 libraries=
visualization,drawing
看起來應該像這樣:
<script src='http://maps.googleapis.com/maps/api/js?libraries=visualization,drawing&callback=initMap&key=YOUR_API_KEY' async defer></script>
新增 DrawingManager
如要將使用者繪製的形狀做為查詢的輸入內容,請將 DrawingManager
新增至地圖,並啟用 Circle
、Rectangle
和 Polygon
工具。
建議您將所有 DrawingManager
設定程式碼放入新函式中,因此請在 index.html 的副本中執行下列操作:
- 新增名為
setUpDrawingTools()
的函式,並使用下列程式碼建立DrawingManager
,然後將其map
屬性設為參照網頁中的地圖物件。
傳遞至 google.maps.drawing.DrawingManager(options)
的選項會設定繪製形狀的預設類型,以及所繪形狀的顯示選項。如要選取地圖區域並傳送為查詢,形狀的不透明度應為零。如要進一步瞭解可用選項,請參閱「DrawingManager 選項」。
function setUpDrawingTools() {
// Initialize drawing manager
drawingManager = new google.maps.drawing.DrawingManager({
drawingMode: google.maps.drawing.OverlayType.CIRCLE,
drawingControl: true,
drawingControlOptions: {
position: google.maps.ControlPosition.TOP_LEFT,
drawingModes: [
google.maps.drawing.OverlayType.CIRCLE,
google.maps.drawing.OverlayType.POLYGON,
google.maps.drawing.OverlayType.RECTANGLE
]
},
circleOptions: {
fillOpacity: 0
},
polygonOptions: {
fillOpacity: 0
},
rectangleOptions: {
fillOpacity: 0
}
});
drawingManager.setMap(map);
}
- 在地圖物件建立後,於
initMap()
函式中呼叫setUpDrawingTools()
function initMap() {
map = new google.maps.Map(document.getElementById('map'), {
center: {lat: 40.744593, lng: -73.990370}, // Manhattan, New York.
zoom: 12
});
setUpDrawingTools();
}
- 重新載入 index.html,並確認繪圖工具是否顯示在畫面上。此外,也請確認這些工具是否能繪製圓形、矩形和多邊形。
您可以點選並拖曳來繪製圓形和矩形,但如要繪製多邊形,則必須點選每個頂點,然後按兩下完成形狀。
處理繪圖事件
您需要一些程式碼來處理使用者繪製完形狀時觸發的事件,就像需要繪製形狀的座標來建構 SQL 查詢一樣。
我們會在後續步驟中新增這項程式碼,但目前會先存根三個空白事件處理常式,以處理 rectanglecomplete
、circlecomplete
和 polygoncomplete
事件。處理常式在這個階段不必執行任何程式碼。
在 setUpDrawingTools()
函式的底部新增下列內容:
drawingManager.addListener('rectanglecomplete', rectangle => {
// We will add code here in a later step.
});
drawingManager.addListener('circlecomplete', circle => {
// We will add code here in a later step.
});
drawingManager.addListener('polygoncomplete', polygon => {
// We will add code here in a later step.
});
您可以在存放區的本機副本中找到這個程式碼的有效範例,位於 step2
資料夾:step2/map.html。
6. 使用 BigQuery Client API
Google BigQuery Client API 可協助您避免編寫大量樣板程式碼,這些程式碼用於建構要求、剖析回應及處理驗證。由於我們要開發以瀏覽器為基礎的應用程式,因此本程式碼研究室會透過 Google API 用戶端程式庫 (適用於 JavaScript) 使用 BigQuery API。
接著,您會在網頁中載入這個 API,並使用該 API 與 BigQuery 互動。
新增 Google JavaScript 用戶端 API
您將使用適用於 JavaScript 的 Google Client API,對 BigQuery 執行查詢。在 index.html
的副本 (位於 work
資料夾中) 中,使用類似這樣的 <script>
標記載入 API。將標記放在載入 Maps API 的 <script>
標記下方:
<script src='https://apis.google.com/js/client.js'></script>
載入 Google Client API 後,授權使用者存取 BigQuery 中的資料。如要執行這項操作,可以使用 OAuth 2.0。首先,您需要在 Google Cloud 控制台專案中設定一些憑證。
建立 OAuth 2.0 憑證
- 在 Google Cloud 控制台中,從「導覽選單」依序選取「API 和服務」>「憑證」。
設定憑證前,請先為授權畫面新增一些設定。當應用程式使用者授權應用程式代表他們存取 BigQuery 資料時,就會看到這個畫面。
如要執行這項操作,請按一下「OAuth 同意畫面」分頁標籤。2. 您必須將 BigQuery API 新增至這個權杖的範圍。在「Scopes for Google APIs」部分中,按一下「Add Scope」按鈕。3. 在清單中,勾選「BigQuery API」項目旁的方塊,並使用 ../auth/bigquery
範圍。4. 按一下「新增」。5. 在「應用程式名稱」欄位中輸入名稱。6. 按一下「儲存」儲存設定,7. 接著,您將建立 OAuth 用戶端 ID。如要建立憑證,請按一下「建立憑證」:
- 在下拉式選單中,按一下「OAuth 用戶端 ID」。
- 在「應用程式類型」下方,選取「網頁應用程式」。
- 在「應用程式名稱」欄位中,輸入專案名稱。例如「BigQuery 和地圖」。
- 在「限制」下方的「已授權的 JavaScript 來源」欄位中,輸入 localhost 的網址,包括通訊埠編號。例如:
http://localhost:8887
- 按一下 [建立] 按鈕。
彈出式視窗會顯示用戶端 ID 和用戶端密鑰。您需要用戶端 ID,才能對 BigQuery 執行驗證。複製程式碼,然後貼到 work/index.html
中,並將其命名為 clientId
,做為新的全域 JavaScript 變數。
let clientId = 'YOUR_CLIENT_ID';
7. 授權和初始化
網頁必須先授權使用者存取 BigQuery,才能初始化地圖。在這個範例中,我們使用 OAuth 2.0,如 JavaScript 用戶端 API 說明文件的授權部分所述。您必須使用 OAuth 用戶端 ID 和專案 ID 傳送查詢。
在網頁中載入 Google Client API 時,請執行下列步驟:
- 授權使用者。
- 如果已授權,請載入 BigQuery API。
- 載入並初始化地圖。
如要查看完成的 HTML 網頁範例,請參閱 step3/map.html。
授權使用者
應用程式的最終使用者必須授權應用程式代表他們存取 BigQuery 中的資料。JavaScript 適用的 Google Client API 會處理 OAuth 邏輯,以執行這項操作。
在實際應用程式中,您可選擇多種方式整合授權步驟。
舉例來說,您可以從按鈕等 UI 元素呼叫 authorize()
,或在網頁載入時呼叫。我們選擇在載入 JavaScript 適用的 Google Client API 後,使用 gapi.load()
方法中的回呼函式授權使用者。
在載入 Google JavaScript 用戶端 API 的 <script>
標記後,立即編寫一些程式碼,載入用戶端程式庫和驗證模組,以便立即驗證使用者。
<script src='https://apis.google.com/js/client.js'></script>
<script type='text/javascript'>
gapi.load('client:auth', authorize);
</script>
授權後載入 BigQuery API
授權使用者後,載入 BigQuery API。
首先,使用在上一個步驟中新增的 clientId
變數呼叫 gapi.auth.authorize()
。在名為 handleAuthResult
的回呼函式中處理回應。
immediate
參數可控制是否向使用者顯示彈出式視窗。如果使用者已獲得授權,請設為 true
,即可禁止顯示授權彈出式視窗。
在網頁中新增名為 handleAuthResult()
的函式。函式需要採用 authresult
參數,讓您根據使用者是否已成功授權,控管邏輯流程。
如果使用者已成功授權,請新增名為 loadApi
的函式來載入 BigQuery API。
在 handleAuthResult()
函式中新增邏輯,在有 authResult
物件傳遞至函式,且物件的 error
屬性值為 false
時,呼叫 loadApi()
。
在 loadApi()
函式中新增程式碼,使用 gapi.client.load()
方法載入 BigQuery API。
let clientId = 'your-client-id-here';
let scopes = 'https://www.googleapis.com/auth/bigquery';
// Check if the user is authorized.
function authorize(event) {
gapi.auth.authorize({client_id: clientId, scope: scopes, immediate: false}, handleAuthResult);
return false;
}
// If authorized, load BigQuery API
function handleAuthResult(authResult) {
if (authResult && !authResult.error) {
loadApi();
return;
}
console.error('Not authorized.')
}
// Load BigQuery client API
function loadApi(){
gapi.client.load('bigquery', 'v2');
}
載入地圖
最後一步是初始化地圖。如要這麼做,您需要稍微變更邏輯的順序。目前,當 Maps API JavaScript 載入時,系統會初始化。
做法是在 gapi.client
物件上呼叫 load()
方法後,從 then()
方法呼叫 initMap()
函式。
// Load BigQuery client API
function loadApi(){
gapi.client.load('bigquery', 'v2').then(
() => initMap()
);
}
8. BigQuery API 概念
BigQuery API 呼叫通常會在幾秒內執行完畢,但可能不會立即傳回回應。您需要一些邏輯來輪詢 BigQuery,找出長時間執行的工作狀態,並只在工作完成時擷取結果。
這個步驟的完整程式碼位於 step4/map.html。
傳送要求
在 work/index.html
中新增 JavaScript 函式,使用 API 傳送查詢,並新增一些變數來儲存 BigQuery 資料集和包含要查詢資料表的專案值,以及任何費用將計入的專案 ID。
let datasetId = 'your_dataset_id';
let billingProjectId = 'your_project_id';
let publicProjectId = 'bigquery-public-data';
function sendQuery(queryString){
let request = gapi.client.bigquery.jobs.query({
'query': queryString,
'timeoutMs': 30000,
'datasetId': datasetId,
'projectId': billingProjectId,
'useLegacySql':false
});
request.execute(response => {
//code to handle the query response goes here.
});
}
查看工作狀態
下方的 checkJobStatus
函式說明如何使用 get
API 方法和原始查詢要求傳回的 jobId
,定期檢查工作狀態。以下範例會每隔 500 毫秒執行一次,直到工作完成為止。
let jobCheckTimer;
function checkJobStatus(jobId){
let request = gapi.client.bigquery.jobs.get({
'projectId': billingProjectId,
'jobId': jobId
});
request.execute(response =>{
if (response.status.errorResult){
// Handle any errors.
console.log(response.status.error);
return;
}
if (response.status.state == 'DONE'){
// Get the results.
clearTimeout(jobCheckTimer);
getQueryResults(jobId);
return;
}
// Not finished, check again in a moment.
jobCheckTimer = setTimeout(checkJobStatus, 500, [jobId]);
});
}
修改 sendQuery
方法,在 request.execute()
呼叫中將 checkJobStatus()
方法做為回呼呼叫。將工作 ID 傳遞至 checkJobStatus
。回應物件會以 jobReference.jobId
形式顯示這項資訊。
function sendQuery(queryString){
let request = gapi.client.bigquery.jobs.query({
'query': queryString,
'timeoutMs': 30000,
'datasetId': datasetId,
'projectId': billingProjectId,
'useLegacySql':false
});
request.execute(response => checkJobStatus(response.jobReference.jobId));
}
取得查詢結果
如要在查詢完成後取得結果,請使用 jobs.getQueryResults
API 呼叫。在網頁中新增名為 getQueryResults()
的函式,並接受 jobId
參數:
function getQueryResults(jobId){
let request = gapi.client.bigquery.jobs.getQueryResults({
'projectId': billingProjectId,
'jobId': jobId
});
request.execute(response => {
// Do something with the results.
})
}
9. 使用 BigQuery API 查詢位置資料
您可以使用三種方式,透過 SQL 對 BigQuery 中的資料執行空間查詢:
在 BigQuery 舊版 SQL 參考資料的「數學函式」一節中,有立方體和半徑查詢的範例,位於「進階範例」下方。
如要查詢邊界方塊和半徑,可以呼叫 BigQuery API query
方法。為每個查詢建構 SQL,並傳遞至您在上一個步驟中建立的 sendQuery
函式。
如需這個步驟的有效程式碼範例,請參閱 step4/map.html。
矩形查詢
如要在地圖上顯示 BigQuery 資料,最簡單的方法是使用小於和大於比較,要求緯度和經度落在矩形內的所有資料列。這可以是目前的地圖檢視畫面,也可以是地圖上繪製的形狀。
如要使用使用者繪製的形狀,請變更 index.html
中的程式碼,以處理矩形完成時觸發的繪圖事件。在這個範例中,程式碼會對矩形物件使用 getBounds()
,取得代表矩形範圍的物件 (以地圖座標表示),並將該物件傳遞至名為 rectangleQuery
的函式:
drawingManager.addListener('rectanglecomplete', rectangle => rectangleQuery(rectangle.getBounds()));
rectangleQuery
函式只需要使用右上 (東北) 和左下 (西南) 座標,針對 BigQuery 資料表中的每個資料列建構小於/大於比較。以下範例會查詢含有名為 'pickup_latitude'
和 'pickup_longitude'
的資料欄 (儲存位置值) 的資料表。
指定 BigQuery 資料表
如要使用 BigQuery API 查詢資料表,您必須在 SQL 查詢中提供資料表的完整名稱。標準 SQL 的格式為 project.dataset.tablename
。在舊版 SQL 中,這是 project.dataset.tablename
。
紐約市計程車行程的資料表有很多。如要查看這些資料集,請前往 BigQuery 網頁版控制台,然後展開「公開資料集」選單項目。找出名為 new_york
的資料集,然後展開該資料集來查看資料表。選擇「Yellow Taxi trips」資料表:bigquery-public-data.new_york_taxi_trips.tlc_yellow_trips_2016
)。
指定專案 ID
在 API 呼叫中,您需要指定 Google Cloud Platform 專案的名稱,以利計費。在本程式碼研究室中,這並非包含資料表的專案。如果您是使用在自己專案中建立的資料表 (透過上傳資料建立),則這個專案 ID 會與 SQL 陳述式中的專案 ID 相同。
在程式碼中加入 JavaScript 變數,以保留對 Public Datasets 專案的參照,其中包含您要查詢的資料表,以及資料表名稱和資料集名稱。您也需要使用另一個變數來參照自己的帳單專案 ID。
在 index.html 的副本中,加入名為 billingProjectId, publicProjectId, datasetId
和 tableName
的全域 JavaScript 變數。
使用 BigQuery 公開資料集專案的詳細資料,初始化 'publicProjectId'
、'datasetId'
和 'tableName'
變數。使用您自己的專案 ID 初始化 billingProjectId
(您在本程式碼研究室稍早的「設定」中建立的專案 ID)。
let billingProjectId = 'YOUR_PROJECT_ID';
let publicProjectId = 'bigquery-public-data';
let datasetId = 'new_york_taxi_trips';
let tableName = 'tlc_yellow_trips_2016';
現在,請在程式碼中新增兩個函式,以生成 SQL,並使用上一個步驟中建立的 sendQuery
函式將查詢傳送至 BigQuery。
第一個函式應命名為 rectangleSQL()
,且必須接受兩個引數,即一對 google.Maps.LatLng
物件,代表地圖座標中的矩形角。
第二個函式應稱為 rectangleQuery()
。這會將查詢文字傳遞至 sendQuery
函式。
let billingProjectId = 'YOUR_PROJECT_ID';
let publicProjectId = 'bigquery-public-data';
let datasetId = 'new_york';
let tableName = 'tlc_yellow_trips_2016';
function rectangleQuery(latLngBounds){
let queryString = rectangleSQL(latLngBounds.getNorthEast(), latLngBounds.getSouthWest());
sendQuery(queryString);
}
function rectangleSQL(ne, sw){
let queryString = 'SELECT pickup_latitude, pickup_longitude '
queryString += 'FROM `' + publicProjectId +'.' + datasetId + '.' + tableName + '`'
queryString += ' WHERE pickup_latitude > ' + sw.lat();
queryString += ' AND pickup_latitude < ' + ne.lat();
queryString += ' AND pickup_longitude > ' + sw.lng();
queryString += ' AND pickup_longitude < ' + ne.lng();
return queryString;
}
此時,您已有足夠的程式碼,可將查詢傳送至 BigQuery,以取得使用者繪製矩形內的所有資料列。在加入其他圓形和手繪形狀的查詢方法之前,我們先來看看如何處理查詢傳回的資料。
10. 將回覆內容製成圖表
BigQuery 資料表可能非常龐大 (數 PB 的資料),且每秒可增加數十萬個資料列。因此,請務必盡量限制傳回的資料量,以便在 Google 地圖上繪製。如果結果集非常龐大 (數萬列以上),繪製每個資料列的位置會導致地圖無法解讀。在 SQL 查詢和地圖中,都有許多彙整位置的技巧,您可以限制查詢傳回的結果。
您可以在 step5/map.html 中取得這個步驟的完整程式碼。
為確保傳輸至網頁的資料量維持在合理大小,請修改 rectangleSQL()
函式,新增將回應限制為 10000 列的陳述式。在下方範例中,這是以名為 recordLimit
的全域變數指定,因此所有查詢函式都能使用相同的值。
let recordLimit = 10000;
function rectangleSQL(ne, sw){
var queryString = 'SELECT pickup_latitude, pickup_longitude '
queryString += 'FROM `' + publicProjectId +'.' + datasetId + '.' + tableName + '`'
queryString += ' WHERE pickup_latitude > ' + sw.lat();
queryString += ' AND pickup_latitude < ' + ne.lat();
queryString += ' AND pickup_longitude > ' + sw.lng();
queryString += ' AND pickup_longitude < ' + ne.lng();
queryString += ' LIMIT ' + recordLimit;
return queryString;
}
如要以視覺化方式呈現地點密度,可以使用熱視圖。Maps Javascript API 提供 HeatmapLayer 類別,可用於達成這個目的。HeatmapLayer 會採用經緯度座標陣列,因此將查詢傳回的資料列轉換為熱視圖相當容易。
在 getQueryResults
函式中,將 response.result.rows
陣列傳遞至名為 doHeatMap()
的新 JavaScript 函式,該函式會建立熱度圖。
每個資料列都會有一個名為 f
的屬性,這是資料欄的陣列。每個資料欄都會有包含值的 v
屬性。
您的程式碼需要逐一檢查每列中的資料欄,並擷取值。
在 SQL 查詢中,您只要求提供計程車上車地點的緯度和經度值,因此回應中只會有兩欄。
將位置陣列指派給熱度圖層後,別忘了呼叫 setMap()
。這樣地圖上就會顯示該地點。
範例如下:
function getQueryResults(jobId){
let request = gapi.client.bigquery.jobs.getQueryResults({
'projectId': billingProjectId,
'jobId': jobId
});
request.execute(response => doHeatMap(response.result.rows))
}
let heatmap;
function doHeatMap(rows){
let heatmapData = [];
if (heatmap != null){
heatmap.setMap(null);
}
for (let i = 0; i < rows.length; i++) {
let f = rows[i].f;
let coords = { lat: parseFloat(f[0].v), lng: parseFloat(f[1].v) };
let latLng = new google.maps.LatLng(coords);
heatmapData.push(latLng);
}
heatmap = new google.maps.visualization.HeatmapLayer({
data: heatmapData
});
heatmap.setMap(map);
}
此時,您應該能夠:
- 開啟頁面並授權存取 BigQuery
- 在紐約市的某個位置繪製矩形
- 查詢結果會以熱視圖的形式呈現。
以下是針對 2016 年紐約市黃色計程車資料執行矩形查詢的結果範例,並以熱點圖呈現。這張圖片顯示 7 月某個週六,帝國大廈周圍的取貨分布情形:
11. 依指定地點附近的半徑範圍查詢
半徑查詢非常相似。使用 BigQuery 的舊版 SQL 數學函式,您可以使用 Haversine 公式建構 SQL 查詢,估算地球表面的圓形區域。
使用矩形的相同技巧,您可以處理 OverlayComplete
事件,取得使用者繪製的圓形中心和半徑,並以相同方式建構查詢的 SQL。
程式碼存放區中包含這個步驟的程式碼有效範例,位於 step6/map.html。
drawingManager.addListener('circlecomplete', circle => circleQuery(circle));
在 index.html 的副本中,新增兩個空白函式:circleQuery()
和 haversineSQL()
。
接著,新增 circlecomplete
事件處理常式,將中心和半徑傳遞至名為 circleQuery().
的新函式
circleQuery()
函式會呼叫 haversineSQL()
建構查詢的 SQL,然後按照下列程式碼範例呼叫 sendQuery()
函式,傳送查詢。
function circleQuery(circle){
let queryString = haversineSQL(circle.getCenter(), circle.radius);
sendQuery(queryString);
}
// Calculate a circular area on the surface of a sphere based on a center and radius.
function haversineSQL(center, radius){
let queryString;
let centerLat = center.lat();
let centerLng = center.lng();
let kmPerDegree = 111.045;
queryString = 'CREATE TEMPORARY FUNCTION Degrees(radians FLOAT64) RETURNS FLOAT64 LANGUAGE js AS ';
queryString += '""" ';
queryString += 'return (radians*180)/(22/7);';
queryString += '"""; ';
queryString += 'CREATE TEMPORARY FUNCTION Radians(degrees FLOAT64) RETURNS FLOAT64 LANGUAGE js AS';
queryString += '""" ';
queryString += 'return (degrees*(22/7))/180;';
queryString += '"""; ';
queryString += 'SELECT pickup_latitude, pickup_longitude '
queryString += 'FROM `' + publicProjectId +'.' + datasetId + '.' + tableName + '` ';
queryString += 'WHERE '
queryString += '(' + kmPerDegree + ' * DEGREES( ACOS( COS( RADIANS('
queryString += centerLat;
queryString += ') ) * COS( RADIANS( pickup_latitude ) ) * COS( RADIANS( ' + centerLng + ' ) - RADIANS('
queryString += ' pickup_longitude ';
queryString += ') ) + SIN( RADIANS('
queryString += centerLat;
queryString += ') ) * SIN( RADIANS( pickup_latitude ) ) ) ) ) ';
queryString += ' < ' + radius/1000;
queryString += ' LIMIT ' + recordLimit;
return queryString;
}
試試看!
加入上述程式碼,然後使用「圓形」工具選取地圖區域。結果看起來會像這樣:
12. 查詢任意形狀
重點回顧:SQL 不支援使用矩形和圓形以外的任意形狀進行查詢。BigQuery 沒有任何原生幾何資料型別,因此如要使用多邊形形狀執行查詢,您需要採用不同於簡單 SQL 查詢的方法。
使用者定義函式 (UDF) 是 BigQuery 的強大功能之一,可用於執行這項作業。UDF 會在 SQL 查詢中執行 JavaScript 程式碼。
這個步驟的有效程式碼位於 step7/map.html。
BigQuery API 中的 UDF
使用 BigQuery API 建立 UDF 的方式與網頁版控制台略有不同,您需要呼叫 jobs.insert method
。
透過 API 進行標準 SQL 查詢時,只要使用單一 SQL 陳述式,即可使用使用者定義函式。useLegacySql
的值必須設為 false
。下方的 JavaScript 範例顯示的函式會建立及傳送要求物件,以插入新工作,在本例中為含有使用者定義函式的查詢。
如要查看這個做法的實例,請參閱 step7/map.html。
function polygonQuery(polygon) {
let request = gapi.client.bigquery.jobs.insert({
'projectId' : billingProjectId,
'resource' : {
'configuration':
{
'query':
{
'query': polygonSql(polygon),
'useLegacySql': false
}
}
}
});
request.execute(response => checkJobStatus(response.jobReference.jobId));
}
SQL 查詢的結構如下:
function polygonSql(poly){
let queryString = 'CREATE TEMPORARY FUNCTION pointInPolygon(latitude FLOAT64, longitude FLOAT64) ';
queryString += 'RETURNS BOOL LANGUAGE js AS """ ';
queryString += 'var polygon=' + JSON.stringify(poly) + ';';
queryString += 'var vertx = [];';
queryString += 'var verty = [];';
queryString += 'var nvert = 0;';
queryString += 'var testx = longitude;';
queryString += 'var testy = latitude;';
queryString += 'for(coord in polygon){';
queryString += ' vertx[nvert] = polygon[coord][0];';
queryString += ' verty[nvert] = polygon[coord][1];';
queryString += ' nvert ++;';
queryString += '}';
queryString += 'var i, j, c = 0;';
queryString += 'for (i = 0, j = nvert-1; i < nvert; j = i++) {';
queryString += ' if ( ((verty[i]>testy) != (verty[j]>testy)) &&(testx < (vertx[j]-vertx[i]) * (testy-verty[i]) / (verty[j]-verty[i]) + vertx[i]) ){';
queryString += ' c = !c;';
queryString += ' }';
queryString += '}';
queryString += 'return c;';
queryString += '"""; ';
queryString += 'SELECT pickup_latitude, pickup_longitude, dropoff_latitude, dropoff_longitude, pickup_datetime ';
queryString += 'FROM `' + publicProjectId + '.' + datasetId + '.' + tableName + '` ';
queryString += 'WHERE pointInPolygon(pickup_latitude, pickup_longitude) = TRUE ';
queryString += 'LIMIT ' + recordLimit;
return queryString;
}
這裡有兩件事正在發生。首先,程式碼會建立 CREATE TEMPORARY FUNCTION
陳述式,封裝 JavaScript 程式碼,判斷指定點是否位於多邊形內。多邊形座標是透過 JSON.stringify(poly)
方法呼叫插入,將 JavaScript 的 x、y 座標配對陣列轉換為字串。多邊形物件會做為引數傳遞至建構 SQL 的函式。
其次,程式碼會建構主要的 SQL SELECT
陳述式。在本例中,UDF 是在 WHERE
運算式中呼叫。
整合 Maps API
如要搭配 Maps API 繪圖程式庫使用這項功能,我們需要儲存使用者繪製的多邊形,並將其傳遞至 SQL 查詢的 UDF 部分。
首先,我們需要處理 polygoncomplete
繪圖事件,以取得形狀的座標 (經緯度組合的陣列):
drawingManager.addListener('polygoncomplete', polygon => {
let path = polygon.getPaths().getAt(0);
let queryPolygon = path.map(element => {
return [element.lng(), element.lat()];
});
polygonQuery(queryPolygon);
});
接著,polygonQuery
函式可以建構 UDF JavaScript 函式 (以字串形式),以及呼叫 UDF 函式的 SQL 陳述式。
如需這項功能的運作範例,請參閱 step7/map.html。
輸出範例
以下是使用手繪多邊形查詢 BigQuery 中「2016 年紐約市 TLC 黃色計程車資料」的結果範例,所選資料會繪製為熱度圖。
13. 進階做法
以下提供一些建議,說明如何擴充這個程式碼研究室,查看資料的其他層面。您可以在程式碼存放區的 step8/map.html 中,找到這些概念的有效範例。
對應下車地點
目前我們只繪製了取貨地點的地圖。要求 dropoff_latitude
和 dropoff_longitude
欄位,並修改熱度圖程式碼來繪製這些欄位,即可查看從特定地點出發的計程車行程目的地。
舉例來說,如果使用者在帝國大廈附近要求接送,計程車通常會在哪裡讓乘客下車?
變更 polygonSql()
中的 SQL 陳述式程式碼,除了取貨地點外,也要求這些資料欄。
function polygonSql(poly){
let queryString = 'CREATE TEMPORARY FUNCTION pointInPolygon(latitude FLOAT64, longitude FLOAT64) ';
queryString += 'RETURNS BOOL LANGUAGE js AS """ ';
queryString += 'var polygon=' + JSON.stringify(poly) + ';';
queryString += 'var vertx = [];';
queryString += 'var verty = [];';
queryString += 'var nvert = 0;';
queryString += 'var testx = longitude;';
queryString += 'var testy = latitude;';
queryString += 'for(coord in polygon){';
queryString += ' vertx[nvert] = polygon[coord][0];';
queryString += ' verty[nvert] = polygon[coord][1];';
queryString += ' nvert ++;';
queryString += '}';
queryString += 'var i, j, c = 0;';
queryString += 'for (i = 0, j = nvert-1; i < nvert; j = i++) {';
queryString += ' if ( ((verty[i]>testy) != (verty[j]>testy)) &&(testx < (vertx[j]-vertx[i]) * (testy-verty[i]) / (verty[j]-verty[i]) + vertx[i]) ){';
queryString += ' c = !c;';
queryString += ' }';
queryString += '}';
queryString += 'return c;';
queryString += '"""; ';
queryString += 'SELECT pickup_latitude, pickup_longitude, dropoff_latitude, dropoff_longitude, pickup_datetime ';
queryString += 'FROM `' + publicProjectId + '.' + datasetId + '.' + tableName + '` ';
queryString += 'WHERE pointInPolygon(pickup_latitude, pickup_longitude) = TRUE ';
queryString += 'LIMIT ' + recordLimit;
return queryString;
}
doHeatMap
函式隨後就能改用中斷值。結果物件的結構定義可供檢查,找出陣列中這些資料欄的位置。在本例中,這兩個項目會位於索引位置 2 和 3。這些索引可以從變數讀取,方便管理程式碼。注意:熱視圖的 maxIntensity
設為最多顯示每像素 20 個捨棄的密度。
新增一些變數,即可變更用於熱視圖資料的資料欄。
// Show query results as a Heatmap.
function doHeatMap(rows){
let latCol = 2;
let lngCol = 3;
let heatmapData = [];
if (heatmap!=null){
heatmap.setMap(null);
}
for (let i = 0; i < rows.length; i++) {
let f = rows[i].f;
let coords = { lat: parseFloat(f[latCol].v), lng: parseFloat(f[lngCol].v) };
let latLng = new google.maps.LatLng(coords);
heatmapData.push(latLng);
}
heatmap = new google.maps.visualization.HeatmapLayer({
data: heatmapData,
maxIntensity: 20
});
heatmap.setMap(map);
}
以下是 2016 年帝國大廈周圍所有上車地點的下車地點分布熱視圖。您可以看到中城目的地的大量集中點 (紅色斑點),特別是時代廣場周圍,以及 23 街和 14 街之間的第五大道。在此縮放層級未顯示的其他高密度位置包括拉瓜迪亞和甘迺迪機場、世界貿易中心和炮台公園。
設定基本地圖的樣式
使用 Maps JavaScript API 建立 Google 地圖時,您可以透過 JSON 物件設定地圖樣式。如果地圖上顯示的是資料視覺化內容,則可以將地圖顏色調暗。您可以使用 Google Maps API 樣式精靈 (網址:mapstyle.withgoogle.com) 建立及試用各種地圖樣式。
您可以在初始化地圖物件時設定地圖樣式,也可以在之後的任何時間設定。以下說明如何在 initMap()
函式中新增自訂樣式:
function initMap() {
map = new google.maps.Map(document.getElementById('map'), {
center: {lat: 40.744593, lng: -73.990370}, // Manhattan, New York.
zoom: 12,
styles: [
{
"elementType": "geometry",
"stylers": [
{
"color": "#f5f5f5"
}
]
},
{
"elementType": "labels.icon",
"stylers": [
{
"visibility": "on"
}
]
},
{
"featureType": "water",
"elementType": "labels.text.fill",
"stylers": [
{
"color": "#9e9e9e"
}
]
}
]
});
setUpDrawingTools();
}
下方範例樣式顯示灰階地圖,並標示搜尋點。
[
{
"elementType": "geometry",
"stylers": [
{
"color": "#f5f5f5"
}
]
},
{
"elementType": "labels.icon",
"stylers": [
{
"visibility": "on"
}
]
},
{
"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": "poi",
"elementType": "geometry",
"stylers": [
{
"color": "#eeeeee"
}
]
},
{
"featureType": "poi",
"elementType": "labels.text.fill",
"stylers": [
{
"color": "#757575"
}
]
},
{
"featureType": "poi.park",
"elementType": "geometry",
"stylers": [
{
"color": "#e5e5e5"
}
]
},
{
"featureType": "poi.park",
"elementType": "labels.text.fill",
"stylers": [
{
"color": "#9e9e9e"
}
]
},
{
"featureType": "road",
"elementType": "geometry",
"stylers": [
{
"color": "#ffffff"
}
]
},
{
"featureType": "road.arterial",
"elementType": "labels.text.fill",
"stylers": [
{
"color": "#757575"
}
]
},
{
"featureType": "road.highway",
"elementType": "geometry",
"stylers": [
{
"color": "#dadada"
}
]
},
{
"featureType": "road.highway",
"elementType": "labels.text.fill",
"stylers": [
{
"color": "#616161"
}
]
},
{
"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": "#c9c9c9"
}
]
},
{
"featureType": "water",
"elementType": "labels.text.fill",
"stylers": [
{
"color": "#9e9e9e"
}
]
}
]
向使用者提供意見
雖然 BigQuery 通常會在幾秒內提供回應,但有時在查詢執行期間向使用者顯示正在進行某些作業,也是很有幫助的做法。
在網頁中新增一些 UI,顯示 checkJobStatus()
函式的回應,以及表示查詢正在進行中的動畫圖像。
可顯示的資訊包括查詢時間長度、傳回的資料量和處理的資料量。
在地圖 <div>
後方新增一些 HTML,在網頁中建立面板,顯示查詢傳回的資料列數、查詢耗費的時間,以及處理的資料量。
<div id="menu">
<div id="stats">
<h3>Statistics:</h3>
<table>
<tr>
<td>Total Locations:</td><td id="rowCount"> - </td>
</tr>
<tr>
<td>Query Execution:</td><td id="duration"> - </td>
</tr>
<tr>
<td>Data Processed:</td><td id="bytes"> - </td>
</tr>
</table>
</div>
</div>
這個面板的外觀和位置是由 CSS 控制。新增 CSS,將面板放置在頁面左上角,地圖類型按鈕和繪圖工具列下方,如下方程式碼片段所示。
#menu {
position: absolute;
background: rgba(255, 255, 255, 0.8);
z-index: 1000;
top: 50px;
left: 10px;
padding: 15px;
}
#menu h1 {
margin: 0 0 10px 0;
font-size: 1.75em;
}
#menu div {
margin: 5px 0px;
}
您可以將動畫圖片新增至網頁,但先隱藏起來,等到 BigQuery 工作執行時,再使用一些 JavaScript 和 CSS 程式碼顯示圖片。
新增一些 HTML,顯示動畫圖片。程式碼存放區的 img
資料夾中有名為 loader.gif
的圖片檔。
<img id="spinner" src="img/loader.gif">
新增一些 CSS,將圖片定位並預設為隱藏,直到需要時再顯示。
#spinner {
position: absolute;
top: 50%;
left: 50%;
margin-left: -32px;
margin-top: -32px;
opacity: 0;
z-index: -1000;
}
最後,加入一些 JavaScript,在查詢執行時更新狀態面板,並顯示或隱藏圖像。您可以根據可用的資訊,使用 response
物件更新面板。
檢查目前的工作時,可以使用 response.statistics
屬性。工作完成後,您就能存取 response.totalRows
和 response.totalBytesProcessed
屬性。將毫秒轉換為秒,並將位元組轉換為 GB,然後顯示給使用者,如以下程式碼範例所示。
function updateStatus(response){
if(response.statistics){
let durationMs = response.statistics.endTime - response.statistics.startTime;
let durationS = durationMs/1000;
let suffix = (durationS ==1) ? '':'s';
let durationTd = document.getElementById("duration");
durationTd.innerHTML = durationS + ' second' + suffix;
}
if(response.totalRows){
let rowsTd = document.getElementById("rowCount");
rowsTd.innerHTML = response.totalRows;
}
if(response.totalBytesProcessed){
let bytesTd = document.getElementById("bytes");
bytesTd.innerHTML = (response.totalBytesProcessed/1073741824) + ' GB';
}
}
在回應 checkJobStatus()
呼叫時,以及擷取查詢結果時,請呼叫這個方法。例如:
// Poll a job to see if it has finished executing.
function checkJobStatus(jobId){
let request = gapi.client.bigquery.jobs.get({
'projectId': billingProjectId,
'jobId': jobId
});
request.execute(response => {
//Show progress to the user
updateStatus(response);
if (response.status.errorResult){
// Handle any errors.
console.log(response.status.error);
return;
}
if (response.status.state == 'DONE'){
// Get the results.
clearTimeout(jobCheckTimer);
getQueryResults(jobId);
return;
}
// Not finished, check again in a moment.
jobCheckTimer = setTimeout(checkJobStatus, 500, [jobId]);
});
}
// When a BigQuery job has completed, fetch the results.
function getQueryResults(jobId){
let request = gapi.client.bigquery.jobs.getQueryResults({
'projectId': billingProjectId,
'jobId': jobId
});
request.execute(response => {
doHeatMap(response.result.rows);
updateStatus(response);
})
}
如要切換動畫圖片,請新增函式來控制其顯示狀態。這個函式會切換傳遞給它的任何 HTML DOM 元素的透明度。
function fadeToggle(obj){
if(obj.style.opacity==1){
obj.style.opacity = 0;
setTimeout(() => {obj.style.zIndex = -1000;}, 1000);
} else {
obj.style.zIndex = 1000;
obj.style.opacity = 1;
}
}
最後,請在處理查詢之前,以及查詢結果從 BigQuery 傳回之後,呼叫這個方法。
使用者繪製完矩形後,這段程式碼會呼叫 fadeToggle
函式。
drawingManager.addListener('rectanglecomplete', rectangle => {
//show an animation to indicate that something is happening.
fadeToggle(document.getElementById('spinner'));
rectangleQuery(rectangle.getBounds());
});
收到查詢回應後,請再次呼叫 fadeToggle()
,隱藏動畫圖像。
// When a BigQuery job has completed, fetch the results.
function getQueryResults(jobId){
let request = gapi.client.bigquery.jobs.getQueryResults({
'projectId': billingProjectId,
'jobId': jobId
});
request.execute(response => {
doHeatMap(response.result.rows);
//hide the animation.
fadeToggle(document.getElementById('spinner'));
updateStatus(response);
})
}
頁面應如下所示。
請參閱 step8/map.html 中的完整範例。
14. 注意事項
標記數量過多
若您使用的是資料量非常巨大的資料表,那麼您的查詢結果可能會多到影響地圖的顯示效率。加上 WHERE
子句或 LIMIT
陳述式來控制結果量。
繪製大量標記可能會讓地圖變得無法讀取。建議使用 HeatmapLayer
顯示密度,或使用每個叢集一個符號的叢集標記,指出多個資料點所在位置。詳情請參閱標記叢集教學課程。
最佳化查詢
每次查詢 BigQuery 皆會掃描整個資料表,為了更精準的使用 BigQuery 的額度,應只針對必要的欄位進行查詢。
經緯度值以浮點數而非字串型態儲存,查詢上會比較快。
匯出有趣的結果
這裡的範例需要使用者對 BigQuery 資料表進行驗證,因此不適用於所有用途。發現有趣的模式後,您可以從 BigQuery 匯出結果,並使用 Google 地圖資料層建立靜態資料集,輕鬆與更多人分享。
無聊的法律條文
請注意《Google 地圖平台服務條款》。如要進一步瞭解 Google 地圖平台價格,請參閱線上說明文件。
使用更多資料!
BigQuery 中有許多公開資料集都包含經緯度欄,例如 2009 年至 2016 年的紐約市計程車資料集、Uber 和 Lyft 紐約市行程資料,以及 GDELT 資料集。
15. 恭喜!
希望這有助於您快速上手,對 BigQuery 資料表執行一些地理查詢,進而發掘模式並在 Google 地圖上以視覺化方式呈現。祝您順利完成對應!
後續步驟
如要進一步瞭解 Google 地圖平台或 BigQuery,請參閱下列建議。
如要進一步瞭解 Google 的無伺服器 PB 級資料倉儲服務,請參閱「什麼是 BigQuery」。
請參閱操作指南,瞭解如何使用 BigQuery API 建立簡易應用程式。
如要進一步瞭解如何啟用使用者互動,以便在 Google 地圖上繪製形狀,請參閱繪圖程式庫的開發人員指南。
請參閱這篇文章,瞭解在 Google 地圖上以視覺化方式呈現資料的其他方法。
請參閱 JavaScript 用戶端 API 入門指南,瞭解如何使用用戶端 API 存取其他 Google API 的基本概念。