1. 概览
在直观呈现与位置相关的某种数据集中的模式时,地图可能是一种非常强大的工具。此关系可以是地点名称、特定纬度和经度值,也可以是具有特定边界的区域的名称,例如人口普查区或邮政编码。
当这些数据集变得非常庞大时,使用传统工具可能难以查询和直观呈现它们。通过使用 Google BigQuery 查询数据和 Google Maps API 构建查询并直观呈现输出,您可以快速探索数据中的地理模式,而无需进行大量设置或编码,也无需管理用于存储超大数据集的系统。
构建内容
在此 Codelab 中,您将编写并运行一些查询,这些查询演示了如何使用 BigQuery 为非常庞大的公共数据集提供基于位置的分析洞见。您还将构建一个使用 Google Maps Platform JavaScript API 加载地图的网页,然后使用 Google API 客户端库(适用于 JavaScript)和 BigQuery API 针对同一超大型公共数据集运行空间查询并直观呈现查询结果。
学习内容
- 如何使用 BigQuery、SQL 查询、用户定义的函数 和 BigQuery API 在几秒钟内查询 PB 级位置信息数据集
- 如何使用 Google Maps Platform 将 Google 地图添加到网页中,并允许用户在其中绘制形状
- 如何在 Google 地图上直观呈现针对大型数据集的查询,如下面的示例图片所示,该图片显示了 2016 年从帝国大厦附近街区出发的行程的出租车下客地点密度。
所需条件
- 具备 HTML、CSS、JavaScript、SQL 和 Chrome 开发者工具方面的基础知识
- 新版网络浏览器,例如最新版本的 Chrome、Firefox、Safari 或 Edge。
- 您自己喜欢用的文本编辑器或 IDE
技术
BigQuery
BigQuery 是 Google 针对超大数据集提供的数据分析服务。它具有 RESTful API,并支持使用 SQL 编写的查询。如果您有包含纬度和经度值的数据,则可以使用这些数据按位置查询数据。优势在于,您可以直观地探索非常大的数据集,查看其中的模式,而无需管理任何服务器或数据库基础架构。无论表有多大,您都可以利用 BigQuery 的出色可伸缩性和托管式基础架构在几秒钟内获得问题的答案。
Google Maps Platform
Google Maps Platform 可让您以编程方式访问 Google 的地图、地点和路线数据。目前,已有超过 200 万个网站和应用使用该服务向其用户提供嵌入式地图和基于位置的查询。
借助 Google Maps Platform JavaScript API 绘图图层,您可以在地图上绘制形状。这些数据可以转换为输入,以便针对在列中存储了纬度和经度值的 BigQuery 表运行查询。
首先,您需要一个已启用 BigQuery 和 Maps API 的 Google Cloud Platform 项目。
2. 准备工作
Google 账号
如果您还没有 Google 账号(Gmail 或 Google Apps),则必须创建一个。
创建项目
登录 Google Cloud Platform Console ( console.cloud.google.com) 并创建一个新项目。在屏幕顶部,有一个“项目”下拉菜单:
点击此项目下拉菜单后,您会看到一个可用于创建新项目的菜单项:
在显示“为项目输入新名称”的框中,输入新项目的名称,例如“BigQuery Codelab”:
系统会为您生成项目 ID。项目 ID 是一个在所有 Google Cloud 项目中均保持唯一的名称。请记下项目 ID,稍后会用到。上述名称已被占用,您无法使用。在本 Codelab 中,请将所有 YOUR_PROJECT_ID 替换为您自己的项目 ID。
启用结算功能
如需注册 BigQuery,请使用在上一步中选择或创建的项目。必须为此项目启用结算功能。启用结算功能后,您便可以启用 BigQuery API。
启用结算功能的方式取决于您是创建新项目还是为现有项目重新启用结算功能。
Google 提供 12 个月的免费试用期,在此期间您可免费使用价值高达 300 美元的 Google Cloud Platform 服务。您或许可以将此优惠用于本 Codelab,如需了解更多详情,请访问 https://cloud.google.com/free/。
新项目
当您创建新项目时,系统会提示您选择要将哪个结算账号关联到项目。如果您只有一个结算账号,该账号会自动关联到您的项目。
如果您没有结算账号,则必须先创建一个并为项目启用结算功能,然后才能使用各项 Google Cloud Platform 功能。如需创建新的结算账号并为项目启用结算功能,请按照创建新的结算账号中的说明操作。
现有项目
如果您有暂时停用了结算功能的项目,则可以按照以下步骤重新启用结算功能:
- 前往 Cloud Platform 控制台。
- 从项目列表中,选择要为其重新启用结算功能的项目。
- 打开控制台左侧菜单,然后选择结算 图标
。系统会提示您选择结算账号。
- 点击设置账号。
创建新的结算账号
如要创建新结算账号,请执行以下操作:
- 前往 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,因为它更符合标准规范。
如果您希望在 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 参考文档。
Point In Polygon 算法
在 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
在此 Codelab 中,您需要在项目中启用 Google Maps Platform 的 Maps JavaScript API。为此,请执行以下操作:
- 在 Google Cloud Platform 控制台中,前往 Marketplace
- 在 Marketplace 中,搜索“Maps JavaScript API”
- 点击搜索结果中的 Maps JavaScript API 图块
- 点击“启用”按钮
生成 API 密钥
若要向 Google Maps Platform 发出请求,您需要生成 API 密钥并将其随所有请求一起发送。如需生成 API 密钥,请执行以下操作:
- 在 Google Cloud Platform 控制台中,点击汉堡菜单以打开左侧导航栏
- 依次选择 “API 和服务”>“凭据”
- 点击“创建凭据”按钮,然后选择“API 密钥”
- 复制新 API 密钥
下载代码并设置 Web 服务器
点击以下按钮可下载本 Codelab 的所有代码:
解压下载的 ZIP 文件。此操作会解压缩一个根文件夹 (bigquery
),其中包含本 Codelab 的每个步骤的一个文件夹,以及您需要的所有资源。
stepN
文件夹包含本 Codelab 的每个步骤的预期结束状态。这些内容可供参考。我们将在名为 work
的目录中执行所有编码工作。
设置本地 Web 服务器
尽管您可以随意使用自己的网络服务器,但此 Codelab 旨在与 Chrome Web 服务器配合使用。如果您尚未安装该应用,则可以通过 Chrome 网上应用店安装。
安装完成后,打开应用。在 Chrome 中,您可以按以下步骤操作:
- 打开 Chrome
- 在顶部的地址栏中,输入 chrome://apps
- 按 Enter 键
- 在打开的窗口中,点击“网络服务器”图标 您还可以右键点击某个应用,以在常规标签页或固定标签页、全屏或新窗口中打开该应用
接下来,您将看到以下对话框,该对话框可让您配置本地网络服务器:
- 点击“选择文件夹”,然后选择您下载 Codelab 示例文件的位置
- 在“选项”部分中,选中“自动显示 index.html”旁边的复选框:
- 将标有“Web Server: STARTED”(Web 服务器:已启动)的切换开关向左滑动,然后再向右滑动,以停止并重启 Web 服务器
5. 加载地图和绘图工具
创建基本地图网页
首先创建一个简单的 HTML 网页,该网页使用 Maps JavaScript API 和几行 JavaScript 代码加载 Google 地图。Google Maps Platform 的简单地图示例中的代码是一个不错的起点。此处再现了该文件,您可以将其复制并粘贴到您选择的文本编辑器或 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 密钥是否正确,以及本地 Web 服务器是否处于活动状态。
更改默认位置和缩放级别
用于设置位置和缩放级别的代码位于 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 启用绘图库。
此 Codelab 还使用了 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 可帮助您避免编写大量构建请求、解析响应和处理身份验证所需的样板代码。由于我们将开发基于浏览器的应用,因此本 Codelab 将通过 JavaScript 版 Google API 客户端库使用 BigQuery API。
接下来,您将添加代码以在网页中加载此 API,并使用该 API 与 BigQuery 进行交互。
添加适用于 JavaScript 的 Google 客户端 API
您将使用适用于 JavaScript 的 Google 客户端 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 添加到此令牌的范围中。点击“Google API 的范围”部分中的添加范围按钮。3. 在列表中,选中具有 ../auth/bigquery
范围的 BigQuery API 条目旁边的复选框。4. 点击添加。5. 在“应用名称”字段中输入一个名称。6. 点击保存,保存您的设置。7. 接下来,您将创建 OAuth 客户端 ID。为此,请点击创建凭据:
- 在下拉菜单中,点击 OAuth 客户端 ID。
- 在“应用类型”下,选择 Web 应用。
- 在“应用名称”字段中,输入项目的名称。例如“BigQuery 和 Maps”。
- 在限制下的“已获授权的 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 客户端 API 会处理 OAuth 逻辑以实现此目的。
在实际应用中,您可以通过多种方式集成授权步骤。
例如,您可以从按钮等界面元素调用 authorize()
,也可以在页面加载完毕后调用。在此示例中,我们选择在加载 JavaScript 版 Google 客户端 API 后,通过在 gapi.load()
方法中使用回调函数来授权用户。
在 <script>
标记后立即编写一些代码,以加载 Google JavaScript 客户端 API,从而加载客户端库和身份验证模块,以便我们立即对用户进行身份验证。
<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 项目的名称以用于结算。在此 Codelab 中,此项目与包含表的项目不是同一项目。如果您处理的是通过上传数据在自己的项目中创建的表,则此项目 ID 与 SQL 语句中的项目 ID 相同。
向代码添加 JavaScript 变量,以保存对包含您要查询的表的公共数据集项目的引用,以及表名称和数据集名称。您还需要一个单独的变量来引用您自己的结算项目 ID。
向 index.html 的副本添加名为 billingProjectId, publicProjectId, datasetId
和 tableName
的全局 JavaScript 变量。
使用 BigQuery 公共数据集项目中的详细信息初始化变量 'publicProjectId'
、'datasetId'
和 'tableName'
。使用您自己的项目 ID(在此 Codelab 前面的“准备工作”中创建的项目 ID)初始化 billingProjectId
。
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 级数据),并且可以每秒增加数十万行。因此,请务必尽量限制返回的数据量,以便在地图上绘制这些数据。如果绘制非常大的结果集(数万行或更多)中每一行的位置,则会导致地图无法读取。您可以使用多种技术在 SQL 查询和地图中汇总位置信息,还可以限制查询将返回的结果。
此步骤的完整代码位于 step5/map.html 中。
为了使传输到网页的数据量在本 Codelab 中保持在合理的大小范围内,请修改 rectangleSQL()
函数以添加一条将响应限制为 10, 000 行的语句。在下面的示例中,此值在名为 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 方法与 Web 控制台略有不同:您需要调用 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)
方法调用插入,以将包含 x、y 坐标对的 JavaScript 数组转换为字符串。多边形对象作为实参传递给用于构建 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. 深入了解
以下是一些建议,可帮助您扩展此 Codelab,以查看数据的其他方面。您可以在代码库中的 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 通常会在几秒钟内给出响应,但有时在查询运行时向用户显示正在进行的操作会很有用。
向网页添加一些界面,用于显示 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;
}
您可以将动画图形添加到网页中,但将其隐藏起来,直到需要时才显示,并使用一些 JavaScript 和 CSS 代码在 BigQuery 作业运行时显示该图形。
添加一些 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
属性。将毫秒转换为秒,并将字节转换为千兆字节以供显示,这对用户很有帮助,如以下代码示例所示。
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 Maps Platform 服务条款。如需详细了解 Google Maps Platform 价格,请参阅在线文档。
畅享更多数据流量!
BigQuery 中有许多包含纬度和经度列的公共数据集,例如 2009 年至 2016 年的纽约市出租车数据集、Uber 和 Lyft 纽约市行程数据以及 GDELT 数据集。
15. 恭喜!
我们希望本文能帮助您快速上手,针对 BigQuery 表运行一些地理位置查询,以便发现模式并在 Google 地图上直观呈现这些模式。祝您在地图上一切顺利!
后续操作
如果您想详细了解 Google Maps Platform 或 BigQuery,请参阅以下建议。
如需详细了解 Google 的无服务器 PB 级数据仓库服务,请参阅什么是 BigQuery。
请参阅有关使用 BigQuery API 创建简单应用的操作指南。
如需详细了解如何启用用户互动以在 Google 地图上绘制形状,请参阅绘图库开发者指南。
不妨了解一下在 Google 地图上直观呈现数据的其他方法。
请参阅 JavaScript 客户端 API 入门指南,了解如何使用客户端 API 访问其他 Google API 的基本概念。