夜風のMixedReality

xRと出会って変わった人生と出会った技術を書き残すためのGeekなHoloRangerの居場所

Microsoft Map-SDK for Unityと気象APIを組み合わせる。 HoloLensアドベントカレンダー2020 11日目

本日はMicrosoft Map-SDK for Unity(以下Map-SDK)の記事です。 HoloLensアドベントカレンダー2020 11日目の記事です。

〇Map-SDKとは?

f:id:Holomoto-Sumire:20200830164131j:plain

Map-SDKMicrosoftによって提供されているBingMapの機能をUnityで使用するためのAPIとそれを含んだツールです。

MixedRealityデバイスを想定して提供されているため、HoloLens 2はもちろん、初代HoloLens、MRTKを使用したVR(Quest)、それらを使用したスマートフォンARなどで使用することができます。

Map-SDKは以下のGitHubで提供されています。

github.com

〇Map-SDKの導入

Map-SDKGitHubからUnityパッケージをインストールしてインポートするか、Unityのパッケージマネージャーを使用して導入する2つの方法があります。

redhologerbera.hatenablog.com

〇Map-SDKの使い方

Map-SDKでは[MapSession]コンポーネント、[MapRenderer]コンポーネントをゲームオブジェクトにアタッチすることで使用できます。

Map-SDKはBingMapの開発者登録が必要ですが、それ以外の登録などは必要なく、[MapRenderer]コンポーネントのプロパティで緯度・経度の情報を入力すると、その座標の地図が表示されます。

f:id:Holomoto-Sumire:20200830174626g:plainf:id:Holomoto-Sumire:20200830174006g:plain

〇Weather API

今回は無料で使用でき()Unityでも導入できそうな[OpenWeatherMap]の[Weather API]を使用します。

[Weather API]は緯度経度の情報を渡すことでその場所の気象に関する情報が返されるAPIを提供しています。

Map-SDKと同じ情報を扱うため、相性が良いと今回選びました。

①[OpenWeatherMap]のサイトへアクセスします。

openweathermap.org

日本語ではなく完全に英語のサイトになります。

アカウントを作成していない場合はアカウントを作成してログイン状態にします。

②[MarketPlace]のタグを選択します。

f:id:Holomoto-Sumire:20201211100032j:plain

※2020年12月現在  商用利用など個人使用の枠を超える場合はライセンスを確認してください。

③weatherAPIのキーを取得してMyKeyをコピーします。 

f:id:Holomoto-Sumire:20201211180127j:plain

〇Unityで組み合わせる。

Weather APIでは次のようなJsonが返されます。

{"coord":{"lon":130.74,"lat":32.79},
"weather":[{
"id":803,
"main":"Clouds",
"description":"broken clouds",
"icon":"04d"}],
"base":"stations",
"main":{"temp":291.15,
"feels_like":289.61,
"temp_min":291.15,
"temp_max":291.15,
"pressure":1018,
"humidity":63},
"visibility":10000,
"wind":{
"speed":2.6,
"deg":290},
"clouds":{"all":75},"dt":1604807826,"sys":{"type":1,"id":8006,"country":"JP","sunrise":1604785254,"sunset":1604823644},"timezone":32400,"id":1858421,"name":"Kumamoto","cod":200}

まずはこのデータを格納するための方を作成します。

Unityのプロジェクトウィンドウから[WeatherEntity]というスクリプトを作成します。

[System.Serializable]
public class WeatherEntity
{
    public oWeather[] weather;
    public Wind wind;
    public Sys sys;
    public string name;
    public Main main;
    public Clouds clouds;
}

[System.Serializable]
public class oWeather
{
    public string description; // Rain, Snow, Clouds ... etc
    public string main;
}

[System.Serializable]
public class Wind
{
    public float deg;
    public float speed;
    public float gust;
}

[System.Serializable]
public class Sys
{
    public float sunrise;
    public string country;
}

[System.Serializable]
public class Main
{
    public float temp;
    public float pressure;
    public float humidity;
    public float temp_min;
    public float temp_max;
}

[System.Serializable]
public class Clouds
{
    public int all;
}

次にWeatherDataGeterというスクリプトを作成し、次のようにコードを書きます。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Microsoft.Maps.Unity;
using Microsoft.Geospatial;
using UnityEngine.Networking;
using UnityEngine.Events;

public class WeatherDataGeter : MonoBehaviour
{

   //緯度経度
    LatLon latLon;
    [SerializeField]
    MapRendererBase renderer;//Map-SDKで使用するRenderer

    [SerializeField]
    Vector2 _location;

    //気象データを格納する
    WeatherEntity _we;

    public string ApiKey = "自分自身のAPIkey";

    string _baseUrl = "http://api.openweathermap.org/data/2.5/weather";

    // Start is called before the first frame update
    void Start()
    {
       GetWetherData();
    }

    public void GetWetherData()
    {
        renderer = GetComponent<MapRendererBase>();
        latLon = renderer.Center;

        _location = new Vector2((float)latLon.LatitudeInDegrees, (float)latLon.LongitudeInDegrees);

        StartCoroutine(postdata(weatherEntity =>
        {
      Render(weatherEntity);
        }));
    }

    IEnumerator postdata(UnityAction<WeatherEntity> callback)
    {
        var url = string.Format("{0}?lat={1}&lon={2}&appid={3}", _baseUrl, _location.x.ToString(), _location.y.ToString(), ApiKey);
        var request = UnityWebRequest.Get(url);
        var progress = request.Send();

        int waitSeconds = 0;
        while (!progress.isDone)
        {
            yield return new WaitForSeconds(1.0f);
            waitSeconds++;
            int TimeOutSeconds = 10;
            if (waitSeconds >= TimeOutSeconds)
            {
                Debug.Log("timeout:" + url);
                yield break;
            }
        }

        if (request.isNetworkError)
        {
            Debug.Log("error:" + url);
        }
        else
        {
            string jsonText = request.downloadHandler.text;
            Debug.Log(jsonText);
            callback(JsonUtility.FromJson<WeatherEntity>(jsonText));
            yield break;
        }

    }
void Render(WeatherEntity weatherEntity)
    {
        //気象情報の説明
        Debug.Log(string.Format("weather:{0}", weatherEntity.weather[0].main));
        //気温
        Debug.Log(string.Format("Temp:{0}", (weatherEntity.main.temp - 273.15)));
        //風速
        Debug.Log(string.Format("Wind;{0}m", weatherEntity.wind.speed));
        //都市名
        Debug.Log(string.Format(weatherEntity.name));
        string sentMessage = string.Format("weather:{0}", weatherEntity.weather[0].main) + "\n" + string.Format("Temp:{0}", (weatherEntity.main.temp - 273.15) + "\n" + string.Format(weatherEntity.name));
     

    }
}

これを実行するとconsoleウィンドウに返されたJsonデータが表示されます。

f:id:Holomoto-Sumire:20201211151813j:plain

少し解説していきます。

  public void GetWetherData()
    {
        renderer = GetComponent<MapRendererBase>();
        latLon = renderer.Center;
  ...
    }

GetWetherData()ではMap-SDKで地形を描画しているMapRenderereを取得してrendererに代入します。

Map-SDKで表示している座標の気象データが欲しいので、Map-SDKで表示している座標を取得します。 これはMapRendererBase.CenterでLatLon型で取得することができます。

latLonにはこの値が代入されます。

  public void GetWetherData()
    {
      ...
        _location = new Vector2((float)latLon.LatitudeInDegrees, (float)latLon.LongitudeInDegrees);

        StartCoroutine(postdata(weatherEntity =>
        {

        }));
    }

LatLon型は2つのdouble型を格納する型なので、そのままでは扱いにくいです。そこで取得したlatLonをVector2型の_locationに変換します。

その後コルーチンを使いpostdateへ飛びます。

  IEnumerator postdata(UnityAction<WeatherEntity> callback)
    {
        var url = string.Format("{0}?lat={1}&lon={2}&appid={3}", _baseUrl, _location.x.ToString(), _location.y.ToString(), ApiKey);
        var request = UnityWebRequest.Get(url);
        var progress = request.Send();

        int waitSeconds = 0;
        while (!progress.isDone)
        {
            yield return new WaitForSeconds(1.0f);
            waitSeconds++;
            int TimeOutSeconds = 10;
            if (waitSeconds >= TimeOutSeconds)
            {
                Debug.Log("timeout:" + url);
                yield break;
            }
        }

        if (request.isNetworkError)
        {
            Debug.Log("error:" + url);
        }
        else
        {
            string jsonText = request.downloadHandler.text;
            Debug.Log(jsonText);
            callback(JsonUtility.FromJson<WeatherEntity>(jsonText));
            yield break;
        }
    }
   var url = string.Format("{0}?lat={1}&lon={2}&appid={3}", _baseUrl, _location.x.ToString(), _location.y.ToString(), ApiKey);
        var request = UnityWebRequest.Get(url);
        var progress = request.Send();

requestでサーバーに送ります。

    while (!progress.isDone)
        {
            yield return new WaitForSeconds(1.0f);
            waitSeconds++;
            int TimeOutSeconds = 10;
            if (waitSeconds >= TimeOutSeconds)
            {
                Debug.Log("timeout:" + url);
                yield break;
            }
        }

通信が完了するまでまち、10秒間完了しなかった場合はタイムアウトのログを表示し処理を終えます。

  if (request.isNetworkError)
        {
            Debug.Log("error:" + url);
        }
        else
        {
            string jsonText = request.downloadHandler.text;
            Debug.Log(jsonText);
            callback(JsonUtility.FromJson<WeatherEntity>(jsonText));
            yield break;
        }

ネットワークエラーが返された場合エラーのログが表示されます。 きちんと通信が行えた場合、jsonTextにデータを格納し、デバッグログとして表示します。

これが上記画像で表示されていたJsonになります。

最後にjsonTextのデータを[WeatherEntity]に格納しています。

この処理が完了した場合、Render(WeatherEntity weatherEntity)が処理されます。

void Render(WeatherEntity weatherEntity)
    {
        //気象情報の説明
        Debug.Log(string.Format("weather:{0}", weatherEntity.weather[0].main));
        //気温
        Debug.Log(string.Format("Temp:{0}", (weatherEntity.main.temp - 273.15)));
        //風速
        Debug.Log(string.Format("Wind;{0}m", weatherEntity.wind.speed));
        //都市名
        Debug.Log(string.Format(weatherEntity.name));
        string sentMessage = string.Format("weather:{0}", weatherEntity.weather[0].main) + "\n" + string.Format("Temp:{0}", (weatherEntity.main.temp - 273.15) + "\n" + string.Format(weatherEntity.name));
    }

ここでは処理された個々のデータをログとして出力します。

f:id:Holomoto-Sumire:20201211175246j:plain

上画像では品川、風速2.6m、気温12.65度、天気曇り というリアルタイムの情報がわかります。

〇使い方

①Map-SDKの[MapRenderer]コンポーネント、[MapSession]コンポーネントがアタッチされているオブジェクトに[WeatherDataGeter]コンポーネントをアタッチします。

 f:id:Holomoto-Sumire:20201211180739j:plain

②[MapRenderer]コンポーネントで表示する座標を変更した際に、[WeatherDataGeter]コンポーネントの[GetWetherData()]を処理させます。 次にサンプルスクリプトを載せておきます。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Microsoft.Geospatial;
using Microsoft.Maps.Unity;


public class MapChanger : MonoBehaviour
{
    [SerializeField]
    float lat;
    [SerializeField]
    float lon;

    [SerializeField]
    float zoomLevel;

    [SerializeField]
    MapRenderer _renderer;

    [SerializeField]
    WeatherDataGeter WeatherData;
    private void Start()
    {
        ChangeAndGetMapDate();
    }
    public void ChangeAndGetMapDate()
    {
        _renderer.SetMapScene(new MapSceneOfLocationAndZoomLevel(new LatLon(lat,lon),zoomLevel));
        StartCoroutine("getWeatherData");

    }

    IEnumerator getWeatherData()
    {
        //マップが切り替わるのを待つ
        yield return new WaitForSeconds(5);
        WeatherData.GetWetherData();
    }
}

実行するとマップが切り替わるようになりました。

f:id:Holomoto-Sumire:20201211183746g:plain

また、ログでその場所の気象データを取得できました。

f:id:Holomoto-Sumire:20201211183908j:plain

〇参考

qiita.com

〇HoloLensアドベントカレンダーとは?

冒頭でもお知らせしましたが、本日の記事はHoloLensアドベントカレンダー11日目の記事になります。

qiita.com

明日はdiberidarshiaさんの『例年の1年間のHoloLens中心MRの動きまとめる』の記事があります。