Androidの地磁気センサーを用いて地図をヘディングアップ表示する

# はじめに

Androidの地磁気センサーを用いて地図をヘディングアップ表示するだけの話なのですが、そこら辺のサンプルはちょっと問題があったので改良した話です。

# そこら辺のサンプル

回転行列を元に SensorManager.getOrientation() でオイラー角を得て、その values[0] を使うのが、そこら辺のサンプルで転がってます。

おおむねうまく動くのですが、水平方向に向けたとき（画面が垂直になったとき）にグルングルン回ってしまいます。

また、それより高く掲げると反転してしまいます。

![図解](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/104360/0b6d9e46-c2d3-6cb9-e8b8-009a66640cbd.png)

# 改良

回転行列から何とかできるんじゃないかと思って試行錯誤したところ、何とかなりました。

回転行列Rを
```
/  R[ 0]   R[ 1]   R[ 2]   \
|  R[ 3]   R[ 4]   R[ 5]   |
\  R[ 6]   R[ 7]   R[ 8]   /
```
としたとき、atan2(R[2], R[5]) が端末の回転を考慮しない方位、atan2(R[6], R[7]) が端末の回転になり、これらを足すと方位になります。

（数学的にちゃんと説明できないのですが、回転行列の任意の2値をatan2に入れるプログラムを書いて、「コレがソレっぽい」という調査の仕方をしましたｗ）

# サンプルプログラム

以下、サンプルプログラムを示します。Maps SDK for Android を組み込むと API Keyの扱い（説明）が面倒なので、テキストで北を指す矢印を表示するサンプルにしました。

```kotlin:MainActivity.kt
package com.example.headingup

import android.content.Context
import android.hardware.Sensor
import android.hardware.SensorEvent
import android.hardware.SensorEventListener
import android.hardware.SensorManager
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.util.TypedValue
import android.view.Gravity
import android.widget.LinearLayout
import android.widget.TextView
import kotlin.math.atan2

class MainActivity : AppCompatActivity() , SensorEventListener {
    private lateinit var sensorManager: SensorManager
    private lateinit var textView: TextView

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

        // 方向表示のテキストビュー
        textView = TextView(this)
        textView.text = "↑"
        textView.layoutParams =
            LinearLayout.LayoutParams(
                LinearLayout.LayoutParams.WRAP_CONTENT,
                LinearLayout.LayoutParams.WRAP_CONTENT
            )
        textView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 300f)
        textView.gravity = Gravity.CENTER
        (textView.layoutParams as LinearLayout.LayoutParams).weight = 1f
        setContentView(textView)

        // センサマネージャ
        sensorManager = getSystemService(Context.SENSOR_SERVICE) as SensorManager
    }

    override fun onResume() {
        super.onResume()

        // センサマネージャ リスナ登録
        sensorManager.getDefaultSensor(Sensor.TYPE_ROTATION_VECTOR)?.also { rotationVector ->
            sensorManager.registerListener(
                this,
                rotationVector,
                SensorManager.SENSOR_DELAY_UI,
                0
            )
        }
    }

    override fun onSensorChanged(event: SensorEvent) {
        if (event.sensor.type == Sensor.TYPE_ROTATION_VECTOR) {
            val dRot = windowManager.defaultDisplay.rotation * 90  // 画面回転 0,90,180,270度

            // 回転行列に変換
            val rotationMatrix = FloatArray(9)
            SensorManager.getRotationMatrixFromVector(rotationMatrix, event.values)

            // 回転行列から北の向きを取得
            val rad = atan2(rotationMatrix[2], rotationMatrix[5]) -
                    atan2(rotationMatrix[6], rotationMatrix[7]) + Math.PI
            // Maps SDK for Android の bearing値に変換
            val bearing = Math.toDegrees(rad).toFloat() + dRot
            // textviewだと逆なのでマイナスに
            textView.rotation = -bearing
        }
    }

    override fun onAccuracyChanged(sensor: Sensor, accuracy: Int) {
    }
}
```

# 欠点

端末を斜め上にかざす向きで左右にひねったときの動きが不穏です。さらに、天頂にかざす向きにするとグルングルン回転してしまいます。ただ、地図を見るときにその角度にしないと思うので実用上は問題ないでしょう。