Google Maps Platform(JavaScript)を使用して BigQuery で位置情報をクエリして可視化する

1. 概要

地図は、データセット内の位置情報に関連するパターンを可視化する際に非常に強力なツールとなります。この関係は、場所の名前、特定の緯度と経度の値、または国勢調査区や郵便番号などの特定の境界を持つ地域の名前です。

このようなデータセットが非常に大きくなると、従来のツールを使用してクエリを実行したり、可視化したりすることが難しくなることがあります。Google BigQuery を使用してデータをクエリし、Google Maps API を使用してクエリを構築して出力を可視化することで、大規模なデータセットを保存するシステムを管理することなく、わずかな設定とコーディングで、データ内の地理的パターンをすばやく探索できます。

作成するアプリの概要

この Codelab では、BigQuery を使用して非常に大規模な一般公開データセットに位置情報に基づく分析情報を提供する方法を示すクエリを作成して実行します。また、Google Maps Platform JavaScript API を使用して地図を読み込むウェブページを作成し、Google APIs Client Library for JavascriptBigQuery API を使用して、同じ非常に大きな一般公開データセットに対して空間クエリを実行して可視化します。

学習内容

  • BigQuery でペタバイト規模の位置情報データセットに対して SQL クエリユーザー定義関数 BigQuery API を使用して数秒でクエリを実行する方法
  • Google Maps Platform を使用して Google マップをウェブページに追加し、ユーザーが地図上に図形を描画できるようにする方法
  • 下の画像例のように、エンパイア ステート ビルディング周辺のブロックから始まった 2016 年のタクシーの降車場所の密度を示す、Google マップ上の大規模なデータセットに対するクエリを可視化する方法。

Screen Shot 2017-05-09 at 11.01.12 AM.png

必要なもの

  • HTML、CSS、JavaScript、SQL、Chrome DevTools の基本的な知識
  • 最新バージョンの 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 API と Maps API が有効になっている Google Cloud Platform プロジェクトが必要です。

2. 設定方法

Google アカウント

Google アカウント(Gmail または Google Apps)をお持ちでない場合は、1 つ作成する必要があります。

プロジェクトを作成

Google Cloud Platform コンソール(console.cloud.google.com)にログインし、新しいプロジェクトを作成します。画面上部に [Project] プルダウン メニューがあります。

f2a353c3301dc649.png

このプロジェクトのプルダウン メニューをクリックすると、新しいプロジェクトを作成できるメニュー項目が表示されます。

56a42dfa7ac27a35.png

[Enter a new name for your project] ボックスに、新しいプロジェクトの名前(例: 「BigQuery Codelab」)を入力します。

Codelab - create project (1).png

プロジェクト ID が生成されます。プロジェクト ID は、すべての Google Cloud プロジェクトを通じて一意の名前にする必要があります。プロジェクト ID は後で使用するため、覚えておいてください。上記の名前はすでに使用されているため使用できません。この Codelab で YOUR_PROJECT_ID が表示されている箇所は、ご自身のプロジェクト ID に置き換えてください。

課金を有効にする

BigQuery に登録するには、前の手順で選択または作成したプロジェクトを使用します。このプロジェクトで課金を有効にする必要があります。課金が有効になると、BigQuery API を有効にできます。

課金を有効にする方法は、新しいプロジェクトを作成するか、既存のプロジェクトに対して課金を再度有効にするかによって異なります。

Google では、Google Cloud Platform の使用量 300 ドル相当までを 12 か月間無料でお試しいただけるトライアルをご用意しています。この Codelab でもご利用いただける可能性があります。詳しくは、https://cloud.google.com/free/ をご覧ください。

新しいプロジェクト

新しいプロジェクトを作成するとき、プロジェクトにリンクする請求先アカウントを選択するよう求められます。請求先アカウントが 1 つしかない場合は、そのアカウントが自動的にプロジェクトにリンクされます。

請求先アカウントがない場合は、Google Cloud Platform の各種機能を使用する前に、請求先アカウントを作成してプロジェクトの課金を有効にする必要があります。新しい請求先アカウントを作成してプロジェクトの課金を有効にするには、新しい請求先アカウントを作成するの手順に沿って操作します。

既存のプロジェクト

既存のプロジェクトの課金を一時的に無効にしている場合は、課金を再度有効にできます。

  1. Cloud Platform Console に移動します。
  2. プロジェクト リストから、課金を再度有効にするプロジェクトを選択します。
  3. Console の左側のメニューを開き、[お支払い ] 課金 を選択します。請求先アカウントを選択するよう求められます。
  4. [アカウントを設定] をクリックします。

新しい請求先アカウントを作成する

新しい請求先アカウントを作成する手順は次のとおりです。

  1. Cloud Platform Console にアクセスしてログインします。まだアカウントを持っていない場合は登録します。
  2. Console の左側のメニューを開き、[お支払い ] 課金 を選択します。
  3. [新しい請求先アカウント] ボタンをクリックします(初めての請求先アカウントでない場合は、まずページ上部にある既存の請求先アカウントの名前をクリックして請求先アカウントのリストを開き、[請求先アカウントを管理] をクリックします)。
  4. 請求先アカウントの名前を入力し、請求先情報を入力します。表示されるオプションは請求先住所が所在する国によって異なります。米国アカウントの場合、アカウントの作成後に税務ステータスは変更できないことに注意してください。
  5. [送信して課金を有効にする] をクリックします。

デフォルトでは、請求先アカウントを作成した人はそのアカウントの課金管理者になります。

銀行口座の確認と予備のお支払い方法の追加については、お支払い方法の追加、削除、更新をご覧ください。

BigQuery API を有効にする

プロジェクトで BigQuery API を有効にするには、コンソールの BigQuery API ページ Marketplace に移動し、青色の [有効にする] ボタンをクリックします。

3. BigQuery で位置情報をクエリする

BigQuery に緯度と経度の値として保存されている位置情報をクエリするには、次の 3 つの方法があります。

  • 矩形クエリ: 最小緯度と最大緯度、最小経度と最大経度の範囲内のすべての行を選択するクエリとして、対象領域を指定します。
  • 半径クエリ: ハーバーサインの公式と数学関数を使用して地球の形状をモデル化し、ポイントを中心とする円を計算して、対象領域を指定します。
  • ポリゴン クエリ: カスタムシェイプを指定し、ユーザー定義関数を使用して、各行の緯度と経度がシェイプの内側にあるかどうかをテストするために必要なポイントインポリゴン ロジックを表現します。

まず、Google Cloud Platform コンソールの BigQuery セクションにあるクエリエディタを使用して、NYC タクシーデータに対して次のクエリを実行します。

標準 SQL とレガシー SQL

BigQuery は、レガシー SQL標準 SQL の 2 つのバージョンの SQL をサポートしています。後者は 2011 年の ANSI 標準です。このチュートリアルでは、標準 SQL を使用します。標準 SQL は標準コンプライアンスが優れているためです。

BigQuery エディタでレガシー SQL を実行する場合は、次の操作を行います。

  1. [その他] ボタンをクリックします。
  2. プルダウン メニューから [クエリ設定] を選択します。
  3. [SQL 言語] で [レガシー] ラジオボタンを選択します。
  4. [保存] ボタンをクリックします。

長方形クエリ

BigQuery では、長方形クエリの作成は非常に簡単です。緯度と経度の最小値と最大値の範囲内の場所にある結果のみを返す WHERE 句を追加するだけです。

BigQuery コンソールで次の例を試してください。このクエリは、ミッドタウンとローワー マンハッタンを含む長方形のエリアで開始された乗車に関する平均乗車統計情報を取得します。試すことができる場所は 2 つあります。2 番目の 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

2 つのクエリの結果から、2 つの場所での乗車について、平均乗車距離、運賃、チップに大きな違いがあることがわかります。

Manhattan

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 クエリを作成できます。

40.73943, -73.99585 を中心とする半径 0.1 km の円形クエリの BigQuery SQL ステートメントの例を次に示します。

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 

ハバサインの公式の SQL は複雑に見えますが、必要なのは円の中心座標、半径、BigQuery のプロジェクト名、データセット名、テーブル名を指定することだけです。

エンパイア ステート ビルディングから 100 m 以内の乗車場所の平均乗車統計情報を計算するクエリの例を次に示します。このコードを 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 クエリとは異なるアプローチが必要です。1 つの方法は、JavaScript でジオメトリ関数を定義し、BigQuery でユーザー定義関数(UDF)として実行することです。

多くのジオメトリ オペレーションは JavaScript で記述できるため、緯度と経度の値を含む BigQuery テーブルに対して簡単に実行できます。カスタム ポリゴンを UDF 経由で渡し、各行に対してテストを実行して、緯度と経度がポリゴンの内側にある行のみを返す必要があります。BigQuery リファレンスで UDF の詳細を確認する

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 アプローチでは 1 つのステートメントのみが必要ですが、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 を使用して 3 種類の空間クエリを実行しました。ご覧のとおり、このデータセットに対するクエリの結果データは場所によって大きく異なりますが、クエリを実行する場所を推測しない限り、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 コンソールで、[Marketplace] に移動します。
  2. Marketplace で「Maps JavaScript API」を検索します。
  3. 検索結果で Maps JavaScript API のタイルをクリックします。
  4. [有効にする] ボタンをクリックします。

API キーを生成

Google Maps Platform にリクエストを行うには、API キーを生成し、すべてのリクエストでその API キーを送信する必要があります。API キーを生成する手順は次のとおりです。

  1. Google Cloud Platform コンソールで、ハンバーガー メニューをクリックして左側のナビゲーションを開きます。
  2. [API とサービス] > [認証情報] を選択します。
  3. [認証情報を作成] ボタンをクリックし、[API キー] を選択します。
  4. 新しい API キーをコピーする

コードをダウンロードしてウェブサーバーを設定する

次のボタンをクリックして、この Codelab のすべてのコードをダウンロードします。

ダウンロードした zip ファイルを解凍すると、ルートフォルダ(bigquery)が展開されます。このフォルダには、この Codelab のステップごとに 1 つのフォルダと、必要なすべてのリソースが含まれています。

この Codelab の各ステップにおける望ましい最終状態は、stepN フォルダに格納されています。これらは参照用に用意されています。コーディング作業はすべて、work という名前のディレクトリで行います。

ローカル ウェブサーバーを設定する

独自のウェブサーバーを自由に使用できますが、この Codelab は Chrome ウェブサーバーでうまく動作するように設計されています。目的のアプリをまだインストールしていない場合は、Chrome ウェブストアからインストールできます。

インストールしたら、アプリを開きます。Chrome でアプリを開くには、次の手順を行います。

  1. Chrome を開く
  2. 上部のアドレスバーに「chrome://apps」と入力します。
  3. Enter キーを押してください
  4. 開いたウィンドウで、ウェブサーバー アイコンをクリックします。アプリを右クリックして、通常のタブ、固定タブ、全画面表示、新しいウィンドウで開くこともできます。a3ed00e79b8bfee7.png 次にこのダイアログが表示され、ローカル ウェブサーバーを構成できます。81b6151c3f60c948.png
  5. [CHOOSE FOLDER] をクリックして、コードラボのサンプル ファイルをダウンロードした場所を選択します。
  6. [オプション] セクションで、[index.html を自動的に表示] の横にあるチェックボックスをオンにします。17f4913500faa86f.png
  7. [Web Server: STARTED] と表示された切り替えボタンを左にスライドしてから右に戻して、ウェブサーバーを停止してから再起動します。

a5d554d0d4a91851.png

5. 地図と描画ツールを読み込む

基本的な地図ページを作成する

Maps JavaScript API と数行の JavaScript を使用して Google マップを読み込む簡単な HTML ページから始めます。Google Maps Platform のシンプルな地図のサンプルのコードから始めることをおすすめします。このコードは、任意のテキスト エディタまたは IDE にコピーして貼り付けるためにここに再掲されています。また、ダウンロードしたリポジトリから index.html を開いて見つけることもできます。

  1. index.html をリポジトリのローカルコピーの work フォルダにコピーします。
  2. リポジトリのローカルコピーの work/ フォルダに img/ フォルダをコピーします。
  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 パラメータを追加し、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 を追加する

ユーザーが描画したシェイプをクエリの入力として使用するには、CircleRectanglePolygon ツールを有効にして、DrawingManager を地図に追加します。

DrawingManager の設定コードをすべて新しい関数にまとめることをおすすめします。index.html のコピーで、次の操作を行います。

  1. 次のコードを含む setUpDrawingTools() という関数を追加して、DrawingManager を作成し、その map プロパティをページ内の地図オブジェクトを参照するように設定します。

google.maps.drawing.DrawingManager(options) に渡されるオプションは、描画された図形のデフォルトの図形描画タイプと表示オプションを設定します。クエリとして送信する地図の領域を選択する場合、シェイプの不透明度は 0 にする必要があります。使用可能なオプションの詳細については、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 の各イベントを処理する 3 つの空のイベント ハンドラをスタブアウトします。この段階では、ハンドラでコードを実行する必要はありません。

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 では、ブラウザベースのアプリケーションを開発するため、Google APIs Client Library for JavaScript を介して BigQuery API を使用します。

次に、この API をウェブページに読み込んで BigQuery とのやり取りに使用するコードを追加します。

JavaScript 用 Google クライアント API を追加する

Google Client API for Javascript を使用して、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 認証情報を作成する

  1. Google Cloud コンソールのナビゲーション メニューで、[API とサービス] > [認証情報] を選択します。

認証情報を設定する前に、アプリケーションのエンドユーザーがアプリに代わって BigQuery データにアクセスすることを承認する際に表示される認証画面の構成を追加する必要があります。

これを行うには、[OAuth 同意画面] タブをクリックします。2. このトークンのスコープに BigQuery API を追加する必要があります。[Google API のスコープ] セクションで [スコープを追加] ボタンをクリックします。3. リストから、../auth/bigquery スコープの [Big Query API] エントリの横にあるチェックボックスをオンにします。4. [追加] をクリックします。5. [アプリケーション名] フィールドに名前を入力します。6. [保存] をクリックして設定を保存します。7. 次に、OAuth クライアント ID を作成します。これを行うには、[認証情報を作成] をクリックします。

4d18a965fc760e39.png

  1. プルダウン メニューで [OAuth クライアント ID] をクリックします。1f8b36a1c27c75f0.png
  2. [アプリケーションの種類] で [ウェブ アプリケーション] を選択します。
  3. [アプリケーション名] フィールドに、プロジェクトの名前を入力します。たとえば、「BigQuery と Maps」などです。
  4. [制限] の [承認済みの JavaScript 生成元] フィールドに、ポート番号を含む localhost の URL を入力します。例: http://localhost:8887
  1. [作成] ボタンをクリックします。

クライアント ID とクライアント シークレットを示すポップアップが表示されます。BigQuery に対して認証を行うには、クライアント ID が必要です。コピーして、work/index.htmlclientId という新しいグローバル JavaScript 変数として貼り付けます。

let clientId = 'YOUR_CLIENT_ID';

7. 認可と初期化

ウェブページでは、地図を初期化する前に、ユーザーが BigQuery にアクセスできるように承認する必要があります。この例では、JavaScript クライアント API ドキュメントの認証セクションで説明されているように、OAuth 2.0 を使用します。クエリを送信するには、OAuth クライアント ID とプロジェクト ID を使用する必要があります。

ウェブページに Google Client API が読み込まれたら、次の手順を行います。

  • ユーザーを承認します。
  • 承認されている場合は、BigQuery API を読み込みます。
  • 地図を読み込んで初期化します。

完成した HTML ページの例については、step3/map.html をご覧ください。

ユーザーを承認する

アプリケーションのエンドユーザーは、自分の代わりに BigQuery のデータにアクセスする権限をアプリケーションに付与する必要があります。この処理は、JavaScript 用 Google クライアント API が OAuth ロジックを処理することで行われます。

実際のアプリケーションでは、承認ステップを統合する方法について多くの選択肢があります。

たとえば、ボタンなどの UI 要素から authorize() を呼び出すことも、ページの読み込み時に呼び出すこともできます。ここでは、gapi.load() メソッドのコールバック関数を使用して、JavaScript 用 Google クライアント API の読み込み後にユーザーを承認することを選択しています。

<script> タグの直後に、JavaScript 用の Google クライアント 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 パラメータを受け取る必要があります。これにより、ユーザーが正常に認証されたかどうかに応じてロジックのフローを制御できます。

また、ユーザーが正常に認証された場合に BigQuery API を読み込む loadApi という関数も追加します。

handleAuthResult() 関数にロジックを追加して、関数に authResult オブジェクトが渡され、オブジェクトの error プロパティの値が false の場合に loadApi() を呼び出すようにします。

gapi.client.load() メソッドを使用して BigQuery API を読み込むコードを loadApi() 関数に追加します。

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]);    
  });
}

request.execute() 呼び出しでコールバックとして checkJobStatus() メソッドを呼び出すように sendQuery メソッドを変更します。ジョブ 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 呼び出しを使用します。jobId のパラメータを受け取る getQueryResults() という関数をページに追加します。

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 内のデータに対して空間クエリを実行する方法は 3 つあります。

  • 長方形で選択(境界ボックスとも呼ばれます)。
  • 半径で選択し、
  • 強力な ユーザー定義関数機能です。

境界ボックスと半径のクエリの例については、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 を参照するための別の変数も必要です。

index.html のコピーに billingProjectId, publicProjectId, datasetIdtableName という名前の JavaScript グローバル変数を作成します。

BigQuery の一般公開データセット プロジェクトの詳細を使用して、変数 'publicProjectId''datasetId''tableName' を初期化します。billingProjectId を独自のプロジェクト ID(この Codelab の「設定」で作成した ID)で初期化します。

let billingProjectId = 'YOUR_PROJECT_ID';
let publicProjectId = 'bigquery-public-data';
let datasetId = 'new_york_taxi_trips';
let tableName = 'tlc_yellow_trips_2016';

次に、コードに 2 つの関数を追加して、SQL を生成し、前の手順で作成した sendQuery 関数を使用してクエリを BigQuery に送信します。

最初の関数は rectangleSQL() と呼び、地図座標で長方形の角を表す google.Maps.LatLng オブジェクトのペアという 2 つの引数を受け取る必要があります。

2 つ目の関数は 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 テーブルは非常に大きく(ペタバイト単位のデータ)、1 秒あたり数十万行のペースで増加する可能性があります。そのため、地図上に描画できるように、返されるデータ量を制限することが重要です。非常に大きな結果セット(数万行以上)のすべての行の位置を描画すると、地図が読めなくなります。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 クエリでは、タクシーの乗車場所の緯度と経度の値のみをリクエストしているため、レスポンスには 2 つの列のみが含まれます。

位置の配列をヒートマップ レイヤに割り当てたら、必ず 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.png

11. 地点を中心とした半径によるクエリ

半径クエリは非常によく似ています。BigQuery のレガシー SQL の数学関数を使用して、地球の表面の円形領域を近似する Haversine 公式を使用して SQL クエリを作成できます。

長方形の場合と同じ手法を使用して、OverlayComplete イベントを処理して、ユーザーが描画した円の中心と半径を取得し、同じ方法でクエリの SQL を作成できます。

この手順のコードの動作例は、コード リポジトリに step6/map.html として含まれています。

drawingManager.addListener('circlecomplete', circle => circleQuery(circle));

index.html のコピーに、circleQuery()haversineSQL() という 2 つの新しい空の関数を追加します。

次に、中心と半径を circleQuery(). という新しい関数に渡す circlecomplete イベント ハンドラを追加します。

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;
}

試してみよう:

上記のコードを追加し、[円] ツールを使用して地図のエリアを選択します。次のような結果が表示されます。

845418166b7cc7a3.png

12. 任意の形状のクエリ

まとめ: SQL では、長方形と円以外の任意の図形を使用したクエリはサポートされていません。BigQuery にはネイティブのジオメトリ データ型がないため、ポリゴン形状を使用してクエリを実行するには、単純な SQL クエリとは異なるアプローチが必要です。

この目的で使用できる BigQuery の非常に強力な機能の 1 つが、ユーザー定義関数(UDF)です。UDF は、SQL クエリ内で JavaScript コードを実行します。

このステップの動作コードは step7/map.html にあります。

BigQuery API の UDF

UDF の BigQuery API アプローチはウェブ コンソールとは少し異なります。jobs.insert method を呼び出す必要があります。

API 経由の標準 SQL クエリでは、ユーザー定義関数を使用するために必要な SQL ステートメントは 1 つだけです。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;
}

ここでは 2 つのことが行われています。まず、コードは、指定された点がポリゴン内にあるかどうかを判断する JavaScript コードをカプセル化する CREATE TEMPORARY FUNCTION ステートメントを作成しています。ポリゴンの座標は、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 年の NYC TLC Yellow Taxi データから乗車場所をクエリした結果の例です。選択したデータはヒートマップとして描画されています。

Screen Shot 2017-05-09 at 10.00.48 AM.png

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 は、1 ピクセルあたり 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 年にエンパイア ステート ビルディング周辺で乗車したすべての乗客の降車場所の分布を示すヒートマップです。ミッドタウンの目的地(赤い斑点)が、特にタイムズ スクエア周辺と 23rd St と 14th St の間の 5th Avenue 沿いに集中していることがわかります。このズームレベルでは表示されていませんが、ラガーディア空港、JFK 空港、ワールド トレード センター、バッテリー パークも高密度の場所です。

Screen Shot 2017-05-09 at 10.40.01 AM.png

基本地図のスタイルを設定する

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() 関数のレスポンスを表示する UI と、クエリが進行中であることを示すアニメーション グラフィックをウェブページに追加します。

表示できる情報には、クエリの実行時間、返されたデータ量、処理されたデータ量などがあります。

地図の <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 プロパティにアクセスできます。次のコードサンプルに示すように、ミリ秒を秒に、バイトをギガバイトに変換して表示すると、ユーザーにとって便利です。

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);
  })
}

次のようなページが表示されます。

Screen Shot 2017-05-10 at 2.32.19 PM.png

完全な例については、step8/map.html をご覧ください。

14. 考慮事項

マーカーが多すぎる

非常に大きなテーブルを処理している場合、地図上に効率的に表示するには多すぎる行がクエリから返されることがあります。WHERE 句または LIMIT ステートメントを追加して、結果を制限します。

多数のマーカーを描画すると、地図が読みにくくなることがあります。HeatmapLayer を使用して密度を表示するか、クラスタ マーカーを使用して、クラスタごとに 1 つのシンボルで多数のデータポイントの位置を示すことを検討してください。詳しくは、マーカー クラスタリングのチュートリアルをご覧ください。

クエリの最適化

BigQuery は、すべてのクエリでテーブル全体をスキャンします。BigQuery の割り当ての使用を最適化するには、クエリで必要な列のみを選択してください。

緯度と経度を文字列ではなく浮動小数点数として保存すると、クエリの実行が高速になります。

興味深い結果をエクスポートする

ここで紹介する例では、エンドユーザーが BigQuery テーブルに対して認証される必要があります。これは、すべてのユースケースに適しているわけではありません。興味深いパターンが見つかった場合は、BigQuery から結果をエクスポートし、Google マップのデータレイヤを使用して静的データセットを作成することで、より多くのユーザーと簡単に共有できます。

Google Maps Platform の利用規約にご留意ください。Google Maps Platform の料金の詳細については、オンライン ドキュメントをご覧ください。

Play With More Data!

BigQuery には、緯度と経度の列を含む一般公開データセットが多数あります。たとえば、2009 ~ 2016 年の NYC Taxi データセットUber と Lyft の NYC 乗車データGDELT データセットなどです。

15. 完了

この記事が、BigQuery テーブルに対する地理空間クエリをすばやく実行し、パターンを検出して Google マップで可視化するのに役立つことを願っています。今後ともどうぞよろしくお願いいたします。

次のステップ

Google Maps Platform または BigQuery の詳細については、以下の提案をご覧ください。

Google のサーバーレスのペタバイト規模のデータ ウェアハウス サービスの詳細については、BigQuery とはをご覧ください。

BigQuery API を使用して簡単なアプリケーションを作成する方法ガイドをご覧ください。

Google マップ上で図形を描画するためのユーザー操作を有効にする方法について詳しくは、図形描画ライブラリのデベロッパー ガイドをご覧ください。

Google マップで データを可視化するその他の方法もご覧ください。

クライアント API を使用して他の Google API にアクセスする基本的なコンセプトについては、JavaScript クライアント API のスタートガイドをご覧ください。