THETA X で衛星測位の結果を利用する方法

## はじめに

リコーの @KA-2 です。

弊社では[RICOH THETA](https://theta360.com/ja/)という全周囲360度撮れるカメラを出しています。
<B>RICOH THETA V</B>, <b>RICOH THETA Z1</b>, <b>RICOH THETA X</b>は、OSに<B>Android</B>を採用しています。Androidアプリを作る感覚でTHETAをカスタマイズすることもでき、そのカスタマイズ機能を「プラグイン」と呼んでいます（詳細は本記事の末尾を参照）。

RICOH THETA Xは、本体のみで衛星測位が行えるようになりました。
今回は、THETAプラグインから、この測位結果を取得し利用する方法の初級編を紹介します。

## 今回作成したサンプルの概要

RICOH THETA Xでは、Androidに予め用意されているサービスを利用して、緯度、経度などが取得できるのですが、取得できる情報の中に、時刻があります。

RICOH THETA X は、これまでのTHETAと同じように、以下２通りの方法で自動の時刻合わせが行えますが、

- スマートフォンと無線LANまたはBluetooth(BLE)で接続されているとき
- 外部ネットワークに接続されているとき

使い方によっては、常に上記いずれかの状態が満たされているとは限りません（常に条件を満たさなくても十分な事象ではあります）。
適度に条件を満たしていても、「今、時刻合ってる！」と実感しにくかったりします。
それではと、手動で本体の設定画面から時刻を合わせるとき、秒まで指定できなかったりします。

そこで、屋外にいるとき「よし、時刻あってるな」と気軽に実感が持てるような小道具があっても便利かもしれないと思い、サンプルコード作成ついでに「第三の手段」を用意してみました。

この記事で紹介する事例は、THETA PLUG-IN STORE に公開してあります。
https://pluginstore.theta360.com/plugins/skunkworks.gnsstimeadjuster/

[![ストア画像案_960x520.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/265455/99718396-d747-5192-c0bd-2ffd6fbcf213.png)](https://pluginstore.theta360.com/plugins/skunkworks.gnsstimeadjuster/)


時刻合わせだけでなく、測位した座標を刻々更新もしますので、「測位ちゃんとできているかな？」という心配性な方にも向いています。
コードのことはわからなくても、おもしろツールとしてお使いいただければ幸いです。



## ソースコード

[こちらのプロジェクト一式](https://github.com/theta-skunkworks/theta-plugin-time-adjuster-via-gnss)を参照してください。

### ファイル構成

[THETA Plug-in SDK](https://github.com/ricohapi/theta-plugin-sdk)をベースに作業しています。
現在では、このサンプルを作成したときよりもバージョンが上がっていますので、新しいSDKをベースに作業されることをおすすめします。

新規作成、または、変更を加えたファイルは以下のとおりです。
パッケージ名をTHETA Plug-in SDKと変えているのでフォルダ構成が少し異なる点はご了承ください。

```
theta-plugin-time-adjuster-via-gnss\app
└src\main
　├assets　　　　　　　 // ベースとしたプロジェクトのままです。
　└java\com\theta360
　　　├pluginapplication
　　　│　├model　　 　　　// ベースとしたプロジェクトのままです。
　　　│　├network　 　　　// HttpConnector.javaは過去記事と同じメソッド追加をしています。
　　　│　└task　　　　　　//　SetDateTimeZoneTask.javaはWebAPIで時刻設定するタスクです。
　　　│　　　　　　　　　　//　以下は未使用のため削除しました。
　　　│　　　　　　　　　　//　- TakePictureTask.java
　　　└gnsstimeadjuster　// MainActivity.java　はプラグインの全体構造が記述されています。
　　　　　　　　　　　　　//　以下は新規追加したファイルです。ダイアログ毎に作成しています。
　　　　　　　　　　　　　//　AdjustCompletedDaialog.java
　　　　　　　　　　　　　//　DateTimeNotAutoDaialog.java
　　　　　　　　　　　　　//　PositionInfoAddOffDaialog.java
```




### パーミッションを定義する。

```AndroidManifest.xml
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
```
上の2行は元々設定されていたパーミッションです。
下の2行を位置情報取得のために追加しました。


### MainActivity(PluginActivity)にLocationListener をimplementsする

今回は、LocationManagerを利用して衛星測位の結果をプラグインの処理で利用しました。
以下のように、MainActivityにLocationListenerをimplementsする記述をして、LocationListenerクラスのimport文も追加すると

```MainActivity.java
import android.location.LocationListener;
　～省略～
public class MainActivity extends PluginActivity implements LocationListener {
```

Android Studioのエディタでは、行全体に赤く波線が付くと思います。
その後、行を選択すると左端にヒントがでて、「Implements methods」が選択できる状態となります。
クリックすると、記述が必要なメソッドのスケルトンがファイル末尾に追加されます。
追加されるのは以下4つのメソッドです。

```MainActivity.java
    @Override
    public void onLocationChanged(Location location) {

    }

    @Override
    public void onStatusChanged(String provider, int status, Bundle extras) {

    }

    @Override
    public void onProviderEnabled(String provider) {

    }

    @Override
    public void onProviderDisabled(String provider) {

    }
```

[ソースコード](https://github.com/theta-skunkworks/theta-plugin-time-adjuster-via-gnss/blob/main/app/src/main/java/skunkworks/gnsstimeadjuster/MainActivity.java#L187-L270)を見て頂くと判るのですが、今回自身の処理を記述したのはonLocationChanged()のみです。
名称からも判る通り、位置が変わったときにこのメソッドが呼ばれます。カメラを動かさなくても測位の揺らぎがありますので、刻々とこのメソッドが呼ばれることとなります。（繰り返し呼び出されるときの、最短時間と最小移動距離は初期化の時に指定できます。後半で説明します。）

onLocationChanged()には、引数にLocationクラスの入力があります。クラスの説明は[こちら](https://developer.android.com/reference/android/location/Location)を参照してください。

この入力から、緯度・経度・時刻を以下のコードで取得できます。

- 緯度：location.getLatitude();
- 経度：location.getLongitude();
- 時刻：location.getTime();

あとは所望の処理を記述するだけです。
Locationクラスには上記以外の情報を取得できるメソッドも用意されていますので用途に応じて利用してください。


### 初期化処理を記述する。

作業順に記述した都合から、位置情報の利用箇所を先に書いてしましましたが、意図通りに位置情報を得るためには初期化が必要となります。

初期化するのはLocationManagerクラスの以下インスタンスです。

```MainActivity.java
    LocationManager locationManager;
```

初期化する箇所はonCreateとしています。
具体的な初期化処理はlocationStart()というメソッドにまとめてあります。

事前にパーミッションのチェックが必要なようです。実行時にパーミッションが与えられてなかった場合、パーミッションを与えるシステムダイアログが表示されるようにしています。
RICOH THETA Xでは、THETA PLUG-IN STOREからプラグインをインストールすると、必要なパーミッションが自動で与えられるので不要なコードなのですが、一般スマートフォンに併せAndroidアプリケーションの作法として記述してあります。

```MainActivity.java
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        //位置情報取得に関するコード：パーミッションの有無チェック
        if (ContextCompat.checkSelfPermission(this,
                Manifest.permission.ACCESS_FINE_LOCATION) !=
                PackageManager.PERMISSION_GRANTED) {
            ActivityCompat.requestPermissions(this,
                    new String[]{Manifest.permission.ACCESS_FINE_LOCATION,},
                    1000);
        }
        else{
            locationStart();
        }
    }
```

locationStart()の処理内容は以下のとおり。

```MainActivity.java
    //位置情報取得開始処理
    private void locationStart(){
        Log.d(TAG,"locationStart()");

        // LocationManager インスタンス生成
        locationManager =
                (LocationManager) getSystemService(LOCATION_SERVICE);

        if (locationManager != null && locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)) {
            Log.d(TAG, "location manager Enabled");

        } else {
            Log.d(TAG, "not gpsEnable, startActivity");

            //ダイアログでOK操作したらfinish()する。onPause()呼ばれる。
            PositionInfoAddOffDaialog dialog = new PositionInfoAddOffDaialog();
            dialog.show( getSupportFragmentManager(), "position_info_add_off_dialog" );
        }

        //パーミッションのチェックをしないとrequestLocationUpdates()がエラーになる
        if (ContextCompat.checkSelfPermission(this,
                Manifest.permission.ACCESS_FINE_LOCATION) !=
                PackageManager.PERMISSION_GRANTED) {
            ActivityCompat.requestPermissions(this,
                    new String[]{Manifest.permission.ACCESS_FINE_LOCATION,}, 1000);

            Log.d(TAG, "checkSelfPermission false");
            return;
        }

        locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER,
                //1000, 50, this);
                200, 0, this);

    }
```

処理末尾の部分が、onLocationChanged()が繰り返し呼び出されるときの、最短時間と最小移動距離の指定です。
この例では200msec、0メートルを指定しています。コメントアウトした上の行では最短時間1000msec,最小移動距離50メートルとする場合の例です。



### THETA Xはダイアログでお知らせができる

前項で説明したlocationStart()の中の以下部分は、位置情報付与が有効であるかチェックしています。

```
locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)
```
位置情報付与が無効であった場合、ダイアログを表示してその主旨をユーザーに通知
ダイアログの処理ではOKボタンしか押せなくなっており、押すとプラグインを終了しています。

他にも同様の処理をしているのが以下です。
onResume()の中で、日時の自動更新が有効になっていないと、プラグインからWeb APIを利用して時刻の更新ができなくなるため、ダイアログで主旨を通知し、ダイアログでOKボタンを押すとプラグインを終了しています。

```MainActivity.java
    @Override
    protected void onResume() {
　　　　～省略～
        //Check "Date/time setting" -> "auto"
        int val = android.provider.Settings.Global.getInt(getContentResolver(), android.provider.Settings.Global.AUTO_TIME, 0);
        Log.d(TAG, "Check Date/time settings=" + String.valueOf(val));
        if (val==0) {
            //ダイアログでOK操作したらfinish()する。onPause()呼ばれる。
            DateTimeNotAutoDaialog dialog = new DateTimeNotAutoDaialog();
            dialog.show( getSupportFragmentManager(), "date_time_notset_dialog" );
        }
　　　　～省略～
    }
```

RICOH THETA Xではタッチディスプレイを搭載したことにより、このような小技が使えます。
ダイアログには、以下のように画像も表示できますので、プラグインが終了したあと何を設定すればよいかを促すこともできます。

![ダイアログ画像.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/265455/7803ba73-58e7-40aa-d2aa-72087d1d7990.png)

位置情報の利用とは関係ないポイントですが、紹介しておきました。


### 位置情報取得の停止

位置情報の取得は、停止を指示するまで動き続けます。
本プラグインでは、プラグイン終了時にonPauseで以下処理を呼び出し、位置情報の取得を停止しています。

```MainActivity.java
    //位置情報取得停止処理
    private void locationStop(){
        if (locationManager != null ) {
            Log.d(TAG, "locationManager.removeUpdates()");
            locationManager.removeUpdates(this);
        }
    }
```


### 時刻合わせにはWeb APIを利用

RICOH THETAでは、アプリケーションから一般的な方法でシステムに時刻をセットすることができません。（アプリケーションに、直接システムに関する操作をする権限がありません）
そこで、[「dateTimeZone」](https://github.com/ricohapi/theta-api-specs/blob/main/theta-web-api-v2.1/options/date_time_zone.md)のWeb APIを利用して時刻をセットしています。

タスクを呼び出しているのは先に説明したonLocationChanged()の中です。
タスクを呼び出す前に、時刻のフォーマットを整えています。

```MainActivity.java
            //THETA用のフォーマットに成形してから日時設定を更新
            // format:YYYY:MM:DD hh:mm:ss+(-)hh:mm
            // hh is in 24-hour time, +(-)hh:mm is the time zone.
            String thetaDateTimeZone = strGnssDateTimeZone.replaceFirst("([0-9][0-9])$", ":$1");
            new SetDateTimeZoneTask(thetaDateTimeZone).execute();
```

タスクの中は過去機種と変わりない作りですので説明を割愛します。



## おわりに

画面表示に関わる細かなことの説明は省略しましたが、今回の記事にした内容で、衛星測位の緯度・経度をRICOH THETA Xで利用できるようになります。
この事例では、さらに時刻情報も利用して、プラグインからWebAPIを利用して本体の時刻合わせまで行っています。

緯度・経度が判ると、地図に重ねてみたくなりますよね？
THETAは Googleの開発者サービスを利用できないのでGoogleマップをTHETA PLUG-IN SOTREから配布可能な形で利用できないのすが、「[OpenStreetMap](https://openstreetmap.jp/#zoom=5&lat=38.06539&lon=139.04297&layers=000B)」であれば利用することができます。
次回は、OpenStreetMapが提供している地図の利用方法ついて記事を書きたいと思います。



## RICOH THETAプラグインパートナープログラムについて

THETAプラグインをご存じない方は[こちら](https://github.com/ricohapi/theta-api-specs/blob/main/core/products/theta-plugin.md)をご覧ください。
パートナープログラムへの登録方法は[こちら](https://qiita.com/mShiiina/items/55d98f366e650ca42251)にもまとめてあります。
Qiitaの[RICOH THETAプラグイン開発者コミュニティ TOPページ「About」](https://qiita.com/organizations/theta-plugin)に便利な記事リンク集もあります。
興味を持たれた方は[Twitter](https://twitter.com/thetaplugin)のフォローと[THETAプラグイン開発者コミュニティ(Slack)](https://join.slack.com/t/thetadeveloper/shared_invite/enQtNzcxODI2MTk2ODg0LWY5YmU2ZTIxNGYwNzlmNWZjMWMyMWFlMzY3ZmQ3NGU3ZjZiZjI0Njk4MGIxMDU5NDJiMzQ4Y2FkNDEzYmFlNGU)への参加もよろしくおねがいします｡