Mapbox Directions APIを使って一周する経路を作る
背景
趣味でジョギング (実態はジョギングとは言えない速度…) をやるのですが、ある地点からスタートして戻ってくるコースを考える必要があります。普段は固定のコースを走るのですが、たまには別のルートを行かないと飽きてしまいます。なので、スタートを固定して、いくつか候補が欲しくなります。このような巡回ルート探しを手伝ってくれるアプリはいろいろ世の中にあり、例えば「Trail Router」などがあります。
この記事は、似た計算をやってみようと思って書きました。
考えたこと
実装した計算の考え方を先に説明します。
周回ルートの設定
マラソンの大会などを見ていると色々なルートが設定されていますが、そこまで長い距離走れないため、円状のルートをとりあえず考えることにします。
もう少し真面目に考えると、高校数学で出てくるような媒介変数表示される曲線ならなんでもいいです。
スタート地点
周回ルートの計算方法
円上の点を辿っていくような巡回ルートを考えます。算数を思い出すと、円上の点
例えば
以下の図は、東京タワー周辺に10この点を置く場合のイメージ図です。
座標の調整
上で
import numpy as np
from collections import namedtuple
# 緯度経度 (サービスによって (lat, lon), (lon, lat) だったりするので、名前を付けて区別する)
LatLon = namedtuple("LatLon", ["lat", "lon"])
# 円の中心
point = LatLon(XXXX, YYYY)
radius = TODO
N = 10
# 中心 `point`、半径 `radius` の円に `N` 個の点を置く
ret_points = []
theta = np.linspace(0, 2 * np.pi, num=N, endpoint=False)
seqLat = point.lat + radius * np.cos(theta)
seqLon = point.lon + radius * np.sin(theta)
for j in range(len(seqLat)):
ret_points.append(LatLon(seqLat[j], seqLon[j]))
ここで問題になるのは、radius
の部分です。というのも地点は、Google Mapなどで調べると分かる通り、10進法の度 (日本語が正しくないかもしれません…) で入力されます。上記の raidus
に、単に
この問題を手抜きで対処するために、中心地点 point
の周辺で、
例えば東京のある中心値で緯度方向に
、経度方向に 0.1\degree 動いた場合の距離が平均で10kmだったとします。すると、10kmが 0.1\degree に相当するとき、 0.1\degree [km]が D に相当すると分かれば、円周上で x\degree に相当する分の円を描くことで、円周上を歩いた距離を、だいたい、 x\degree [km]ぐらいにすることができます。もちろん、この例の場合 D なので、 x=D/10*0.1 の計算はこの D / 2\pi の分だけ補正すれば良いと考えることができます。 x
上の説明した処理を実装したものです。ただし LatLon
が2点入力された場合の距離は Haversineの式で求めているので、ズレが含まれますが、ここでは無視しました。
# 中心から0.1動いたときの距離を測定する (緯度・経度で平均)
dx_point = LatLon(point.lat + 0.1, point.lon)
dy_point = LatLon(point.lat, point.lon + 0.1)
dx = haversine(point, dx_point)
dy = haversine(point, dy_point)
# 例の 10[km] の部分
avg = (dx + dy) / 2
# 距離 `dist` を達成するための、緯度経度の世界への換算
desired_value = dist / avg * 0.1
# 描く半径は換算した量 (度) が 2 pi r となる `r` 部分
radius = desired_value / (2 * np.pi)
本気で計算しようと思うともう少し調整がいりそうな気がします。ここまでの処理を合わせて、matplotlibで可視化したものが次の図です。円周上の距離が、概ね設定した3kmぐらいになっていまる。
ルートの取得
MapboxのDirectionsAPIを利用しました。今回は簡単のために、simplified
したルートで geojson
形式で取得し、緯度経度情報 (Mapboxの場合は [経度, 緯度] の順) を取得し、つなぐことにしました。
今回はエラー処理などを真面目に書いていない簡易版の処理で緯度経度情報を抽出しました (本当はもう少し真面目に処理しないとダメです)。
import requests
from API_KEY import MAPBOX_API_KEY
URL_BASE = "https://api.mapbox.com/directions/v5/mapbox"
def search(point1: LatLon, point2: LatLon, sleep_time: int = 2) -> list[LatLon]:
"""
Mapbox APIに投げて簡易緯度経度列を取得する
"""
time.sleep(sleep_time)
url = f"{URL_BASE}/walking/{point1.lon},{point1.lat};{point2.lon},{point2.lat}?access_token={MAPBOX_API_KEY}&geometries=geojson"
res = requests.get(url)
if res.status_code != 200:
return None
# route
ret = []
data = res.json()
for elem in data["routes"][0]["geometry"]["coordinates"]:
# メモ: polylineの場合はここでdecodeが必要
ret.append(LatLon(elem[1], elem[0]))
return ret
結果
これまでの処理を全部くっつけて出力された経路を可視化すると、次のような巡回ルート候補が得られました。
Matplotlibによる可視化
中心点を打つのを忘れてしまいましたが、東京タワー周辺に10個ほど点を考え、3キロぐらい歩くルートを繋げた結果の巡回ルートです。
geojsonの可視化
githubにgeojsonファイルを登録すると可視化してくれますが、可視化した結果です。だいたいイメージできるでしょうか。
反省点
とりあえず、それっぽい巡回ルートの例が得られるようになりました 💪
一方で怪しいところもありそうです。
- そもそも走れなさそうな地点が候補に入ると良くなさそう (円を分割する部分)
- 部分ルートを繋げているので、不自然な行き戻りが発生してしまう
- ここは、今回何も対策していない部分です
- 無駄にカクカクしたルートがある
- そこまで市街地をウネウネ走りたくなさそう
他にもあると思いますが、このあたりを対処すると、より使いやすくなりそうです。
まとめ
ある地点からスタートして戻ってくるコースを考えるジョガーをサポートする方法(すごくシンプルなもの)を考え、簡単な実装の動作確認ができました。
実装したコードはこちらにありますが、Mapbox の APIキーが別途必要になります。また API のアクセス上限にも気をつける必要があります (とはいえ、Mapbox さんのAPI上限は無料版でもだいぶ高めに設定されています)。
Discussion