使用 Google Maps Platform (JavaScript) 在 BigQuery 中查询和直观呈现位置数据

1. 概览

在可视化数据集中与位置相关的模式时,地图是一个非常强大的工具。这种关系可以是地点的名称、具体的纬度和经度值,也可以是具有特定边界(如人口普查区或邮政编码)的区域的名称。

当这些数据集很大时,它们很难使用传统工具进行查询和直观呈现。通过使用 Google BigQuery 查询数据并使用 Google Maps API 构建查询并直观呈现输出结果,您只需极少量的设置或编码即可快速探索数据中的地理模式,而无需管理存储超大型数据集的系统。

构建内容

在此 Codelab 中,您将编写并运行一些查询,演示如何使用 BigQuery 向超大型公共数据集提供基于位置的数据分析。您还将构建一个网页,以使用 Google Maps Platform JavaScript API 加载地图,然后使用适用于 JavaScript 的 Google API 客户端库BigQuery API 针对相同的大型公共数据集运行空间查询,并直观呈现这些数据。

学习内容

  • 如何使用 SQL 查询用户定义的函数BigQuery API,在 BigQuery 中快速查询 PB 级的位置数据集
  • 如何使用 Google Maps Platform 将 Google 地图添加到网页中并允许用户在页面上绘制形状
  • 如何显示 Google 地图上大型数据集的查询(如下面的示例图片所示),其中显示了 2016 年从帝国大厦开始的行程中出租车前往地点的密度。

2017 年 5 月 9 日上午 11.01.12 的屏幕截图

您需要满足的条件

  • 具备 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) 并创建一个新项目。屏幕顶部有一个“项目”下拉菜单:

f2a353c3301dc649.png

点击此项目下拉菜单后,您会看到一个让您创建新项目的菜单项:

56a42dfa7ac27a35.png

在显示“输入项目的新名称”的框中,输入新项目的名称,例如“BigQuery Codelab”:

Codelab - 创建项目 (1.png)

系统会为您生成一个项目 ID。项目 ID 在所有 Google Cloud 项目中都是唯一名称。请记住您的项目 ID,稍后将会用到。以上名称已被占用,您无法使用。在此 Codelab 的任意位置看到 YOUR_PROJECT_ID,请插入您自己的项目 ID。

启用结算功能

如需注册 BigQuery,请使用上一步中选择或创建的项目。必须为此项目启用结算功能。启用结算功能后,您可以启用 BigQuery API。

启用结算功能的方式取决于您是创建新项目还是为现有项目重新启用结算功能。

Google 提供 12 个月的免费试用期,供您试用价值高达 300 美元的 Google Cloud Platform,此 Codelab 也可用于此 Codelab。如需了解详情,请访问 https://cloud.google.com/free/

新项目

创建新项目时,系统会提示您选择要关联到该项目的结算帐号。如果您只有一个结算帐号,该帐号会自动关联到您的项目。

如果您没有结算帐号,则必须先创建一个并为项目启用结算功能,然后才能使用诸多 Google Cloud Platform 功能。如需创建新的结算帐号并为项目启用结算功能,请按照创建新结算帐号中的说明操作。

现有项目

如果您的某个项目暂时停用了结算功能,则可以重新启用结算功能:

  1. 转到 Cloud Platform Console
  2. 从项目列表中,选择要为其重新启用结算功能的项目。
  3. 打开控制台左侧菜单,然后选择结算 结算。系统会提示您选择结算帐号。
  4. 点击设置帐号

创建新的结算帐号

如要创建新结算帐号,请执行以下操作:

  1. 转到 Cloud Platform Console 并登录;如果您还没有帐号,请注册一个。
  2. 打开控制台左侧菜单,然后选择结算 结算
  3. 点击新建结算帐号按钮。(请注意,如果这不是您的第一个结算帐号,您需要先点击页面顶部附近的现有结算帐号的名称,然后点击管理结算帐号,以打开结算帐号列表。)
  4. 输入结算帐号的名称以及您的结算信息。您看到的选项取决于您的帐单邮寄地址所在的国家/地区。请注意,对于美国帐号,创建帐号后无法更改纳税身份。
  5. 点击提交并启用结算功能

默认情况下,结算帐号的创建者是该帐号的结算管理员。

如需了解如何验证银行账户和添加备用付款方式,请参阅添加、移除或更新付款方式

启用 BigQuery API

如需在项目中启用 BigQuery API,请转到控制台中的 BigQuery API 页面 Marketplace,然后点击蓝色的“Enable'”按钮。

3.在 BigQuery 中查询位置数据

您可以通过三种方式查询在 BigQuery 中存储为纬度、经度值的位置数据。

  • 矩形查询:将相关区域指定为一个查询,用于选择最小和最大纬度和经度范围中的所有行。
  • 半径查询:通过使用半正弦公式和数学函数计算点周围的圆形,为地球形状建模,以指定感兴趣的区域。
  • 多边形查询:指定自定义形状,并使用用户定义函数来表示点逻辑,以便测试每行的纬度和经度是否位于形状内。

首先,使用 Google Cloud Platform Console 的“BigQuery”部分中的查询编辑器,对纽约市的出租车数据运行以下查询。

标准 SQL 与旧版 SQL

BigQuery 支持两种 SQL 版本:旧版 SQL标准 SQL。后者是 2011 ANSI 标准。在本教程中,我们将使用标准 SQL,因为它具有更好的标准合规性。

如果要在 BigQuery 编辑器中执行旧版 SQL,请执行以下操作:

  1. 点击“更多”按钮
  2. 从下拉菜单中选择“查询设置”
  3. 在“SQL 方言”下,选择“旧版”单选按钮
  4. 点击“保存”按钮

矩形查询

在 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 数学函数,您可以使用哈维公式构建 SQL 查询,该公式近似于地球表面上的圆形区域或球帽。

下面是一个以 40.73943, -73.99585 为中心、半径为 0.1 公里的圆形查询的 BigQuery SQL 语句示例。

它使用 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 

半正规公式的 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 月 8 日

45.28 岁

10.53

6.42

布朗克斯(The Bronx)

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 传入自定义多边形,并对每行执行测试,从而仅返回纬度和经度在该多边形范围内的行。在 BigQuery 参考文档中详细了解 UDF

多边形入点算法

在 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。为此,请执行以下操作:

  1. 在 Google Cloud Platform Console 中,转到市场
  2. 在 Marketplace 中搜索“Maps JavaScript API”
  3. 在搜索结果中点击 Maps JavaScript API 的图块
  4. 点击“启用”按钮

生成 API 密钥

要向 Google Maps Platform 发出请求,您需要生成 API 密钥并将其随所有请求一起发送。如需生成 API 密钥,请执行以下操作:

  1. 在 Google Cloud Platform Console 中,点击汉堡形菜单以打开左侧导航栏
  2. 选择 API 和 Service ' > Credentials'
  3. 点击“Create Credential”按钮,然后选择“API Key'
  4. 复制新的 API 密钥

下载代码并设置网络服务器

点击以下按钮下载此 Codelab 的所有代码:

解压下载的 ZIP 文件。此操作将解压根文件夹 (bigquery),其中包含此 Codelab 的每个步骤的对应文件夹,以及您需要的所有资源。

stepN 文件夹包含本 Codelab 的每个步骤所需的结束状态。它们仅供参考。我们将在名为 work 的目录中完成所有编码工作。

设置本地网络服务器

尽管您可以随意使用自己的网络服务器,但此 Codelab 非常适合 Chrome 网络服务器。如果您尚未安装此应用,可以从 Chrome 网上应用店安装它。

安装完毕后,打开该应用。在 Chrome 中,您可以按照以下步骤操作:

  1. 打开 Chrome
  2. 在顶部的地址栏中输入 chrome://apps
  3. 按 Enter 键
  4. 在打开的窗口中,点击 Web 服务器图标 您也可以右键点击应用,在常规或固定标签页、全屏或新窗口中打开 a3ed00e79b8bfee7.png 您接下来会看到以下对话框,您可在其中配置本地网络服务器: 81b6151c3f60c948.png
  5. 点击“选择文件夹”并选择您下载 Codelab 示例文件的位置
  6. 在“选项”部分,选中“自动显示 index.html'”旁边的复选框:17f4913500faa86f.png
  7. 向左滑动标有“网络服务器:STARTED&#39”的切换开关,然后向右滑动,以停止并重启网络服务器

a5d554d0d4a91851.png

5. 加载地图和绘图工具

创建基本地图页面

从一个简单的 HTML 网页开始,该网页使用 Maps JavaScript API 和几行 JavaScript 内容加载 Google 地图。您不妨从 Google Maps Platform 的简单地图示例中的代码入手。您可在此处复制该文件,然后将其复制并粘贴到您所选的文本编辑器或 IDE 中,或者从您下载的代码库中打开 index.html,即可找到此文件。

  1. index.html 复制到代码库的本地副本的 work 文件夹中
  2. 将 img/ 文件夹复制到存储库的本地副本的 work/ 文件夹中
  3. 在文本编辑器或 IDE 中打开 work/index.html
  4. YOUR_API_KEY 替换为您之前创建的 API 密钥
<script src="https://maps.googleapis.com/maps/api/js?key=YOUR_API_KEY&callback=initMap"
    async defer></script>
  1. 在浏览器中,打开 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 启用绘图库。

此 Codelab 还使用 HeatmapLayer,因此您还将更新脚本以请求可视化库。为此,请添加 libraries 参数,并以逗号分隔值的形式指定 visualizationdrawing 库,例如 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 添加到您的地图中,同时启用 CircleRectanglePolygon 工具。

最好将所有 DrawingManager 设置代码放入一个新函数中,以便在 index.html 的副本中执行以下操作:

  1. 添加一个名为 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);
}
  1. 创建地图对象后,在 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();
}
  1. 重新加载 index.html 并检查您的绘制工具是否可见。此外,检查您能否用其绘制圆形、矩形和多边形。

您可以点击并拖动绘制圆形和矩形,但需要为每个顶点点击并双击以完成绘制,才能绘制多边形。

处理绘图事件

您需要一些代码来处理用户完成形状绘制时触发的事件,就像您需要绘制形状的坐标来构建 SQL 查询一样。

我们将在后续步骤中添加此代码,但现在我们将对三个空事件处理脚本进行打桩,以处理 rectanglecompletecirclecompletepolygoncomplete 事件。处理程序无需在此阶段运行任何代码。

将以下内容添加到 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,并使用它与 BigQuery 进行交互。

添加适用于 JavaScript 的 Google Client 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 Console 项目中设置一些凭据。

创建 OAuth 2.0 凭据

  1. 在 Google Cloud Console 的导航菜单中,选择 API 和服务 > 凭据

在设置凭据之前,您需要为应用的授权屏幕添加一些配置,让最终用户在授权您的应用代表他们访问 BigQuery 数据时会看到该配置。

为此,请点击 OAuth 同意屏幕标签页。2. 您需要将 Big Query API 添加到此令牌的范围内。点击“Google API 的作用域”部分中的添加作用域按钮。3.在列表中,选中范围为 ../auth/bigqueryBig Query API 条目旁边的复选框。4.点击添加。5. 在“应用名称”字段中输入一个名称。6. 点击保存,保存您的设置。7. 接下来,您可以创建 OAuth 客户端 ID。为此,请点击创建凭据

4d18a965fc760e39.png

  1. 在下拉菜单中,点击 OAuth 客户端 ID1f8b36a1c27c75f0.png
  2. 在“应用类型”下,选择网络应用
  3. 在“应用名称”字段中,为您的项目输入一个名称。例如“BigQuery 和 Google 地图”。
  4. 限制下的“已获授权的 JavaScript 来源”字段中,输入 localhost 的网址,包括端口号。例如:http://localhost:8887
  1. 点击创建按钮。

弹出式窗口会显示客户端 ID 和客户端密钥。您需要具有客户端 ID 才能对 BigQuery 进行身份验证。将其复制并粘贴到 work/index.html 中,作为一个名为 clientId 的新全局 JavaScript 变量。

let clientId = 'YOUR_CLIENT_ID';

7. 授权和初始化

在初始化地图之前,您的网页需要授权用户访问 BigQuery。在此示例中,我们使用 OAuth 2.0,如 JavaScript Client API 文档的授权部分中所述。您需要使用 OAuth 客户端 ID 和项目 ID 来发送查询。

在网页上加载 Google Client API 后,您需要执行以下步骤:

  • 授权用户。
  • 如果授权,请加载 BigQuery API。
  • 加载并初始化地图。

有关完成后的 HTML 网页的外观示例,请参阅 step3/map.html

授权用户

应用的最终用户需要代表应用访问 BigQuery 中的数据。用于 JavaScript 的 Google Client API 会处理 OAuth 逻辑以完成此操作。

在实际应用中,您可以就如何集成授权步骤做出许多选择。

例如,您可以从按钮等界面元素调用 authorize(),或者在页面加载后调用该函数。在这里,我们选择使用 gapi.load() JavaScript 方法中的回调函数,在 JavaScript 版 Google Client API 加载后授权用户。

在用于加载 JavaScript 版 Google Client 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 的数据集并将其展开即可查看表格。选择黄色出租车行程表:bigquery-public-data.new_york_taxi_trips.tlc_yellow_trips_2016)。

指定项目 ID

在 API 调用中,您需要指定 Google Cloud Platform 项目的名称以便进行结算。在此 Codelab 中,这与包含表的项目不同。如果您正在使用通过上传数据在您自己的项目中创建的表,则此项目 ID 将与 SQL 语句中的项目 ID 相同。

将 JavaScript 变量添加到您的代码中,以保留对包含您要查询的表的公共数据集项目的引用,以及表名称和数据集名称。您还需要一个单独的变量来引用您自己的结算项目 ID。

将名为 billingProjectId, publicProjectId, datasetIdtableName 的全局 JavaScript 变量添加到 index.html 的副本中。

使用 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() 函数,以添加一个将响应限制为 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 类。HotmapLayer 采用一组纬度和经度坐标,因此您可以轻松地将查询返回的行转换为热图。

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 月周六上车分布的分布情况:

7b1face0e7c71c78

11. 以点为中心按半径查询

半径查询非常相似。借助 BigQuery 的旧版 SQL 数学函数,您可以使用哈维公式构造一个 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;
}

试试看!

添加上面的代码,然后尝试“Circle'”工具以选择地图区域。结果应如下所示:

845418166b7cc7a3.png

12. 查询任意形状

回顾:SQL 不支持使用除矩形和圆形以外的任意形状进行查询。BigQuery 没有任何原生几何图形数据类型,因此要使用多边形形状运行查询,您需要使用另一种方法来简化 SQL 查询。

用户定义的函数 (UDF) 是一项非常强大的 BigQuery 功能。UDF 会在 SQL 查询中执行 JavaScript 代码。

此步骤的有效代码位于 step7/map.html

BigQuery API 中的 UDF

适用于 UDF 的 BigQuery API 方法与 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 语句。在此示例中,在 WHERE 表达式中调用了 UDF。

与 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 黄色出租车数据中的自提点数,并将所选数据绘制为热图。

2017 年 5 月 9 日上午 10.00.48 屏幕截图

13. 更进一步

以下是扩展此 Codelab 以查看数据其他方面的一些建议。您可以在代码库中的 step8/map.html 中找到这些提示的有效示例。

映射流失次数

到目前为止,我们仅在地图上显示取货地点。通过请求 dropoff_latitudedropoff_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。可以从变量中读取这些索引,使代码更易于管理。NB 热图的 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 街之间的第 5 大道上。此缩放级别未显示的高密度地理位置包括拉瓜迪亚机场和肯尼迪国际机场、世界贸易中心和炮台公园。

2017 年 5 月 9 日上午 10.40.01 屏幕截图

设置基本地图的样式

使用 Maps JavaScript API 创建 Google 地图时,您可以使用 JSON 对象设置地图样式。若要直观呈现数据,可将地图中的颜色静音。您可以访问 mapstyle.withgoogle.com,使用 Google Maps API 样式向导创建和试用地图样式。

您可以在初始化地图对象时或之后的任何时间设置地图样式。下面展示了如何在 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;
}

动画图片可以添加到页面中,但需要隐藏起来,以及在 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.totalRowsresponse.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);
  })
}

网页看起来应如下所示。

2017 年 5 月 10 日下午 2:32.19 的屏幕截图

请查看 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,请查看以下建议。

请参阅什么是 BigQuery,详细了解 Google 的无服务器 PB 级数据仓库服务。

查看关于使用 BigQuery API 创建简单应用的方法指南。

请参阅绘图库开发者指南,详细了解如何在 Google 地图上启用用户互动以绘制形状。

了解在 Google 地图上直观呈现数据的其他方法。

请参阅 JavaScript Client AP 入门指南,了解使用 Client API 访问其他 Google API 的基本概念。