🎉

日本のPOI検索でRoutable Pointsがサポートされました!

2024/02/02に公開

はじめに

Search Box APIにおける日本のPOI検索で、Routable Pointsがサポートされました。この記事では、Routable Pointsの使い方等について解説します。

Search Box APIとは

Search Box APIとはMapboxのSearchサービスが提供するAPIの一つです。ユーザー入力に応じて次々と候補を表示するような、インタラクティブな検索に強みがあるのが特徴です。インタラクティブな検索を実現するため、以下の2つのエンドポイントが使用されます。

  • /suggest: ユーザの入力した住所やPOI名に部分一致する結果一覧を返します。一般にユーザが入力するに従って徐々に候補が絞られている挙動にするため、一定の入力毎にこのエンドポイントを呼び出します。
  • /retrieve: 候補の中からユーザが一つ選択した際に、その選択した候補に関する詳細情報を取得するために使用します。

一般的な検索では複数回の/suggestと一回の/retrieveがセットになります。このセットのことをセッションと呼び、課金もセッションに対して行われます。そのため、同一セッションの検索であることを明示するためにクエリパラメータとしてsession_tokenが必要となります。また、この値はアプリケーション側で準備しますが、APIドキュメントにある通りUUIDv4の使用が推奨されています。

クエリ例

それでは、試しに「新宿御苑」を検索してみましょう。

最初は/suggestエンドポイントで検索文字列に合致する候補一覧を取得します。各種指定はクエリパラメータで行います。検索文字列はq=で指定します。ここでは「新宿御苑」をURLエンコーディングした文字列を指定します。日本語で検索するのでlanguage=ja、日本国内の検索なのでcountry=JPを指定します。言語・国を設定しないと期待通りの結果が得られません。また、簡単のためにsession_token1にしています。YOUR_MAPBOX_ACCESS_TOKENの部分にはご自身のパブリックトークンを入れてください。

ここでは出力結果をパイプでjqコマンドに入力させることで、JSONを見やすく出力しています。

% curl "https://api.mapbox.com/search/searchbox/v1/suggest?q=%E6%96%B0%E5%AE%BF%E5%BE%A1%E8%8B%91&language=ja&country=JP&session_token=1&access_token=YOUR_MAPBOX_ACCESS_TOKEN" | jq

{
  "suggestions": [
    {
      "name": "新宿御苑",
      "mapbox_id": "dXJuOm1ieHJldDp5cERoSGlleTlSTkxTMF9BRTFGQndNaEMyeUhnb1cyQkE0MUZBNmo2bUlTNVJEcEd0bF9RdHAxRTZQNURjVTY5V29PQlVSYmJidGNXOXlOZW1zd2d3MGVYTkFQMlFpY2V4Rm02Nkk4bmQ2ZG5rWWJwbFY3OXdXYzNkZGM4TVhxbWdSRmpEa01TQS1mT0FXbDJ6aDhQUFc5MWhIcFlCeDNOMEFEdWo5NnU5SWJqMG5sTGoyVUYzTW
5SWllZckd1V1NxX28tQ2F2a1J3TkN4TGNJLTRvV3pldWpvT0hJUHZCM1lRUDRvLVJJX3d3SEI4Zk1rQkxmY3JZYkk2Nk05Y0xkclA0WmNyd0JQajBfTDNwN1Fxeks5VThhZzd3aHJPR0F0LUR1ajM0Z01kVU1kSUg2clNEN3ZuQlVjRjVjS2Z2dnN6dlBfYmVINTd1ZHBiTERlendXcE1zOG1yN0VsYXU5LURjTWc1c2lkWkVRZVVXOFhwRzkwblhSeURsMnBUeDFsSDNjQmN4
ek9ESXBWNFBOSTZmWWlLVjNzSUtra0xtbVBXV0x6bmN5RHZCRHpDQWtrU1Z1SWk1UFExMzhoWExkYWJMZmI0MkhzdUVSMVBTRkE3eEZKdUZVcUltdU9saFRXMGxxLWxrMHZOeElsYXlFWDc2Uld5Tlp1blJ5TVZ2YXJlSUNRaE9sWHAwNGRMY3dheFBaMUUxM1B2NW9OQWlkN3NNdUNMRFQtOWFJd3B5QWxpR3hSczZoNmY4dl9LQzkwc2VFNEszYTVlTlBpdFFRdDFBRmh1V1
B0TE9Vbkc0dlVFQnl6UDhDX0VPVXZnXy1RN2N3NmxXaHhUeTU4QkpyX2RPNEF2TkFka0QyUTBNaU00N2ZoanlWVmFpVnhMQVc2S01yS0x2VkQzTC0tNzRQQ19WaHJwUHNFdjF4SkM3X19BNzd5bFpjNk5PeF9lMHZrTW9LWXVQeDVpT05VX3Y1bW0yNDRGNmdaN3dQbVE2N25KUkZ3LURlenVIempvMllqTmxxUnBBaXZ0TndKRkZyYXAybGZUODQtbmJNLWRZeXNpSy1tV1Ft
MnVVOXVzbkpaN0dLMmRmS1h1LXVpWWQxR0Vqem9EYjh1OEtQUHZXeTdnZmNSdmlNbjFETGM5TFpjcDJuZ2J2M0pIWUZna1FIbnQ3X2F1Rl8wNWdwN0hGcmR5Q3JoRS0wZExVVzFacDFnM2EzNzBnRXlucnFoOXFoZ1FQbGpuaVBHb1lDS0otVnlBbGY5aHJLZVhiQ0VZdV9yYVJOYnJ3OHlGQ1hJV1N2Sm5vNXJseE5EM09oX3F4QXBjaWJUSE1xZVpDa0c0T1dXdmJEaWxSWU
1UN0dZdmE0aFhaeGU2SUJiMlJjZ0xabnEyYWFrbDdSQ0loa1JoWUVjemc1QnQ1ZzFEM3BxLTVRZ1B0YktBbTBsRUs5d3VybnRvRlRSaVJwTzMxXy1TaHlFeG9iM3VqMTRGb0FHVXNmLTlEZkhINEpvWjlkT1M4cVpOd05DUklTOVpDR3hXQWF5V2U0SnBDUFhiejFHYlpWUEVsVUVnaVR1ZkVUcWZYQjU3clp3R01RWUI0Z1FXeXJmdTFzaFlia2xnZ3FJdkxtb215b0Rvck5v
Y1EyazVITm1ob1prejhlMkFkOGc0cExWaHhkVTlFOTVuQno0eUk5cFBxZHY5X1U4cGlyUGVnQW1tb3c5NDliVDFHV3dVRlhFLWtNblRDYjlGUmFDcWpIUFVUa0pxVTFsWmNCd0x6VkdHZ25fTUlXcHRtUnpleGxxN1ZhME5UVnBlV2lka3hvOHRJUTdDQ3BNZGcyQnZmVEtpNkU2czk4bXdQdXlpOXV0SDUteFpOOXB0MUxnQUtPQ0U4VElfMVFSNTcwZm4ybzU0bDc1SlZqY0
V0bUNkMEFSLUxjMENJT3pwblRhY21SVW1YQXlUMWw5QUtSeHhjRmJmcC0taEdkeHg5UlNicnVTeEpkQzdBeDlxNVE2Ym1BNDFaYUtfRTZMR29MRFZjbUNSQXJMTnF3TTdveTB2dlF2d2MzSV9NYm1udmd4Z2RWa0cwZWk1MXVmXzgzb29EZXBrTEZteC1nczRNWmMxYzB0SGdacmhuUS0xd2RNdERXZE42eTNoTlMxcEhYV1FmZWduX2hPaWhMSTZGNUo3LWxqRWx1MVhFcWZh
WnB4YnlqQ0pVeGl3b0xrMGxFYV9mTlphRVhYVXdtandha3BOeXVmRDJteXVJTHVFUDlzWVFSdTVvWUtTdjFpcVpfVUVjZ3hzSHoyck9WWEhDMkpXQUphRWhnMjBrZ2RPNVFMY044MjJTajVKTUhEQUJubmk0U0YxSXA0ZlI5clcyb0N1NjVPeWxZdS13a1VWWXA0blpmU1pjLXluR3JMUUoyenlRLXQyd2F5clh5dF85TmlOWnVjTzR1S1hTYldTR203ZkdDUTlfXzJPRHZFVW
RwVXhqVjFLdElFSXppT1ctZkk2SlVKM1JlTUpjV0lwVzR0ZkdnSGUxamtzY1ZiY1JDQUw2bW54b2hSSHA1Vi1Ed1R6bDBSWnF1ZmhrMnkyY2xUSjgtN3ZlbVd1YWpMenF6a0FkQzl2aC10QmNjOFdsUUE2RjRSck5iWDZhU1lTTGx0enVkYVUwYTVYRnd6M05SdkJvYzFMQ1RaUFJXdnBHV3U2eFE2eUFLS1o1aXI2SF9OcHNxaF9tUEFhVUhFQzY4ZlR5aTBQWVVUMENKNU5p
Tkh6VXZZeUJkNmFRa3pPVTlqaERycEFDU1hrUE0yV2NRc25lcXMtMzA4SG43QWtYTDItbzk4dTlyX1ZfLUN0ajEtZGNySW5aNjdWNzA3SDVCRnpwd19vY01yR3NjVXkzeTY4dkg1bmxKNVpYZFU1YmdNWXhac0c2UWNZY2FwZnZLdEpDejJqeDdaRE9MRnlPdVNzSGYtMEU3SVU0MmNpcGN1TGVySGo2Q1p1Wng2UkUwcklwVWJ2NkVkVG9yczBTa2FLd3dYRXpfWElneW5FZF
RPMnJTMUF5Y1k0OWJEcW5Dc28yejc4QWtWZzd3Y1BHNmRjTXpPanU5eXByRmtaTU9FWmEyNnItYjc2UXNwQnh3MHhXQldPcUlxS3RaWVZDN2FCbkhYdTBoNVp0ZHRKOWh3U1MxZWJMeEJobV9TNHF5RGR2bW9INmhVU0FlNDU0TFVqR3dNbjNBUGxENWxnaHdZLXVBVkxGWl9hWVZSaUJZN2VSZnEyc0NVUT09",
      "feature_type": "poi",
      "address": "東京都新宿区内藤町11",
      "full_address": "東京都新宿区内藤町11, 東京都新宿区内藤町11",
      "place_formatted": "東京都新宿区内藤町11",
      "context": {
        "region": {
          "name": "東京都",
          "region_code": "13",
          "region_code_full": "JP-13"
        },
以下略

次に/retrieveエンドポイントで詳細情報を取得します。具体的には/suggestで得られたmapbox_idを使用します。ここでは/suggestエンドポイントの1つ目が新宿御苑の情報になるので、以下のようなクエリになります。

% curl "https://api.mapbox.com/search/searchbox/v1/retrieve/dXJuOm1ieHJldDp5cERoSGlleTlSTkxTMF9BRTFGQndNaEMyeUhnb1cyQkE0MUZBNmo2bUlTNVJEcEd0bF9RdHAxRTZQNURjVTY5V29PQlVSYmJidGNXOXlOZW1zd2d3MGVYTkFQMlFpY2V4Rm02Nkk4bmQ2ZG5rWWJwbFY3OXdXYzNkZGM4TVhxbWdSRmpEa01TQS1mT0FXbDJ6aDhQUFc5MWhIcFlCeDNOMEFEdWo5NnU5SWJqMG5sTGoyVUYzTW5SWllZckd1V1NxX28tQ2F2a1J3TkN4TGNJLTRvV3pldWpvT0hJUHZCM1lRUDRvLVJJX3d3SEI4Zk1rQkxmY3JZYkk2Nk05Y0xkclA0WmNyd0JQajBfTDNwN1Fxeks5VThhZzd3aHJPR0F0LUR1ajM0Z01kVU1kSUg2clNEN3ZuQlVjRjVjS2Z2dnN6dlBfYmVINTd1ZHBiTERlendXcE1zOG1yN0VsYXU5LURjTWc1c2lkWkVRZVVXOFhwRzkwblhSeURsMnBUeDFsSDNjQmN4ek9ESXBWNFBOSTZmWWlLVjNzSUtra0xtbVBXV0x6bmN5RHZCRHpDQWtrU1Z1SWk1UFExMzhoWExkYWJMZmI0MkhzdUVSMVBTRkE3eEZKdUZVcUltdU9saFRXMGxxLWxrMHZOeElsYXlFWDc2Uld5Tlp1blJ5TVZ2YXJlSUNRaE9sWHAwNGRMY3dheFBaMUUxM1B2NW9OQWlkN3NNdUNMRFQtOWFJd3B5QWxpR3hSczZoNmY4dl9LQzkwc2VFNEszYTVlTlBpdFFRdDFBRmh1V1B0TE9Vbkc0dlVFQnl6UDhDX0VPVXZnXy1RN2N3NmxXaHhUeTU4QkpyX2RPNEF2TkFka0QyUTBNaU00N2ZoanlWVmFpVnhMQVc2S01yS0x2VkQzTC0tNzRQQ19WaHJwUHNFdjF4SkM3X19BNzd5bFpjNk5PeF9lMHZrTW9LWXVQeDVpT05VX3Y1bW0yNDRGNmdaN3dQbVE2N25KUkZ3LURlenVIempvMllqTmxxUnBBaXZ0TndKRkZyYXAybGZUODQtbmJNLWRZeXNpSy1tV1FtMnVVOXVzbkpaN0dLMmRmS1h1LXVpWWQxR0Vqem9EYjh1OEtQUHZXeTdnZmNSdmlNbjFETGM5TFpjcDJuZ2J2M0pIWUZna1FIbnQ3X2F1Rl8wNWdwN0hGcmR5Q3JoRS0wZExVVzFacDFnM2EzNzBnRXlucnFoOXFoZ1FQbGpuaVBHb1lDS0otVnlBbGY5aHJLZVhiQ0VZdV9yYVJOYnJ3OHlGQ1hJV1N2Sm5vNXJseE5EM09oX3F4QXBjaWJUSE1xZVpDa0c0T1dXdmJEaWxSWU1UN0dZdmE0aFhaeGU2SUJiMlJjZ0xabnEyYWFrbDdSQ0loa1JoWUVjemc1QnQ1ZzFEM3BxLTVRZ1B0YktBbTBsRUs5d3VybnRvRlRSaVJwTzMxXy1TaHlFeG9iM3VqMTRGb0FHVXNmLTlEZkhINEpvWjlkT1M4cVpOd05DUklTOVpDR3hXQWF5V2U0SnBDUFhiejFHYlpWUEVsVUVnaVR1ZkVUcWZYQjU3clp3R01RWUI0Z1FXeXJmdTFzaFlia2xnZ3FJdkxtb215b0Rvck5vY1EyazVITm1ob1prejhlMkFkOGc0cExWaHhkVTlFOTVuQno0eUk5cFBxZHY5X1U4cGlyUGVnQW1tb3c5NDliVDFHV3dVRlhFLWtNblRDYjlGUmFDcWpIUFVUa0pxVTFsWmNCd0x6VkdHZ25fTUlXcHRtUnpleGxxN1ZhME5UVnBlV2lka3hvOHRJUTdDQ3BNZGcyQnZmVEtpNkU2czk4bXdQdXlpOXV0SDUteFpOOXB0MUxnQUtPQ0U4VElfMVFSNTcwZm4ybzU0bDc1SlZqY0V0bUNkMEFSLUxjMENJT3pwblRhY21SVW1YQXlUMWw5QUtSeHhjRmJmcC0taEdkeHg5UlNicnVTeEpkQzdBeDlxNVE2Ym1BNDFaYUtfRTZMR29MRFZjbUNSQXJMTnF3TTdveTB2dlF2d2MzSV9NYm1udmd4Z2RWa0cwZWk1MXVmXzgzb29EZXBrTEZteC1nczRNWmMxYzB0SGdacmhuUS0xd2RNdERXZE42eTNoTlMxcEhYV1FmZWduX2hPaWhMSTZGNUo3LWxqRWx1MVhFcWZhWnB4YnlqQ0pVeGl3b0xrMGxFYV9mTlphRVhYVXdtandha3BOeXVmRDJteXVJTHVFUDlzWVFSdTVvWUtTdjFpcVpfVUVjZ3hzSHoyck9WWEhDMkpXQUphRWhnMjBrZ2RPNVFMY044MjJTajVKTUhEQUJubmk0U0YxSXA0ZlI5clcyb0N1NjVPeWxZdS13a1VWWXA0blpmU1pjLXluR3JMUUoyenlRLXQyd2F5clh5dF85TmlOWnVjTzR1S1hTYldTR203ZkdDUTlfXzJPRHZFVWRwVXhqVjFLdElFSXppT1ctZkk2SlVKM1JlTUpjV0lwVzR0ZkdnSGUxamtzY1ZiY1JDQUw2bW54b2hSSHA1Vi1Ed1R6bDBSWnF1ZmhrMnkyY2xUSjgtN3ZlbVd1YWpMenF6a0FkQzl2aC10QmNjOFdsUUE2RjRSck5iWDZhU1lTTGx0enVkYVUwYTVYRnd6M05SdkJvYzFMQ1RaUFJXdnBHV3U2eFE2eUFLS1o1aXI2SF9OcHNxaF9tUEFhVUhFQzY4ZlR5aTBQWVVUMENKNU5pTkh6VXZZeUJkNmFRa3pPVTlqaERycEFDU1hrUE0yV2NRc25lcXMtMzA4SG43QWtYTDItbzk4dTlyX1ZfLUN0ajEtZGNySW5aNjdWNzA3SDVCRnpwd19vY01yR3NjVXkzeTY4dkg1bmxKNVpYZFU1YmdNWXhac0c2UWNZY2FwZnZLdEpDejJqeDdaRE9MRnlPdVNzSGYtMEU3SVU0MmNpcGN1TGVySGo2Q1p1Wng2UkUwcklwVWJ2NkVkVG9yczBTa2FLd3dYRXpfWElneW5FZFRPMnJTMUF5Y1k0OWJEcW5Dc28yejc4QWtWZzd3Y1BHNmRjTXpPanU5eXByRmtaTU9FWmEyNnItYjc2UXNwQnh3MHhXQldPcUlxS3RaWVZDN2FCbkhYdTBoNVp0ZHRKOWh3U1MxZWJMeEJobV9TNHF5RGR2bW9INmhVU0FlNDU0TFVqR3dNbjNBUGxENWxnaHdZLXVBVkxGWl9hWVZSaUJZN2VSZnEyc0NVUT09?session_token=1&access_token=YOUR_MAPBOX_ACCESS_TOKEN" | jq

{
  "type": "FeatureCollection",
  "features": [
    {
      "type": "Feature",
      "geometry": {
        "coordinates": [
          139.71068888876172,
          35.68703888893127
        ],
        "type": "Point"
      },
      "properties": {
        "name": "新宿御苑",

新宿御苑の座標等が取得できているのがわかります。

Routable Pointsとは

場所を知るだけであれば、検索した施設の代表点の座標だけでよいです。しかし、ナビゲーションで検索するときに知りたいのはその施設の入口のはずです。もし入口から遠い位置にある代表点を目的地としてナビゲーションを実行すると、目的地周辺に到着したものの入口が見つからないという事態が発生します。

そこで必要となるのがRoutable Pointsです。これは名前の通り「ルート検索に使える点」です。つまり、一般的にその施設や駐車場の入口の座標が設定されます。

新宿御苑の例では以下のようにfeatures.properties.coordinatesの中にroutable_pointsが含まれています。

{
  "type": "FeatureCollection",
  "features": [
    {
      "type": "Feature",
      "geometry": {
        "coordinates": [
          139.71068888876172,
          35.68703888893127
        ],
        "type": "Point"
      },
      "properties": {
        "name": "新宿御苑",
中略
        },
        "coordinates": {
          "latitude": 35.68703888893127,
          "longitude": 139.71068888876172,
          "routable_points": [
            {
              "name": "default",
              "latitude": 35.68741666661368,
              "longitude": 139.7138750002119
            }
          ]
        },

デモ

実際にRoutable Pointsを地図上に表示し、経路探索した際の結果を見てみましょう。以下のデモは赤いマーカーが代表点、青いマーカーがRoutable Pointsを表しています。また、各点に対して東京駅からの経路を赤い線、青い線で表示しています。

代表点は新宿御苑の中ほどにあり、経路もそこが終点となっています。この赤い線が走っている道路は甲州街道で、新宿御苑の地下を通っています。つまり、代表点に対してナビゲーションを実行すると地下トンネルの真ん中でナビゲーションが終了して途方に暮れることになります。それに対し、Routable Pointsは新宿御苑の入口を指しており、青い線もそこが終点となっています。

Navigatoin SDK で試してみる

それでは、実際にナビゲーションアプリを作成して挙動を試してみましょう。Navigation SDK (v3)のUX FrameworkにはSeachが統合されており、行き先検索のUIが標準で利用できます。また、このSearchはSearch Box APIを使用しており、Routable Pointsを利用するようになっています。

ここではNavigation SDK for Androidを試してみます。基本的にインストールガイドに従えばOKです。

また、開発環境はAndroid Studio Hedgehog | 2023.1.1 Patch 2を使用しました。

準備

  1. Mapbox Maps SDK for Android/iOSのサンプルの実行環境を作るを参考にシークレットトークン・パブリックトークンを準備
  2. Android Studioを起動し、新規プロジェクトを作成
  3. Empty Views Activityを選択
  4. Project名を適当に設定し、Minimum SDKをAPI 22("Lollipop", Android 5.1)に設定

依存関係

  1. プロジェクトレベルのsettings.gradle.ktsdependencyResolutionManagement.repositoriesに以下を追加
maven {
    url = uri("https://api.mapbox.com/downloads/v2/releases/maven")
    credentials {
        username = "mapbox"
        password = providers.gradleProperty("MAPBOX_DOWNLOADS_TOKEN").get() //インストールガイドから変更が必要です
    }
    authentication {
        create<BasicAuthentication>("basic")
    }
}
  1. モジュールレベルのbuild.gradle.ktsdependenciesに以下を追加
implementation("com.mapbox.navigationux:android:1.0.0-beta.20")
  1. Syncする

既知の問題への対処

  1. モジュールレベルのbuild.gradle.ktsandroidに以下を追加
packagingOptions {
    resources {
        excludes += setOf(
            // To compile the current version of Dash SDK you need to add only these two lines:
            "META-INF/DEPENDENCIES",
            "META-INF/INDEX.LIST",
        )
    }
}
  1. モジュールレベルのbuild.gradle.ktsdependenciesに以下を追加
configurations.all {
    exclude(group = "com.google.guava", module = "listenablefuture")
}
  1. Syncする

コーディング

マニフェストの設定

AndroidManifest.xmに以下を追加

<!-- Always include this permission -->
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />

<!-- Include only if your app benefits from precise location access. -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />

パブリックトークンの設定

app/src/main/res/values/strings.xmlに以下を追加(YOUR_PUBLIC_MAPBOX_ACCESS_TOKENを自身のパブリックトークンに変更してください)

<string name="mapbox_access_token" translatable="false" >YOUR_PUBLIC_MAPBOX_ACCESS_TOKEN</string>

フラグメントコンテナの追加

app/src/main/res/layout/activity_main.xmlの中身を以下に変更

<androidx.fragment.app.FragmentContainerView
    android:id="@+id/container"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:name="com.mapbox.dash.sdk.DashNavigationFragment" />

コード

MainActivity.ktを以下のように記述

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        val config = DashConfig.create(
            applicationContext = applicationContext,
            accessToken = getString(R.string.mapbox_access_token)
        )

        Dash.init(config)

        setContentView(R.layout.activity_main)
    }
}

実行

それでは実際に実行してみます。

  1. 虫眼鏡アイコンをタップ
    android 0
  2. 新宿御苑を検索
    android 1
  3. 新宿御苑をタップし、行き先のプレビューを表示
    android 2
  4. Preview routeボタンをタップし、経路のプレビューを表示
    android 3
  5. Navigateボタンをタップし、ターンバイターンナビゲーションの実行
    android 4

ちゃんとRoutable Pointsが示す場所に案内されていますね!

まとめ

Routable Pointsを使用することで、特に案内終了地点が正確になり、ナビゲーション体験が大きく向上します。

GitHubで編集を提案
マップボックス・ジャパン合同会社

Discussion