🐕

Mapbox Directions APIを使って一周する経路を作る

2024/01/27に公開

背景

趣味でジョギング (実態はジョギングとは言えない速度…) をやるのですが、ある地点からスタートして戻ってくるコースを考える必要があります。普段は固定のコースを走るのですが、たまには別のルートを行かないと飽きてしまいます。なので、スタートを固定して、いくつか候補が欲しくなります。このような巡回ルート探しを手伝ってくれるアプリはいろいろ世の中にあり、例えば「Trail Router」などがあります。

https://trailrouter.com/

この記事は、似た計算をやってみようと思って書きました。

考えたこと

実装した計算の考え方を先に説明します。

周回ルートの設定

マラソンの大会などを見ていると色々なルートが設定されていますが、そこまで長い距離走れないため、円状のルートをとりあえず考えることにします。

もう少し真面目に考えると、高校数学で出てくるような媒介変数表示される曲線ならなんでもいいです。

スタート地点 (x, y) の周辺に走りたい距離 D の円を考え、中心から円までウォーミングアップで歩き、円上を走って一周して、あとはスタート地点までクールダウンで歩いて帰るという設定にします。円周上を D [km] 走るとき、設定すべき円の半径は r = D / 2\pi なので、これをベースに考えていけそうです。

周回ルートの計算方法

円上の点を辿っていくような巡回ルートを考えます。算数を思い出すと、円上の点 (x_i, y_i) は、 (x+r\cos\theta_i, y+r\sin\theta_i) に位置します。これを繋いでいけば、実際にジョギングする円に沿った巡回ルートが構築できそうです。

例えばN=10個ぐらいの中継点を考え、(x_0, y_0) から (x_1, y_1) までのコース、(x_1, y_1) から (x_2, y_2) までのコース、…というように、繋げば良いです。なので以下では、個別の点を計算する方法と、個別のコースを計算する方法を考えることにします (残りは繋げるだけ)。

以下の図は、東京タワー周辺に10この点を置く場合のイメージ図です。

イメージ図

座標の調整

上で (x_i, y_i) のように書いた円上の点は、Pythonなどを使うと簡単に計算できそうです。

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 に、単に D / 2\pi を入力してしまうと、とてつもなく大きい数値になってしまいます。

この問題を手抜きで対処するために、中心地点 point の周辺で、0.1\degree が何メートルに相当するのかを調べ、半径を調整するようにします。

例えば東京のある中心値で緯度方向に0.1\degree、経度方向に0.1\degree動いた場合の距離が平均で10kmだったとします。すると、10kmが0.1\degreeに相当するとき、D[km]がx\degreeに相当すると分かれば、円周上でx\degreeに相当する分の円を描くことで、円周上を歩いた距離を、だいたい、D[km]ぐらいにすることができます。もちろん、この例の場合 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ぐらいになっていまる。

Matplotlibによる可視化の例

ルートの取得

MapboxのDirectionsAPIを利用しました。今回は簡単のために、simplified したルートで geojson 形式で取得し、緯度経度情報 (Mapboxの場合は [経度, 緯度] の順) を取得し、つなぐことにしました。

https://docs.mapbox.com/api/navigation/directions/

今回はエラー処理などを真面目に書いていない簡易版の処理で緯度経度情報を抽出しました (本当はもう少し真面目に処理しないとダメです)。

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ファイルを登録すると可視化してくれますが、可視化した結果です。だいたいイメージできるでしょうか。

geojsonの可視化例

反省点

とりあえず、それっぽい巡回ルートの例が得られるようになりました 💪

一方で怪しいところもありそうです。

  • そもそも走れなさそうな地点が候補に入ると良くなさそう (円を分割する部分)
  • 部分ルートを繋げているので、不自然な行き戻りが発生してしまう
    • ここは、今回何も対策していない部分です
  • 無駄にカクカクしたルートがある
    • そこまで市街地をウネウネ走りたくなさそう

他にもあると思いますが、このあたりを対処すると、より使いやすくなりそうです。

まとめ

ある地点からスタートして戻ってくるコースを考えるジョガーをサポートする方法(すごくシンプルなもの)を考え、簡単な実装の動作確認ができました。

実装したコードはこちらにありますが、Mapbox の APIキーが別途必要になります。また API のアクセス上限にも気をつける必要があります (とはいえ、Mapbox さんのAPI上限は無料版でもだいぶ高めに設定されています)。

https://github.com/cocomoff/CircularRoutes

Discussion