RACCOON TECH BLOG

株式会社ラクーンホールディングスのエンジニア/デザイナーから技術情報をはじめ、世の中のためになることや社内のことなどを発信してます。

UnityでARアプリの作り方3/4 Google Direction APIの経路を地図に表示

こんにちは、Paid開発ユニット werdnaの酒井です。

第3回ARアプリの作り方です。
UnityでARアプリの作り方1/4 環境構築+androidビルドからカメラ表示
UnityでARアプリの作り方2/4 GPSの緯度経度から地図の表示
UnityでARアプリの作り方3/4 Google Direction APIの経路を地図に表示(本記事)
UnityでARアプリの作り方4/4 磁気センサーで進行方向オブジェクトを回転

概要

本記事では「目的地の入力欄を作成し、前回表示できるようになった地図に目的地までの経路表示」を目標とします。
具体的には下記内容を順に説明していきます。
(第1回、第2回で環境構築が完了したので実装がメインになります。)

Google Directions APIについて

経路として表示するにあたって Google Directions API を利用します。
こちらは開始地点(origin)・目的地(destination)を渡すことで、目的地への経路をJSON形式で返してくれるAPIです。

下記のURLを叩くと、弊社から浜町公園までの経路のJSONを返してくれます。
https://maps.googleapis.com/maps/api/directions/json?origin=%E6%A0%AA%E5%BC%8F%E4%BC%9A%E7%A4%BE%E3%83%A9%E3%82%AF%E3%83%BC%E3%83%B3%E3%83%9B%E3%83%BC%E3%83%AB%E3%83%87%E3%82%A3%E3%83%B3%E3%82%B0%E3%82%B9&destination=%E6%B5%9C%E7%94%BA%E5%85%AC%E5%9C%92&mode=walking&key=${APIKey}

これを使って経路を取得し、Google Maps Static APIにpathパラメータとして経路の座標を渡していくことで、マップに経路を表示できます。
下記が実際に弊社から浜町公園までの経路を表示した地図を開くためのURLです。
(交差点や曲がるタイミングでそれぞれ座標が出てくるのでかなり長いパラメータになります。)
https://maps.googleapis.com/maps/api/staticmap?center=35.68352,139.782&key=${APIKey}&zoom=15&size=640x640&scale=2&maptype=terrain&style=element:labels|visibility:off&markers=35.68352,139.782&path=color:red|35.68352,139.782|35.6837966,139.7826428|35.6839462,139.7825885|35.6843817,139.7833476|35.6848073,139.7829592|35.686659,139.7860699|35.6871702,139.7868946|35.6872018,139.7871073|35.6872651,139.7871401|35.6873362,139.7870832|35.6880801,139.788473|35.6878793,139.7888879|35.688282,139.7892796

経路表示

早速実装を始めていきます。
まず、JSON形式で受け取った文字列をデシリアライズするためのクラスを作成します。
中身を一通りマッピングしていますが、今回経路として利用するのはroutes.legs.steps.end_location.lat/lngの2つです。

using System.Collections;
using System.Runtime.Serialization;
using System.Collections.Generic;

[DataContract]
public class GoogleDirectionData
{
    // DataMemberを付けたオブジェクトは、同じ名前のパラメータが入ったときに自動でマッピングされる([DataMember (name="hoge")]で明示的にマッピングも可能)
    [DataMember]
    public List<GeocodedWaypoints> geocoded_waypoints;

    [DataMember]
    public List<Route> routes;

    [DataMember]
    public string status;

    public class GeocodedWaypoints
    {
        [DataMember]
        public string geocoder_status;
        [DataMember]
        public string place_id;
        [DataMember]
        public ArrayList types;
    }

    public class Route
    {
        [DataMember]
        public List<Bound> bounds;
        [DataMember]
        public string copyrights;
        [DataMember]
        public List<Legs> legs;
        [DataMember]
        public List<OverviewPolyline> overview_polyline;
        [DataMember]
        public string summary;
        [DataMember]
        public List<string> warnings;
        [DataMember]
        public List<string> waypoint_order;
   }

    public class Bound
    {
        [DataMember]
        public Location northeast;
        [DataMember]
        public Location southwest;
    }

    public class Legs
    {
        [DataMember]
        public Distance distance;
        [DataMember]
        public Duration duration;
        [DataMember]
        public string start_address;
        [DataMember]
        public string end_address;
        [DataMember]
        public Location start_location;
        [DataMember]
        public Location end_location;
        [DataMember]
        public List<Step> steps;
    }

    public class OverviewPolyline
    {
        [DataMember]
        public string points;
    }

    public class Distance
    {
        [DataMember]
        public string text;
        [DataMember]
        public string value;
    }

    public class Duration
    {
        [DataMember]
        public string text;
        [DataMember]
        public string value;
    }

    public class Location
    {
        [DataMember]
        public string lat;
        [DataMember]
        public string lng;
    }

    public class Step
    {
        [DataMember]
        public Distance distance;
        [DataMember]
        public Duration duration;
        [DataMember]
        public Location start_location;
        [DataMember]
        public Location end_location;
        [DataMember]
        public string html_instructions;
        [DataMember]
        public Polyline polyline;
        [DataMember]
        public string travel_mode;
    }

    public class Polyline
    {
        [DataMember]
        public string points;
    }

}

次にGoogle Directions APIを叩くコントローラを作成します。
オブジェクトを作成してscriptを付けるという流れは変わりませんが、経路は直接画面に表示するのではなく先ほど作成したStaticMapに描画するためのステータスを取得するものなので、
Planeの作成と同様に画面左上から[Create→Create Empty] もしくはHierarchyを右クリックして[Create Empty]でGameObjectを作成しスクリプトの追加をしましょう。
(GameObjectは任意のオブジェクトの基になるもので、ここにMeshやColider等を付与することでCubeやCanvasといった3D Objectとして扱えるようになります。
 そのままだと実体を持たないオブジェクトとして使えるので、今回はscriptを処理するためのコンテナとして利用しています。)

また、目的地を入力するためのオブジェクトとして[Create→UI→Input Field]を作成します。
Input Fieldは、特に何も指定しなくともスマホでタップするとキーボードが表示され入力が出来ます。
このオブジェクトを作成したコントローラに紐づけて、入力した目的地への経路を表示します。

追加するスクリプトには下記を書き込んでください。

using System.Collections;
using UnityEngine;
using UnityEngine.Networking;
using System.Runtime.Serialization.Json;
using System.Text;
using System.IO;
using UnityEngine.UI;

public class DirectionController : MonoBehaviour
{
    private const string GOOGLE_DIRECTIONS_API_URL = "https://maps.googleapis.com/maps/api/directions/json?key=${APIKey}&mode=walking";

    // 目的地を入力させるInputField
    public InputField destination;
    // APIより取得した経路(staticMapControllerに渡すためのパラメータ)
    public string destinationRoute = "";

    private int frame = 0;

    // 初期状態では目的地が入ることはないのでStartでは何もしない。
    void Start()
    {

    }

    void Update()
    {
        // 更新は5秒に一度、目的地が設定されていない場合は取得しない。
        if (frame >= 300 && destination.text != "")
        {
            Debug.Log(UnityWebRequest.UnEscapeURL(destination.text));
            StartCoroutine(GetDirection());
            frame = 0;
        }
        frame++;
    }

    private IEnumerator GetDirection()
    {
        // origin=開始地点。現在地からの経路を出したいので現在地を渡す。
        var query = "&origin=" + UnityWebRequest.UnEscapeURL(string.Format("{0},{1}", Input.location.lastData.latitude, Input.location.lastData.longitude));

        // destination=目的地。InputFieldに入力した文字列をエスケープして渡す。
        query += "&destination=" + UnityWebRequest.UnEscapeURL(destination.text);
        UnityWebRequest req = UnityWebRequest.Get(GOOGLE_DIRECTIONS_API_URL + query);
        yield return req.SendWebRequest();

        if (req.error == null)
        {
            // 返ってきたjsonをByte[]形式を処理できるMemoryStreamで受け取る
            MemoryStream ms = new MemoryStream(Encoding.UTF8.GetBytes(req.downloadHandler.text));

            // google DirectionのJSONをオブジェクトとして読み込めるようにシリアライザを作成
            var serializer = new DataContractJsonSerializer(typeof(GoogleDirectionData));

            // 作成したJSON用のクラスにデシリアライズ
            GoogleDirectionData googleDirectionData = (GoogleDirectionData)serializer.ReadObject(ms);

            // データの形式としてroutes[0].legs[0]は固定なので一旦変数に格納
            var leg = googleDirectionData.routes[0].legs[0];
            // 書き込み前に初期化
            destinationRoute = "";
            for (int i = 0; i < leg.steps.Count; i++) 
            { 
                // 経路は|緯度,経度|という書き方になるので、受け取ったlatitude, longitudeをパイプとカンマを付けて追加していく 
                destinationRoute += "|" + leg.steps[i].end_location.lat + "," + leg.steps[i].end_location.lng; 
                // 経路が多すぎるとUriFormatExceptionで落ちるため上限を設定しておく。 
                if (i > 20) break;
            }
        }
    }
}

最後に地図上へ経路を表示するために、staticMapControllerを少し書き換えます。

using System.Collections;
using UnityEngine;
using UnityEngine.Networking;

public class StaticMapController : MonoBehaviour
{
    //Google Maps API Staticmap URL
    // ${APIKey}を自分で作成したapiキーに書き換えてください。
    private const string STATIC_MAP_URL = "https://maps.googleapis.com/maps/api/staticmap?key=${APIKey}&zoom=15&size=640x640&scale=2&maptype=terrain&style=element:labels|visibility:off";

    private int frame = 0;

    // DirectionControllerを付与したオブジェクトを追加する
    public DirectionController direction = null;

    // Start is called before the first frame update
    void Start()
    {
        // 非同期処理
        StartCoroutine(getStaticMap());
    }

    // Update is called once per frame
    void Update()
    {
        // 5秒に一度の実行
        if (frame >= 300)
        {
            StartCoroutine(getStaticMap());
            frame = 0;
        }
        frame++;
    }

    IEnumerator getStaticMap()
    {
        var query = "";

        // centerで取得するミニマップの中央座標を設定
        query += "&center=" + UnityWebRequest.UnEscapeURL(string.Format("{0},{1}", Input.location.lastData.latitude, Input.location.lastData.longitude));
        // markersで渡した座標(=現在位置)にマーカーを立てる
        query += "&markers=" + UnityWebRequest.UnEscapeURL(string.Format("{0},{1}", Input.location.lastData.latitude, Input.location.lastData.longitude));
        // DirectionControllerより経路が格納されていればpathを追加する
        if (direction.destinationRoute != "")
        {
            query += "&path=color:red|" + UnityWebRequest.UnEscapeURL(string.Format("{0},{1}", Input.location.lastData.latitude, Input.location.lastData.longitude)) +  direction.destinationRoute;
        }
        // リクエストの定義
        var req = UnityWebRequestTexture.GetTexture(STATIC_MAP_URL + query);
        // リクエスト実行
        yield return req.SendWebRequest();

        if (req.error == null)
        {
            // すでに表示しているマップを更新
            Destroy(GetComponent<Renderer>().material.mainTexture);
            GetComponent<Renderer>().material.mainTexture = ((DownloadHandlerTexture)req.downloadHandler).texture;
        }
    }
}

これらの対応で経路まで表示が完了しました!
下記のような画面になります。(弊社から浜町公園までの経路を出しています)

5秒おきに更新がかかるので、目的地を更新したり、移動して端末の位置が変わるごとに経路情報も更新されていきます。
ここまでで経路案内アプリとしては8割方完了です!
あとは次の経路へと進む方向を指し続けるようなオブジェクトを作成して完成になります。
UnityでARアプリの作り方4/4 磁気センサーで進行方向オブジェクトを回転

一緒にラクーンのサービスを作りませんか? 採用情報を詳しく見る

関連記事

運営会社:株式会社ラクーンホールディングス(c)2000 RACCOON HOLDINGS, Inc