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 でペタバイト規模のロケーション データセットを数秒でクエリする方法
  • Google Maps Platform を使用して Google マップをウェブページに追加し、ユーザーがシェイプを描画できるようにする方法
  • 以下のサンプル画像のように、大規模なデータセットに対するクエリを Google マップで可視化する方法。この図は、エンパイア ステート ビル周辺で開始された移動からの 2016 年のタクシー降車場所の密度を示しています。

スクリーンショット 2017-05-09 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 と 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 では、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 ページ マーケットプレイスに移動し、青色の [有効にする] ボタンをクリックします。

3. BigQuery でロケーション データのクエリを行う

BigQuery に緯度と経度の値として保存されている位置情報を照会する方法は 3 つあります。

  • 長方形クエリ: 関心のあるすべての地域を緯度と経度の最小値と最大値の範囲内で選択するクエリとして指定します。
  • 半径クエリ: ハベルネの方程式と数学関数を使用して、ある点の周囲で円を計算し、地球の形状をモデル化して、関心のある地域を指定します。
  • ポリゴンクエリ: カスタム シェイプを指定し、各行の緯度と経度がシェイプ内に収まっているかどうかをテストするために必要なポイントインポリゴン ロジックを表現します。

まず、Google Cloud Platform Console の BigQuery セクションにある クエリエディタを使用し、ニューヨーク市のタクシーデータに対して次のクエリを実行します。

標準 SQL とレガシー SQL

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

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

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

長方形クエリ

長方形のクエリは、BigQuery で簡単に作成できます。WHERE 句を追加するだけです。は、返される結果を、緯度と経度の最小値と最大値を範囲とする結果に制限します。

以下の例を BigQuery コンソールでお試しください。このクエリは、マンハッタンの繁華街と低地の長方形のエリアを出発点とする乗車データの平均統計をクエリします。2 つの場所で試すことができます。JFK 空港で開始された配車に対してクエリを実行するために、2 番目の WHERE 句のコメント化を解除します。

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 つの場所での平均乗車距離、運賃、乗車料金に大きな差があることを示しています。

マンハッタン

avg_tip

avg_fare

avg_distance(平均距離)

avg_tip_pc

avg_fare_mile

2.52

12.03

9.97

22.39

5.97

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

これは、半径 0.1 km の 40.73943, -73.99585 を中心とする円クエリに対する BigQuery SQL ステートメントの例です。

1 度で表されます。

これは、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 Formula の 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

クエリの結果は以下のとおりです。平均的なチップ、運賃、移動距離、チップに比例した運賃、1 マイルあたりの平均走行距離に大きな差があることがわかります。

エンパイア ステート ビル:

avg_tip

avg_fare

avg_distance(平均距離)

avg_tip_pc

avg_fare_mile

1.17

2011 年 8 月

4,528

10.53

6.42

ブロンクス

avg_tip

avg_fare

avg_distance(平均距離)

avg_tip_pc

avg_fare_mile

0.52

1763

4.75

4.74

10.9

ポリゴンクエリ

SQL では、長方形と円以外の任意のシェイプを使用したクエリはサポートされていません。BigQuery にはネイティブ ジオメトリ データ型や空間インデックスがないため、ポリゴンの形状を使用してクエリを実行するには、簡単な SQL クエリとは異なる方法を使用する必要があります。1 つの方法は、JavaScript でジオメトリ関数を定義し、BigQuery でユーザー定義関数(UDF)として実行することです。

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

ポイント イン ポリゴン アルゴリズム

ある地点が JavaScript のポリゴンに含まれるかどうかは、さまざまな方法で計算できます。これは、レイトレーシング アルゴリズムを使用して、無限の線が形状の境界をまたがる回数を数えることで、あるポイントがポリゴンの内部か外側かを判断する、よく知られた実装の C からのポートです。わずか数行のコードで完了します。

function pointInPoly(nvert, vertx, verty, testx, testy){
  var i, j, c = 0;
  for (i = 0, j = nvert-1; i < nvert; j = i++) {
    if ( ((verty[i]>testy) != (verty[j]>testy)) &&
                (testx < (vertx[j]-vertx[i]) * (testy-verty[i]) / (verty[j]-verty[i]) + vertx[i]) )
      c = !c;
  }
  return c;
}

JavaScript への移行

このアルゴリズムの JavaScript バージョンは次のようになります。

/* This function includes a port of C code to calculate point in polygon
* see http://www.ecse.rpi.edu/Homepages/wrf/Research/Short_Notes/pnpoly.html for license
*/

function pointInPoly(polygon, point){
    // Convert a JSON poly into two arrays and a vertex count.
    let vertx = [],
        verty = [],
        nvert = 0,
        testx = point[0],
        testy = point[1];
    for (let coord of polygon){
      vertx[nvert] = coord[0];
      verty[nvert] = coord[1];
      nvert ++;
    }

        
    // The rest of this function is the ported implementation.
    for (let i = 0, let j = nvert - 1; i < nvert; j = i++) {
      if ( ((verty[i] > testy) != (verty[j] > testy)) &&
         (testx < (vertx[j] - vertx[i]) * (testy - verty[i]) / (verty[j] - verty[i]) + vertx[i]) )
        c = !c;
    }
    return c;
}

BigQuery で標準 SQL を使用する場合、UDF アプローチは 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 Console で、Marketplace に移動します。
  2. マーケットプレイスで「Maps JavaScript API'
  3. 検索結果で Maps JavaScript API のタイルをクリックします。
  4. [有効にする] ボタンをクリックします。

API キーを生成

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

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

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

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

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

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

ローカル ウェブサーバーの設定

ご自身のウェブサーバーを使用してもかまいませんが、この Codelab は Chrome ウェブサーバーで適切に機能するように設計されています。このアプリをまだインストールしていない場合は、Chrome ウェブストアからインストールできます。

アプリのインストールが完了したら、Chrome で次の手順で開くことができます。

  1. Chrome を開く
  2. 上部のアドレスバーに「chrome://apps」と入力します。
  3. Enter キーを押してください
  4. 表示されたウィンドウで [ウェブサーバー] アイコンをクリックします。アプリを右クリックして、通常のタブまたは固定されたタブ、全画面表示、または新しいウィンドウで開くこともできます。a3ed00e79b8bfee7.png次のダイアログが表示され、ローカル ウェブサーバーを構成できます。103b6151c3f60c948.png
  5. [フォルダを選択] をクリックして、Codelab サンプル ファイルをダウンロードした場所を選択します。
  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. リポジトリのローカルコピー内の work フォルダに index.html をコピーします。
  2. img/ フォルダをリポジトリのローカルコピー内の work/ フォルダにコピーします
  3. テキスト エディタまたは IDE で仕事用または 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 では、ブラウザベースのアプリケーションを開発するため、JavaScript の Google API クライアント ライブラリを介して BigQuery API を使用します。

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

JavaScript 用の Google Client API を追加する

BigQuery に対してクエリを実行するには、JavaScript 用 Google Client API を使用します。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. このトークンのスコープに BigQuery API を追加する必要があります。[Google API のスコープ] で [スコープを追加] ボタンをクリックします。3. リストで、../auth/bigquery スコープの Big Query API エントリの横にあるチェックボックスをオンにします。4. [Add] をクリックします。5. [アプリケーション名] に名前を入力します。6. [保存] をクリックして設定を保存します。7. 次に、OAuth クライアント ID を作成します。それには、[認証情報を作成] をクリックします。

2018-0514-47

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

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

let clientId = 'YOUR_CLIENT_ID';

7. 承認と初期化

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

ウェブページに Google Client API を読み込んだ後、次の手順を行う必要があります。

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

完成した HTML ページがどのように表示されるかについては、step3/map.html をご覧ください。

ユーザーを認証する

アプリケーションのエンドユーザーは、ユーザーの代わりにアプリケーションが BigQuery 内のデータにアクセスすることを承認する必要があります。Google Client API for JavaScript は、OAuth ロジックを処理するために使用します。

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

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

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

また、ユーザーが正常に承認された場合に BigQuery API を読み込むための関数 loadApi を追加します。

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

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

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 呼び出しを使用します。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 を参照する個別の変数も必要です。

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

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

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

ここで、SQL を生成し、前のステップで作成した sendQuery 関数を使用してクエリを BigQuery に送信する 2 つの関数をコードに追加します。

最初の関数は 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 では、ウェブページに転送されるデータの量を適切なサイズにするために、レスポンスを 10, 000 行に制限するステートメントを追加して rectangleSQL() 関数を変更します。以下の例では、すべてのクエリ関数が同じ値を使用できるように、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 数学関数を使用すると、地表上の円圏を近似するハバーシンの公式を使用して SQL クエリを作成できます。

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

このステップのコード例は、step6/map.html としてコード リポジトリに含まれています。

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

index.html に 2 つの新しい空の関数 circleQuery()haversineSQL() を追加します。

次に、中心と半径を 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;
}

お試しください。

上のコードを追加してから、「Circle」ツールで地図上のエリアを選択してください。次のような結果が表示されます。

16164b7cc7a3.png

12. 任意のシェイプのクエリ

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

BigQuery の強力な機能の一つに、ユーザー定義関数(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 データからの検索をクエリした結果です。選択されたデータはヒートマップとして描画されます。

スクリーンショット 2017-05-09 午前 10:00.48

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 年のエンパイア ステート ビル周辺における、すべてのピックアップからの離脱の分布を示しています。特にタイムズ スクエア周辺と 23 丁目と 14 番街の間にある 5 番街沿いは、ミッドタウン デスティネーションが集中している場所(赤いブロブ)や、ラ ガーディア空港と JFK 空港、ワールド トレード センター、バッテリー パークがあります。

スクリーンショット 2017-05-09 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;
}

アニメーション グラフィックをページに追加できますが、必須になるまで非表示にします。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);
  })
}

ページは次のようになります。

スクリーンショット 2017-05-10 2.32.19 PM.png

step8/map.html で完全な例をご覧ください。

14. 留意点

マーカーの数が多すぎます

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

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

クエリの最適化

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

文字列ではなく緯度と経度を浮動小数点数として保存すると、クエリを高速化できます。

面白い結果をエクスポート

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

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

豊富なデータを楽しもう

BigQuery には、緯度と経度の列を含む一般公開データセットが数多くあります。たとえば、2009 年から 2016 年までの NYC Taxi データセットUber と Lyft のニューヨーク市の移動データGDELT データセットなどがあります。

15. 完了

このクエリが BigQuery テーブルに対する地理クエリをすばやく実行できるようになれば、パターンを見つけて Google マップで可視化できるようになります。今後ともどうぞよろしくお願いいたします。

次のステップ

Google Maps Platform または BigQuery についてさらに詳しく知りたい場合は、以下の方法をご検討ください。

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

BigQuery API を使用して簡単なアプリケーションを作成する方法を確認する。

ユーザーが Google マップで図形を描画できるようにする方法について詳しくは、図形描画ライブラリのデベロッパー ガイドをご覧ください。

Google マップでデータを視覚化するその他の方法を確認する。

JavaScript クライアント API のスタートガイドを参照して、Client API を使用して他の Google API にアクセスする基本的な概念を理解してください。