2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

フラー株式会社Advent Calendar 2024

Day 14

GraphHopper Directions APIでランダム周回ルートを楽しむ

Last updated at Posted at 2024-12-14

この記事は フラー株式会社 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 14.56.06.png
(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を組み立ててリクエストを投げてみます。

リクエスト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
}  

結果がこちらです。
スクリーンショット 2024-12-14 15.22.20.png
API Explorerの地図上にルートが表示されました!

なお、ルートはround_trip.seedの値によって変わります。
これを1, 2, 3と変更すると、以下のように結果が変わります。
スクリーンショット 2024-12-14 15.22.55.png
スクリーンショット 2024-12-14 15.23.13.png
スクリーンショット 2024-12-14 15.23.23.png

これでランダムに、出発地に戻ってくる経路検索ができました!簡単!
ちなみにレスポンスはこのような形になっています。

レスポンスJSON
{
  "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で定義しておきます。

Route
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の形式に変換しましょう。

座標配列を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の配列に変換します。

GeoJSONを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)」です、お楽しみに!

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?