



この記事は、「Google Cloud Platform Advent Calendar 2018」の9日目の記事です。
BigQuery GISとFlaskとMapbox GL JSを組み合わせて可視化してみました!
今回は、BigQueryのGIS系関数の利用方法や、BigQuery Geo Viz以外の可視化方法を試していきます。BigQueryへのデータ登録については触れません。
BigQuery GISのデータ登録の流れについては、こちらの記事がすごく参考になります。今回は、国土数値情報 都市公園データをBigQuery GISにインポートしてみました。
可視化の方法については、BigQuery Geo Vizを利用すると手軽に確認はできますが、今回はMapbox GL JSと組み合わせてより自由度が高い可視化をします。
全体の構成はできるだけシンプルに。
バックエンド - BigQuery GISとFlaskを利用して空間検索可能なAPI構築
- Flask
- BigQuery GIS
フロントエンド - Mapbox GL JSを利用して地図上にデータを可視化
- Mapbox GL JS
- webpack
バックエンド
まずは、バックエンドを構築していきます。
最初に、GIS関数を試すために、下記画像の範囲内の公園のみ抽出してみます。

クエリを作成して、BigQueryのコンソールで実行してみます。フィールドについては、経緯度・公園名・施設を抽出してみます。
SELECT xcoord, ycoord, equipment, name FROM park.sample WHERE ST_WITHIN(ST_GeogFromText(wkt),ST_MakePolygon(ST_GeogFromText("LINESTRING(141.3370943069458 43.05835290494474, 141.36644840240479 43.05835290494474, 141.36644840240479 43.07496958260876, 141.3370943069458 43.07496958260876)")));
範囲内の公園のみを抽出することができました。
次に、BigQuery GISをGeoJSONで配信するAPIを構築してみます。
事前準備として、BigQueryに接続するためには認証設定が必要になります。認証用JSONファイルを作成し記述する必要があります。
認証用JSONファイルを作成するには、GCPのコンソールから「APIとサービス」→「認証情報」→「認証情報を作成」→「サービスアカウントキー」を選択します。

キー作成のために、下記のように設定します。設定後「作成」を実行するとJSONファイルがダウンロードできます。

ダウンロードされたJSONファイルを、「api.py」と同じ場所に置いておきます。今回はkey.jsonとしました。
※JSONファイルにはGCPの認証情報が入っているので、取扱には注意してください。
各パッケージインストール
pip install Flask
pip install flask-cors
pip install google-cloud-bigqueryapi.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#モジュールインポート
from flask import Flask, jsonify, abort, make_response, request
from flask_cors import CORS
from google.cloud import bigquery
#flaskのインスタンス作成
app = Flask(__name__)
#CORS対応
CORS(app)
#日本語表示対応
app.config['JSON_AS_ASCII'] = False
#JSON取得処理
@app.route('/api', methods=['GET'])
def api_get():
    #認証設定
    client = bigquery.Client.from_service_account_json('./key.json')
    #検索条件(全データから指定ポリゴンの範囲内のポイント抜き出し検索)
    query = 'SELECT xcoord, ycoord, equipment, name FROM park.sample WHERE ST_WITHIN(ST_GeogFromText(wkt),ST_MakePolygon(ST_GeogFromText("LINESTRING(141.3370943069458 43.05835290494474, 141.36644840240479 43.05835290494474, 141.36644840240479 43.07496958260876, 141.3370943069458 43.07496958260876)")));'
    #データ取得
    queryall = client.query(query).result()
    #GeoJSON作成
    result = []
    for r in queryall:
        add = {
                "type": "Feature",
                "properties": {
                    "name": r.name,
                    "equipment": r.equipment
                },
                "geometry": {
                    "type": "Point",
                    "coordinates": [r.xcoord, r.ycoord]
                }
            }
        result.append(add)
    #GeoJSON定義
    resultall = {"type": "FeatureCollection"}
    resultall["features"] =  result
    #結果表示
    print(resultall)
    #GeoJSONを出力
    return make_response(jsonify(resultall))
#エラー処理
@app.errorhandler(404)
def not_found(error):
    #エラーJSON作成
    result = {
        "error": "存在しません。",
        "result":False
        }
    #エラーJSONを出力
    return make_response(jsonify(result), 404)
#app実行
if __name__ == '__main__':
    app.run(host='0.0.0.0',port=5000,debug=True)
APIの構築が完了したので、ローカルサーバーを起動してみます。
python api.py下記アドレスにアクセスしてみます。
http://0.0.0.0:5000/api
BigQuery GISからのデータ取得と、GeoJSON形式の変換ができているのを確認できました。
フロントエンド
最後に、フロントエンドを構築していきます。
今回は、mapboxgljs-starterというMapbox GL JSを手軽に始めるビルド環境を利用します。
mapboxgljs-starterをダウンロードして「script.js」の変更と、任意のアイコン画像「./img/sample.png」を追加します。
script.js
//API取得
fetch('http://localhost:5000/api')
    .then(response => {
        //APIからJSON取得
        return response.json();
    })
    .then(result => {
        //MIERUNE MONO読み込み
        var map = new mapboxgl.Map({
            container: "map",
            style: {
                "version": 8,
                "sources": {
                    "MIERUNEMAP": {
                        "type": "raster",
                        "tiles": ['https://tile.mierune.co.jp/mierune_mono/{z}/{x}/{y}.png'],
                        "tileSize": 256
                    }
                },
                "layers": [{
                    "id": "MIERUNEMAP",
                    "type": "raster",
                    "source": "MIERUNEMAP",
                    "minzoom": 0,
                    "maxzoom": 18
                }]
            },
            center: [141.366166, 43.06483],
            zoom: 11
        });
        map.on('load', function () {
            // アイコン画像設定
            map.loadImage('./img/sample.png', function (error, res) {
                map.addImage('sample', res);
            });
            // GeoJSON設定
            map.addSource('symbol_sample', {
                type: 'geojson',
                data: result
            });
            // スタイル設定
            map.addLayer({
                "id": "symbol_sample",
                "type": "symbol",
                "source": "symbol_sample",
                "layout": {
                    "icon-image": "sample",
                    "icon-allow-overlap": true,
                    "icon-size": 1.00
                },
                "paint": {}
            });
            // アイコンクリックイベント
            map.on('click', "symbol_sample", function (e) {
                var coordinates = e.lngLat;
                // 属性設定
                var description = '<p>属性</p>' +
                                  '名称: ' + e.features[0].properties.name + '<br>' +
                                  '施設: ' + e.features[0].properties.equipment;
                while (Math.abs(e.lngLat.lng - coordinates[0]) > 180) {
                    coordinates[0] += e.lngLat.lng > coordinates[0] ? 360 : -360;
                }
                new mapboxgl.Popup()
                    .setLngLat(coordinates)
                    .setHTML(description)
                    .addTo(map);
            });
            //カーソルON,OFF
            map.on('mouseenter', "symbol_sample", function () {
                map.getCanvas().style.cursor = 'pointer';
            });
            map.on('mouseleave', "symbol_sample", function () {
                map.getCanvas().style.cursor = '';
            });
        });
        // コントロール関係表示
        map.addControl(new mapboxgl.NavigationControl());
    })
    .catch((error) =>
        console.log(error)
    );
実行環境
node v10.0.0
npm v6.4.1
パッケージインストール
npm installビルド
npm run build開発用
npm run dev開発用で確認してみます。

BigQuery GISとFlaskとMapbox GL JSを組み合わせて可視化できることを確認できました!
BigQueryが地理空間情報を扱えるようになったことで、例えば今まで分析に利用していた大量のBigQueryデータのGIS版社内ツールや、web上でビューアツールとして利用可能になりそうですね。
 
         
                
