Google Maps Static APIを使ってツーリングで巡ったルートを地図表示する

はじめに

前回、StravaのAPIを使ってサイクリングの情報を持ってくる方法を記事にしました。

muscle-keisuke.hatenablog.com

Stravaにはどこを走ったかも記録されていて、位置情報が以下のように地図に表示されています。

ちなみにこれは去年今年に北海道で年越しツーリングして大晦日に吹雪に遭ったときのアクティビティです。

遠軽町から湧別町を時速4kmくらいで移動しました。

そのアクティビティをAPIで取得すると位置情報は以下のフォーマットで取得されます。

なんのこっちゃって感じですね。

Strava APIから取得した位置情報もこうなっては人間が読めないので地図画像として出力する必要があります。

今回はGoogle Map Static APIを使ってこの呪文みたいな文字列からStravaに表示されるような地図を生成します。

Google Maps Static APIとは

Google Mapの一部を画像として切り出して返してくれるAPIです。

あらゆる情報をパラメータとして指定することで追加の情報を付加した地図も返せます。

developers.google.com

今回は位置情報を付加した地図を返します。

APIキー取得まで

プロジェクトの作成

developers.google.com

まずは前提として、Googleアカウントが必要です。持っていない場合はアカウントを作成してください。

Googleにログインした後は、プロジェクトを作成します。 Google Cloud Platformにアクセスします。

console.cloud.google.com

右上の「プロジェクトを作成する」から新規のプロジェクトを作成します。

自分がわかるような名前を付けます。

作成後は作成したプロジェクトの画面に遷移します。

支払情報の登録

APIの使用は従量課金制です。APIを使用する前にプロジェクトに支払い方法を登録する必要があります。

Google Maps Static APIの料金

developers.google.com

APIのリクエスト1回毎に0.02USDかかる計算です。

しかし、

developers.google.com

なお、すべての Google Maps Platform ユーザーは、課金が有効になっているアカウントで、毎月 200 ドルのクレジットを受け取ります。

とあるので、2022年6月16日現在はGoogle Maps Static APIのみの利用に限れば、月に10000回までは無料でAPIを叩けます。

それ以上使う場合はプロジェクトに登録された支払い方法によって、従量課金が発生します。

プロジェクトに支払情報を登録する

Google Cloud Platformトップに戻ってサイドバーの「お支払い」を押す。

「請求先アカウントをリンク」を押す。

「請求先アカウントを作成する」を押す。

ここから請求先を登録していきます。まずは必要事項を入力して利用規約を読んで同意します。

プロジェクトのニーズは自分の場合は「個人的なプロジェクト」にしました。

連絡先を確認します。SMSを受信できる電話番号を入力します。

コードを送信すると、入力した電話番号宛にSMSが届きます。

SMSの中に記載されているコードを入力します。

次にお支払い情報の入力です。有効なクレジットカードの情報と住所を入力します。

これで登録は完了です。再度、お支払いのページを見ると、紐付いている請求先アカウントの情報が表示されます。

APIを有効化する

使用するAPIを選びます。 左のサイドバーから「APIとサービス」 -> 「有効なAPIとサービス」を選びます。

APIとサービス」画面が開くので上の「APIとサービスの有効化」を押します。

APIライブラリ」で有効にしたいAPIを検索します。

Google Maps Static APIを探します。 staticなどで検索するとヒットすると思います。

該当するAPIを選択します。

「有効にする」を押すと、有効になります。

そのまま有効なAPIが表示される画面に遷移します。

次にAPIキーを発行します。 サイドバーから「認証情報」に遷移して上の「認証情報を作成」からAPIキーを選択します。

すると、APIキーが発行されて表示されます。

Google Maps Static APIを使ってみる

APIキーを利用して画像を表示させてみます。 APIのエンドポイントは以下のようなフォーマットです。

https://maps.googleapis.com/maps/api/staticmap?center=[LATITUDE],[LONGTITUDE]&zoom=[ZOOM_LEVEL]&size=[WIDTH]x[HEIGHT]&key=[API_KEY]

クエリパラメータのcenterにある[LATITUDE],[LONGTITUDE]は表示する地図の中心の緯度経度を入力します。

zoomはズームレベルといって地図の縮尺です。 詳細は

muscle-keisuke.hatenablog.com

の「正方形タイルによる地図表現」セクションに載せています。

sizeの[WIDTH]x[HEIGHT]は表示する地図画像のサイズです。

keyの[API_KEY]は先程発行したAPIキーを入力します。

例えば、小田原駅の地図を表示してみます。 Google Mapで小田原駅を真ん中に寄せたときのURLが

https://www.google.co.jp/maps/@35.2562828,139.1554468,17z?hl=ja

です。URL内に緯度経度とズームレベルがあるのでこれを使います。

画像サイズは600x600にしてみます。すると叩くAPI

https://maps.googleapis.com/maps/api/staticmap?center=35.2564493,139.1532045&zoom=17&size=600x600&key=[API_KEY]

です。APIキーは伏せているので各自のキーに置き換えてください。

GETリクエストなのでブラウザにURL貼って遷移すると、地図画像が表示されました。

Google Mapで見た地図とまんま一緒です。

これでAPIが使えることを確認できました。

署名の導入

今のままでもAPIは使えますが、GoogleAPIを叩くときにリクエストを署名することを推奨しています。

developers.google.com

We strongly recommend that you use both an API key and digital signature, regardless of your usage.

リクエストにはAPIキーが含まれています。このAPIキーが何かしらの原因で抜き取られた場合は第三者APIを叩けてしまいます。 本人だけが持っている情報でリクエストを署名することで第三者が抜き取ったAPIキーだけではAPI叩けないようにして安全性を高めます。

こちらにGoogle Maps Static APIを使うときに署名する理由などが書いてあります。

Google Developers Japan: Google Maps Platform ベストプラクティス:Static Map API と Street View API を使う際の、より安全な API キーの設定方法

ちなみに前回説明した

muscle-keisuke.hatenablog.com

Strava APIはOAuth2.0による認証です。

仕組みなど定義はこちらがわかりやすかったです。

qiita.com

この中の「認可コードフロー」と「リフレッシュトークンフロー」をStrava APIは採用しています。

GoogleAPIGoogle DriveAPIなど個人のデータにアクセスするAPIにはOAuth2.0を採用して、 Google Maps Static APIのような取得する地図データは個人情報にあたらないのでAPIキーによる認証を採用しているようです。

cloud.google.com

署名を試す

URLやパラメータの署名のやり方です。まず、Google Maps Platformに行くと、先程発行したAPIキーともう一つ、URL 署名シークレットというものがあります。

これが署名には必要です。発行されていない場合はボタンを押して発行してください。

発行後、「URL に署名」の欄から試しにURLを署名することができます。先程の小田原駅を表示するURLを署名してみます。

https://maps.googleapis.com/maps/api/staticmap?center=35.2564493,139.1532045&zoom=17&size=600x600&key=[API_KEY]

APIキーも含めてフォームに貼り付けます。

すると、署名済みのURLが返ってきます。

署名前のURLとの違いは最後のクエリパラメータにsignatureが追加されていることです。

https://maps.googleapis.com/maps/api/staticmap?...&key=[API_KEY]&signature=[SIGNATURE]

このsignatureが署名済みであることを示しています。

このsignatureが付加された状態でAPIを叩くときはURL署名シークレットとリクエストURLで署名をしてみて、[SIGNATURE]と一致するかどうかを検証します。

一致した場合正しい署名(=第三者によるリクエストではない)なので通常通りレスポンスとして画像が返ってきます。一致しない場合はエラーを返します。

ちなみに署名に使われる関数は一方向関数で[SIGNATURE]からURL署名シークレットを算出することは難しいです。

署名済みのURLをブラウザに入力してリクエストを送ると、先程と全く同じ画像が出力されるはずです。

署名を実装する

署名を試すことはできましたが、実際APIを使うのはコード中なので、未署名のURLから署名済みのURLを作る実装が必要です。

署名は

  • 署名シークレットをURLセーフのbase64*1から通常のbase64へ変換する
  • 通常のbase64になった署名シークレットをデコードする
  • デコードした署名シークレットをキーとしてリクエストのURLをHMAC-SHA1*2でハッシュ化する
  • ハッシュをURLセーフのbase64に変換する
  • 署名済みとしてsignatureパラメータに↑のbase64のハッシュを渡す

といった流れで行われます。

公式が様々な言語のサンプルコードを出しているのでこれを踏まえて実装していきます。

developers.google.com

現在、開発しているアプリがTypescript + NodeJSなのでNodeJSのサンプルコードからdeprecatedなも部分を改変してTypescriptで書きました。

import crypto from "crypto";
import url from "url";
...
function removeWebSafe(safeEncodedString: string) {
  return safeEncodedString.replace(/-/g, "+").replace(/_/g, "/");
}
function makeWebSafe(encodedString: string) {
  return encodedString.replace(/\+/g, "-").replace(/\//g, "_");
}
function decodeBase64Hash(code: string) {
  return Buffer.from(code, "base64");
}

function encodeBase64Hash(key: Buffer, data: string) {
  return crypto.createHmac("sha1", key).update(data).digest("base64");
}
function async sign(apiPath: string, signatureSecret: string) {
  const uri = new URL(apiPath);
  const safeSecret = this.decodeBase64Hash(
    this.removeWebSafe(signatureSecret)
  );
  const hashedSignature = this.makeWebSafe(
    this.encodeBase64Hash(safeSecret, uri.pathname + uri.search)
  );
  return url.format(uri) + "&signature=" + hashedSignature;
}

API呼び出しの制限

署名によるリクエストを設定したので未署名のリクエストはできないよう設定します。

Google Maps Platformから「割り当て」を選びます。

未署名のURLによるリクエストの設定を展開して、各API呼び出し回数の上限をすべて0にします。

これで未署名のURLでリクエストを送ると

このような画像が返ってきました。制限できているっぽいです。

位置情報を付与した地図を取得する

やっとメインコンテンツです。

地図自体の表示はできているので、次はその地図に位置情報によるルート表示をしてみます。

これまでの地図表示のAPIにpathというパラメータを足すだけです。

pathのフォーマットは以下のようになっています。

path=weight:[WEIGHT]|color:[COLOR]|[LAT1],[LON1]|[LAT2],[LON2]|...

[WEIGHT]は表示するルートの線の太さです。 [COLOR]は線の色です。カラーコードや名称で指定できます。 [LAT1],[LON1]は位置情報です。|で繋げて複数の緯度経度を指定するとその位置を通る線を引っ張ります。

オプションは他にもあります。他のオプションについては公式Docを参照してください。

developers.google.com

他のオプションも|で繋いでいきます。

ただし、|はURLにおいて無効な文字なのでURLエンコードする必要があります。URLエンコードすると、| -> %7Cに変わります。

これを元に小田原駅(35.2564493,139.1532045)から国府津駅(35.2811428,139.2140066)までの線を引く地図は以下のURLで取得できます。

https://maps.googleapis.com/maps/api/staticmap?size=600x600&path=weight:5%7Ccolor:blue%7C35.2564493,139.1532045%7C35.2811428,139.2140066&key=[API_KEY]&signature=[SIGNATURE]

APIを叩くと以下のような画像を取得できます。

小田原駅から国府津駅まで線を引けているっぽいですね。

これを応用してStrava APIから取得した位置情報を地図に付加します。

冒頭で述べたとおり、Strava APIから返ってくる位置情報は緯度経度で表示されていません。

この文字列は緯度経度をPolyline Encoding と呼ばれるエンコーディングを行ったものです。

ちなみにGoogle Maps Static APIはこのPolyline Encoding による位置情報もそのままリクエストに渡せることができます。

developers.google.com

pathパラメータにencを足し、そこにPolyline Encoding による位置情報を与えます。 Polyline EncodingにはURLにとって無効な文字も含まれているため、URLエンコーディングする必要があります。

Polyline Encodingに含まれる文字はASCIIコードの64(@) ~ 126(~)でその中でURLにとって無効な文字は

www.ipentec.com

に載っています。

ちなみにPolyline EncodingにはURLにとって予約文字となる文字も含まれていますが、その文字はURLエンコーディングしないように注意してください。

これを踏まえて遠軽町から湧別町までのアクティビティを地図に表示するURLは

https://maps.googleapis.com/maps/api/staticmap?size=400x400&path=weight:5%7Ccolor:blue%7Cenc:wuakGe%60akZ%5B%60@SKe@FqBx@uB%60AoAt@YKf@A~@i@XDbAbGQPGh@sGdIyDfFuE%60GkBtBiF~GqDhEwBjAiS%7CEcC~@_Ax@aGdH%7DDhEeBfA%7DCp@aMj@mCSiDiA%7Bp@aXaA_AgEyGqAuAaz@cc@_C_B%7BF%7BFoBkAiCy@oDy@cCWgE@aCZsC~@yA%60AcBrBwCdF%7B@dAsAhA%7BAp@uAZuC@kBWeDy@aAa@qMsDe%7D@cX%7BDa@eCB_BPyCr@_A%5EgRtJqCbAaDb@%7DBAcHu@oCIOUI?OTcFX_E%7C@uBz@_NdHmCzB%7BCbEqMpTkDhEkE%7CDwVzRoEjE%7DAxBsC%7CFyBdG_ClIq@~AqBvC%7BArAgCrAs%5EbKgBz@cA%7C@sAnBcAnCk@fCSWMi@aCeAwGuDo%5EaReKwFwR%7BJ%7BMeHaAq@%7DAm@cC%7BAeDuA%5DDMj@DFDw@Wu@eDwAwFcDkMqGc@GsAFcA%60@gGvAyBl@yC~@%7D@b@c@t@aAhA%5Br@kDfEuAp@mH%7CBkAEg@m@mB%7BFS%5D_Ao@yAo@eBgAcIyDy@o@kDmB_@IOSwAk@sBsA%7BCqA%7DQ%7DJiBu@y@m@qDkB%7BCsAu@m@mHoDyCiBkKkFaE_C_Bs@wIoEcBiAcCcAsC?o@PsHd@cEf@iIf@yEf@oKVoFWqDa@oFiAoIgCaGsAwAUeCAgDl@sBdAgEnCqFfCyC%60A%7BEx@wENoD?eCOoFy@eHcCoE_CkDgCeEiEiCgDyVob@qCeF_C_EUQ_H%7DLmKqQaC%7DCyAoA_FiC%5BG%7BByAsAi@%7DEsCkHoDiH_EaKcFgAu@uOaIwEkCmLkGiOyHGM%7D@%5Dq@g@mAe@gGiDgO_I%7BBaAeHsD%7DBuAi@QaRaKq@Wu@i@%7B@WaCyAcAYyAcAaAYYYcA_@c@_@s@Wa@i@iA_@iC%7BA%7BAk@%7B@o@iEmBmCcBuB%7B@aDqB%5BAWTHC?YUYiA_@yBoAeAw@i@MWYgCoAoAg@m@i@aCiAc@_@w@QsC%7DAsA%7D@iBu@cGiDyAg@%5BYgD_B%5BY_@KwByAwCuAUWgFmC_DwAgAm@_Am@yAm@MSkDeBQUm@OmNoHmBkAy@WaAq@kAe@%7BDsBgAu@%7BBcAsDwBiD%7DAEMaAk@eCkAwA%7D@%5BGyBuAuKiFe@_@aIiEa@IkAk@k@g@u@WmB%7B@w@o@_By@kA_@g@e@i@Sg@a@w@WgB_A%5D%5By@%5DSYgA_@aBcA_AY%5CBFc@C_@Ji@Tu@NkAfAwET@zBpAb@@BFOT?L&key=[API_KEY]&signature=[SIGNATURE]

です。

これでリクエストすると

Stravaのスクショ通りの画像を取得できました。

おまけ

Polyline Encoding Algorithmについて

緯度経度からPolyline Encodingする具体的なフローについてです。

公式がPolyline Encodingの具体的な手順について載せています。

developers.google.com

小田原駅(35.2564493,139.1532045)から国府津駅(35.2811428,139.2140066)までの線をPolyline Encodingする例を考えます。

小田原駅から考えます。

まずは105して丸めます。

3525644,13915320

次に2の補数の2進数に変換します(32bit)。正の数なので変わりないです。

緯度: 00000000001101011100110000001100
経度: 00000000110101000101010010111000

1ビットの左へのシフト演算によって値を2倍する。

緯度: 00000000011010111001100000011000
経度: 00000001101010001010100101110000

この時点で負の数の場合はビットを反転させますが、今回は正の数なので操作はありません。

緯度: 00000000011010111001100000011000
経度: 00000001101010001010100101110000

変換した2進数を右から5ビットずつに分割します。溢れた分は消します。

緯度: 00000 00110 10111 00110 00000 11000
経度: 00000 11010 10001 01010 01011 10000

分割した5ビットの1単位をチャンクと呼びます。チャンクを逆順にします。

緯度: 11000 00000 00110 10111 00110 00000
経度: 10000 01011 01010 10001 11010 00000

左のチャンクから順番に0x20の論理和演算をします。 最後のチャンク(=次以降のチャンクが0)の場合はチャンク先頭に0を付加します。

緯度: 111000 100000 100110 110111 000110 000000
経度: 110000 101011 101010 110001 011010 000000

それぞれのチャンクを10進数に戻します。

緯度: 56 32 38 55 6
経度: 48 43 42 49 26

それぞれの値に63を足します。

緯度: 119 95 101 118 69
経度: 111 106 105 112 89

それぞれの値をASCIIコード*3として文字に変換します。

すると緯度経度それぞれ

緯度: w_evE
経度: ojipY

で表されます。

よって、小田原駅の緯度経度をPolyline Encodingすると w_evEojipYです。

実際にAPIを叩いてみると

https://maps.googleapis.com/maps/api/staticmap?&size=600x600&path=weight:5%7Ccolor:red%7Cenc:w_evEojipY&key=[API_KEY]&signature=[SIGNATURE]

小田原駅周辺の画像を取れました。

次に国府津駅のPolyline Encodingをしていきます。

入力する緯度経度が複数の場合2つ目以降は一つ前の緯度経度との差分を考えます。 つまり、 (35.2811428,139.2140066) - (35.2564493,139.1532045) = (0.00246935, 0.00608021) です。

105して丸めます。

2469,6080

次に2の補数の2進数に変換します(32bit)。

緯度: 00000000000000000000100110100101
経度: 00000000000000000001011111000000

1ビットの左へのシフト演算によって値を2倍します。

緯度: 00000000000000000001001101001010
経度: 00000000000000000010111110000000

正の数なのでビット反転はしません。

緯度: 00000000000000000001001101001010
経度: 00000000000000000010111110000000

変換した2進数を右から5ビットずつに分割する。溢れた分は消します。

緯度: 00000 00000 00000 00100 11010 01010
経度: 00000 00000 00000 01011 11100 00000

チャンクを逆順にします。

緯度: 01010 11010 00100 00000 00000 00000
経度: 00000 11100 01011 00000 00000 00000

左のチャンクから順番に0x20の論理和演算をします。 最後のチャンク(=次以降のチャンクが0)の場合はチャンク先頭に0を付加します。

緯度: 101010 111010 000100 00000 00000 00000
経度: 100000 111100 001011 00000 00000 00000

それぞれのチャンクを10進数に戻します。

緯度: 42 58 4 経度: 32 60 11

それぞれの値に63を足します。

緯度: 105 121 67
経度: 95 123 74

ASCIIコードに変換すると、 iyC_{J です。

URLに載せるときはURLエンコーディングして iyC_%7BJ になります。

小田原駅のPolyline Encodingと合わせると、

https://maps.googleapis.com/maps/api/staticmap?&size=600x600&path=weight:5%7Ccolor:red%7Cenc:w_evEojipYiyC_%7BJ&key=[API_KEY]&signature=[SIGNATURE]

APIを叩くと小田原駅から国府津駅の線が出ました。

*1:base64の違いは https://qiita.com/kunihiros/items/2722d690b1525813c45e を参考にしました

*2:HMAC-SHA1についてはhttps://e-words.jp/w/HMAC.htmlを参考にしました

*3:https://www.k-cube.co.jp/wakaba/server/ascii_code.htmlを参考にしました。