リングフィットアドベンチャーの道具でグーグルストリートビューを歩き回る【Unity+GoogleMapsJavaScriptAPI】

リングコンは使いません（レッグバンドとジョイコンだけ）

## 経緯

* スタンディングデスク+ステッパーという意識高そうな在宅ワーク環境を手に入れた...のはいいものの、すぐ飽きた。座りたい。
* 運動の動機付けとしてサイクリングマシンでストリートビューを操作するやつ([これ](https://omocoro.jp/kiji/230063/)とか)をステッパーで作ってみようと思った。
* 運動を検知するためにセンサーが必要。何かしらの動きをマイコンで検知するとして、さらにそれを何かしらの手段でPCに送信して、...結構大変だなあ。お金もかかりそうだ。
* どっかに**運動（特に傾き）を検知できて**、**BluetoothでPCに接続できる**機械ないかなあ～～～～～。

![36661.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/356208/08259a15-0326-487c-28ce-a2bf0a0bba01.png)


* あった～～～～～～！！！！。ジョイコンで傾き検知が可能。ついでにレッグバンド(リングフィットアドベンチャーの付属品)でちょうど足につけられる。(レッグバンドはなくてもどうにでもなります)

というわけでジョイコンを用いてストリートビューの世界を歩き回ろうと思います。

## 方針

joyコンとの連携のためにライブラリが充実しているUnityを使用することは決定。ただしストリートビューを利用するためにはブラウザからHTMLファイルとして開く必要があるらしい。よって、**Unityからブラウザを開く**+**開いたページをSeleniumで操作する**という方針に決定。

以下の二つから構成される。

* 表示するページ(HTML+javascript)
  * GoogleMapsJavaScriptAPIを用いてグーグルストリートビューの表示を行う。
  * 「ボタンを押すと前進する」という機能だけ持つ。
* Unity側
  * selenium+chromeDriverを用いてページを開く。
  * ジョイコンの傾きを検知し、歩数をカウントする。歩くたびにwebページのボタンを押す。


## 表示するページ

参考サイト: 

GoogleStreetViewが勝手に動く!?GoogleMapAPIを使った自動再生プログラムを作ってみる。|株式会社アーティス
https://www.asobou.co.jp/blog/web/streetview

まずAPIキーを取得します。[このページ](https://nendeb.com/276)等を参考にしてGoogleMapsJavaScriptAPIの有効化とキーの取得を行ってください。googleのAPIには無料枠があり、月額200ドル分まで(およそ14000回ほど)無料で使えるようです。

表示するページは以下のように作成します。これはほとんど[参考サイト](https://www.asobou.co.jp/blog/web/streetview)で書かれたコードのままですが、ボタンを追加してそれを押すたびに(5回に1回)移動する、というように書き換えました。また、[この記事](https://helog.jp/api/googlemaps-street-view-getpanoramabylocation/)を参考に多少座標がずれても修正できるようにしています。

```html
<html>
<head>
    <title>StreetView</title>
    <meta name="viewport" content="initial-scale=1.0">
    <meta charset="utf-8">
    <input type="button" onclick="proceed()" id="count" value="0歩">
    <style>
        html,
        body {
            height: 100%;
            margin: 0;
            padding: 0;
        }
 
        #street-view {
            height: 100%;
        }
    </style>
</head>
<body>
 
<div id="street-view"></div>


<script>
    const LIMIT = 300; // 移動回数の上限値
    const START_LAT_LNG = {lat: 任意の緯度, lng: 任意の経度}; // 開始地点の緯度、経度
    //const START_LAT_LNG = {lat: 35.1010033, lng: 139.0779688}; // 例
    const START_HEADING = 180; // 開始時の方角
    let panorama;
    isProceed = false;
    let count = 0;
    let m = 0;
    let n = 5; // 何歩ごとに1進むか。要検討。少ない方が嬉しいが無料枠に注意
    let buttonText = document.getElementById('count');
    function initMap() {
        let Links, count = 0, timer_id;
         panorama = new google.maps.StreetViewPanorama(
            document.getElementById('street-view'), {
                position: START_LAT_LNG,
                pov: {
                    heading: START_HEADING,
                    pitch: 0
                },
                zoom: 1
            }); // 
        // 座標の微妙なズレに補正を加える
        svs = new google.maps.StreetViewService();
        svs.getPanoramaByLocation(START_LAT_LNG, 50, function(result, status) {
            if (status == google.maps.StreetViewStatus.OK){
                    panorama.setPosition(result.location.latLng);
            }
        });
        Links = panorama.getLinks();
 


    }

    function proceed()
    {
        m++;
        if(m >= n)
        {
            m = 0;
            if (panorama.getStatus() == "OK") {
                Links = panorama.getLinks();
                if (count > LIMIT) {
                    alert('stop');
                    return false;
                }
                let target = 0;
                if (Links.length >= 4) {
                    target = Math.floor(Math.random() * Links.length);
                } else {
                    let val = 360;
                    let currentPov = panorama.getPov();
                    Links.forEach(function (element, index) {
                        let ans = Math.abs(currentPov.heading - element.heading);
                        if (val > ans) {
                            val = ans;
                            target = index;
                        }
                    });
                }
                panorama.setPov({
                    heading: Links[target].heading,
                    pitch: 0
                });
                panorama.setPano(Links[target]['pano']);

            };            
        }

        count++;
        buttonText.value = String(count) + "歩";
    }
</script>

<script src="https://maps.googleapis.com/maps/api/js?key=取得したキー&callback=initMap"
        async defer></script>
</body>
</html>
```

## Unity側の設定

### ジョイコン検知のための設定

以下のサイトを参考にしました。

[【Unity】Nintendo Switch の Joy-Con を使用する方法](https://baba-s.hatenablog.com/entry/2017/10/24/130000)

[【Unity】Nintendo Switch の Joy-Con のジャイロ・加速度・傾きの値を取得したり、振動させたりすることができる「JoyconLib」紹介](https://baba-s.hatenablog.com/entry/2017/11/12/090000)

上の内容に従いProject Settingsから入力の変更、二つのライブラリの導入を行いました。

### selenium+chromedriverの導入

参考サイト： [UnityのC#からSelenium経由でChromeをゴニョゴニョする](https://qiita.com/su10/items/2abbbd6daf1eb16f2c6a)

上の内容に従い、SeleniumとChromeDriverの導入を行いました。ChromeDriverはバージョンに注意してください。

### コード

Start()時にChromeDriverを用いてブラウザを起動。歩行を感知するたびにボタンを押す。

歩数の取得をどうやるかは一考の余地がありますが、とりあえず以下のように設定しました。

```cs
using UnityEngine;
using System;
using System.Collections;
using System.Collections.Generic;

using OpenQA.Selenium;
using OpenQA.Selenium.Chrome;


public class JoyConWalker : MonoBehaviour
{
    private ChromeDriver _driver;
    private List<Joycon>    m_joycons;
    private Joycon          m_joyconL;
    private Joycon          m_joyconR;
    private Joycon.Button?  m_pressedButtonL;
    private Joycon.Button?  m_pressedButtonR;
    int count = 0;
    float LlastUpper = 0;
    float LlastLower = 0;
    float Llast = 0;
    bool LisUp = true;
    float theta = 0.08f; //閾値。設定は微妙なので要調整。
    void Start()
    {
        var path = Application.streamingAssetsPath;
        _driver = new ChromeDriver(path);

        _driver.Navigate().GoToUrl(Application.dataPath + "/StreetView.html");

        m_joycons = JoyconManager.Instance.j;
        if ( m_joycons == null || m_joycons.Count <= 0 ) return;
        m_joyconL = m_joycons.Find( c =>  c.isLeft );
        m_joyconR = m_joycons.Find( c => !c.isLeft );
    }

    void Update()
    {
        var orientationL = m_joyconL.GetVector();
        var current = orientationL.z;
        if(LisUp)
        {
            if(current >= Llast)
            {
                LlastUpper = current;
            }
            else
            {
                if(LlastUpper - LlastLower >= theta)
                {
                    Proceed();
                }
                LisUp = false;
                LlastLower = current;
            }
        }
        else
        {
            if(current <= Llast)
            {
                LlastLower = current;
            }
            else
            {
                if(LlastUpper - LlastLower >= theta)
                {
                    Proceed();
                }
                LisUp = true;
                LlastUpper = current;
            }            
        }
        Llast = current;


    }

    void Proceed()
    {
        count++;
        Debug.Log(count);
        _driver.FindElement(By.Id("count")).Click();

    }

    void OnDestroy()
    {
        _driver.Dispose();
    }
}
```

### Unityで組み立てる

シーンに空のゲームオブジェクトを配置して↑のcsファイルと、JoyConManager.csをつける。htmlファイルをAssets下に配置。

これで完成。起動し、ページが開いたら左のジョイコンをレッグバンドに入れて足踏みをします。

座標は自分の好きな観光地である熱海に設定。歩数表示が左上に出ています。ステッパーで足踏みしてる間、延々と熱海をさまようことができます。

![rapture_20201125221939.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/356208/36c47691-12f5-fad9-2abf-931662d88338.jpeg)
Asset下の構成はgitにアップロードしておきました。

https://github.com/NNNiNiNNN/JoyCon_StreetView


## 課題

#### 初期座標の決定が難しいこと
現在は完全に固定した座標からスタートして、(アルゴリズム的に)ほとんど固定されたルートを通るようになっています。座標の決定を完全にランダムにしてから最寄りのデータを持つ座標に補正する、というやり方も試してみましたが、[この手段](https://helog.jp/api/googlemaps-street-view-getpanoramabylocation/)で得られるパノラマ座標は「ストリートビューとして移動できるパノラマ座標」に限らないため、移動先を失うパターンが多くありました。apiも他にどのようなものが使えるのか把握していないため、自分がすぐ直すのは難しそうです。理想を言えば**初期地点と目的地点を手動で決定**して**自動でルート案内する**のがいいと思います。実装も多分可能ですが、今回はなしで。

#### 歩行の検知
単純にジョイコンの傾きの差を取ればいいだけ、だとは思うのですがいまいち最適化できていません。そもそもxyzのどれを取ればいいのかもはっきり決め切れていません。現在は動きの閾値を小さめに設定することでだいたいの動きを無理やり取る形にしています。

#### ジョイコンの接続エラー？
一度接続したジョイコンは、その接続が途切れた後はPCを再起動しないと再接続できなくなることがあります。理由も再現性もよくわかっていません。

## あとがき

課題は多くありますが、よく見るフィットネスバイク+ストリートビュー....の亜種を自力で作れたことが嬉しいです。ジョイコンとかgoogleのAPIとか、見渡せば色々便利なものがあるんですねえ。苦戦した部分もありましたが、最終的には簡単な内容で仕上がったのもよかったです。

作ったもので運動するかはわかりません。

## 参考

[GoogleStreetViewが勝手に動く!?GoogleMapAPIを使った自動再生プログラムを作ってみる。|株式会社アーティス](https://www.asobou.co.jp/blog/web/streetview)

[Googleマップストリートビューで目的地に最寄りのストリート位置（座標情報）を取得する方法](https://helog.jp/api/googlemaps-street-view-getpanoramabylocation/)

[【Unity】Nintendo Switch の Joy-Con を使用する方法](https://baba-s.hatenablog.com/entry/2017/10/24/130000)

[【Unity】Nintendo Switch の Joy-Con のジャイロ・加速度・傾きの値を取得したり、振動させたりすることができる「JoyconLib」紹介](https://baba-s.hatenablog.com/entry/2017/11/12/090000)

[UnityのC#からSelenium経由でChromeをゴニョゴニョする](https://qiita.com/su10/items/2abbbd6daf1eb16f2c6a)