この記事は フラー株式会社 Advent Calendar 2024 の14日目の記事です。
13日目は @maro114510さんで、「そこはかとなく仕事を早くする環境づくり -2024-」でした。
はじめに
こんにちは、フラー株式会社でエンジニア組織のマネージャーをやりつつiOSのテックリードをやっております@nirazoです。
好きなお菓子はグミです。
私夜のドライブが好きでして、ドライブしながらそれまで通ったことがない、走っていて気持ちがいい道を見つけるとすごく嬉しくなってしまうんですね。
特段目的地は決めずに大体これくらいの時間で、出発地点に帰ってこれるルートでドライブしたい、ただし同じようなルートは避けたい。そんなことってよくありますよね。あるんですよ。
GraphHopper
Google MapsやAppleのマップのAPIだと、先述の要求を満たすのがなかなかに難しい。
そこで選択肢に上がるのがGraphHopper Directions APIです。
GraphHopperはOSSとして開発されているルーティングエンジンで、Web API経由で利用できます。
経路検索API、経路の最適化API、Matrix APIなど様々なAPIが用意されていますが、今回は経路検索APIであるRouting APIについてお話しします。
他のAPIについては公式ページをご覧ください。
料金体系
料金体系は以下の図の通りです。
(2024/12/14現在の公式のPricingページより抜粋)
今回使用するRouting APIは無料プランでも使用できますが、APIによっては有料プランが必須となります。
無料プランは500Credits/dayとなっていますが、1回の経路検索について、出発地点、目標地点と経由地点の合計地点数が2~10で1クレジット、それ以上は10ロケーション毎に1クレジットずつ消費が増えるようです(詳細は説明ページをご覧ください)。
本記事では出発地点からランダムなルートを通って返ってくる、round_trip
という経路検索を行います。
round_trip
の場合は1回の検索で2クレジットを消費します。
なお、optimize
パラメータをtrue
にすると巡回サラリーマン問題を解くことになり、10倍のクレジットが消費されるようなので用法、用量にご注意を。
ドライブルートのランダム生成を試してみる
では実際に試してみましょう。
GraphHopperにはAPI Explorerが用意されており、検索クエリの指定とマップ上での結果確認が簡単にできます。便利!
フラーの柏の葉オフィスを出発して5km程度ドライブして戻って来る、という経路検索をしてみます。
Routing APIはGET, POST両方が用意されていますがAPI ExplorerではPOSTしか使えないので、JSONを組み立ててリクエストを投げてみます。
{
"points": [[139.952404199447, 35.89523482673045]],
"locale": "jp",
"ch.disabled": true,
"algorithm": "round_trip",
"points_encoded": false,
"round_trip.distance": 5000,
"round_trip.seed": 0
}
結果がこちらです。
API Explorerの地図上にルートが表示されました!
なお、ルートはround_trip.seed
の値によって変わります。
これを1, 2, 3と変更すると、以下のように結果が変わります。
これでランダムに、出発地に戻ってくる経路検索ができました!簡単!
ちなみにレスポンスはこのような形になっています。
{
"hints": {
"visited_nodes.sum": 2180,
"visited_nodes.average": 1090
},
"info": {
"copyrights": [
"GraphHopper",
"OpenStreetMap contributors"
],
"took": 7,
"road_data_timestamp": "2024-12-10T05:00:00Z"
},
"paths": [
{
"distance": 4833.886,
"weight": 846.545029,
"time": 659653,
"transfers": 0,
"points_encoded": false,
"bbox": [
139.937242,
35.887307,
139.954871,
35.896675
],
"points": {
"type": "LineString",
"coordinates": [
[
139.952626,
35.895398
],
[
139.952814,
35.895464
],
[
139.952885,
35.895465
],
...
[
139.952626,
35.895398
]
]
},
"instructions": [
{
"distance": 24.898,
"heading": 66.61,
"sign": 0,
"interval": [
0,
2
],
"text": "Continue",
"time": 8963,
"street_name": ""
},
{
"distance": 51.358,
"sign": -2,
"interval": [
2,
4
],
"text": "Turn left",
"time": 18489,
"street_name": ""
},
{
"distance": 119.544,
"sign": -2,
"interval": [
4,
5
],
"text": "Turn left",
"time": 15370,
"street_name": ""
},
...
{
"distance": 0,
"sign": 4,
"last_heading": 246.61249418020893,
"interval": [
70,
70
],
"text": "Arrive at destination",
"time": 0,
"street_name": ""
}
],
"legs": [],
"details": {},
"ascend": 51.22882080078125,
"descend": 51.22882080078125,
"snapped_waypoints": {
"type": "LineString",
"coordinates": [
[
139.952626,
35.895398
],
[
139.937483,
35.887948
],
[
139.952626,
35.895398
]
]
}
}
]
}
ちなみに注意点として、points_encoded
パラメータをfalseにしないと(デフォルトはtrue)ルートが表示されないのでご注意ください。
points_encoded
がtrueだとポリラインエンコーディングされたデータが返ってくるため、それをデコードする必要があります。
iOSアプリでルートを表示してみる
せっかくなのでアプリ上でルートを表示できるようにしてみましょう。
今回はiOS17から使えるようになったMapPolyline
を使って、SwiftUIのMap
上にルートを表示します。
1. APIを叩いて経路データを取得
Routing APIを叩く部分は特段変わった部分が無いので説明は割愛します。
クエリパラメータの指定部分だけ掲載しておきます。
var queryParameters: [String : Any]? {
var queryParameters: [String: Any] = [:]
queryParameters["point"] = "\(latitude),\(longitude)"
queryParameters["locale"] = "jp"
queryParameters["algorithm"] = "round_trip"
queryParameters["round_trip.distance"] = roundTripDistance
queryParameters["round_trip.seed"] = roundTripSeed
queryParameters["points_encoded"] = true
queryParameters["key"] = "Your API Key"
return queryParameters
}
2. ポリライン情報をデコードしGeoJSON形式に変換
レスポンスをこのような形で定義しておきます。ポリラインエンコードされた形式で返却されるpointsをStringで定義しておきます。
struct Route: Decodable {
let paths: [Path]
struct Path: Decodable {
let points: String
}
}
Polyline形式のデコードには、その名もズバリPolylineというライブラリを使用しました。
APIレスポンスとしているRoute
型のroute
というオブジェクトを以下のように料理するとポリライン情報のデコードが可能です。
if let point = route.paths.first?.points {
let coordinates = Polyline(encodedPolyline: point).coordinates
...
}
デコードしたら、GeoJSONの形式に変換しましょう。
func coordinatesToGeoJSON(coordinates: [CLLocationCoordinate2D]) -> [String: Any] {
let geoJSON: [String: Any] = [
"type": "FeatureCollection",
"features": [
[
"type": "Feature",
"geometry": [
"type": "LineString",
"coordinates": coordinates.map { [$0.longitude, $0.latitude] }
],
"properties": [
"name": "GraphHopper Route"
]
]
]
]
return geoJSON
}
3. GeoJSONの情報をMKPolylineの配列に変換
[String: Any]
の形にしたGeoJSONデータを、Mapで表示できるようにMKPolylineの配列に変換します。
func geoJSONToPolylines(json: [String: Any]) -> [MKPolyline] {
do {
let data: Data = try JSONSerialization.data(withJSONObject: json, options: [])
let decoder = MKGeoJSONDecoder()
let features = try decoder.decode(data)
return features
.compactMap { $0 as? MKGeoJSONFeature }
.flatMap { $0.geometry }
.compactMap { $0 as? MKPolyline }
} catch {
print("GeoJSONデコードエラー: \(error.localizedDescription)")
return []
}
}
4. Map上に経路を表示
最後に、MKPolyline
の配列をMapPolyline
を使ってMap上に表示します。
fetchRoundTripRoute
メソッドは距離(m)を引数で受取り、API経由でランダムなルートを取得して返却する自作のメソッドです。
struct ContentView: View {
@State private var polylines: [MKPolyline] = []
var body: some View {
Map {
ForEach(polylines, id: \.self) { polyline in
MapPolyline(polyline).stroke(.blue, lineWidth: 3)
}
}
.task {
let repository = RouteRepository(apiClient: APIClient())
do {
let route = try await repository.fetchRoundTripRoute(distance: 5000)
guard let point = route.paths.first?.points,
let coordinates = Polyline(encodedPolyline: point).coordinates else { return }
let geoJSON = coordinatesToGeoJSON(coordinates: coordinates)
polylines = geoJSONToPolylines(json: geoJSON)
} catch {
print(error)
}
}
}
}
実行結果
無事マップ上に経路が表示されました!
おわりに
こんなに簡単にランダムな周回経路を取得できるなんて素晴らしいですね。
クレジットを増やしたり商用利用したりする場合は安くない金額がかかりますが、これだけ便利であれば納得…ということにしましょう。
これで無事、私の欲求であった「特段目的地は決めずに大体これくらいの時間で、出発地点に帰ってこれるルートでドライブしたい、ただし同じようなルートは避けたい」を満たすことができそうです。うれしいです。
厳密に言うと時間の指定はできないのですが、カーナビでそうなっているように30km/hとして、時間から大体の距離を計算して指定してあげると良いでしょう。
フラーのアドベントカレンダー2024、明日は@kanterburyさんで「Next.jsに試験的に追加されたError Boundary用API(forbidden, unauthorized)」です、お楽しみに!