marunomaruno-memo

marunomaruno-memo

[Android] センサー (3) 方位

2012年01月14日 | Android
センサー (3) 方位
================================================================================

■ 方位を取得する

方位は、それ専用のセンサーがあるのではなく、加速度センサーと磁気センサーの値から
算出する。この 2 つのセンサーを使うこと以外は、いままでのサンプルと同じである。

Sensor.TYPE_ORIENTATION は、非推奨になっているので、これは使わない。


◆ アクティビティのクラス。

□ OrientationSensor01Activity.java
---
package jp.marunomaruno.android.orientationsensor;

import android.app.Activity;
import android.hardware.Sensor;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.os.Bundle;
import android.widget.TextView;
import jp.marunomaruno.android.orientationsensor.R;

/**
 * 方位を表示する。
 * @author marunomaruno
 * @version 1.0, 2011-12-01
 */
public class OrientationSensor01Activity extends Activity {

    // TextView
    private TextView orientationTextView; // 方位センサーの表示域

    private SensorManager sensorManager;
    private SensorEventListener sensorEventListener; 
                    // onPause()が呼ばれたときに、すべてのリスナーを解除するため

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

        orientationTextView = (TextView) findViewById(R.id.sensor_value);
        sensorManager = (SensorManager) getSystemService(SENSOR_SERVICE);
    }

    @Override
    protected void onResume() {
        super.onResume();

        sensorEventListener = new OrientationSensorListener(orientationTextView); // (1)

        // 加速度センサーを取得して登録する
        for (Sensor sensor : sensorManager
                .getSensorList(Sensor.TYPE_ACCELEROMETER)) {
            sensorManager.registerListener(sensorEventListener, sensor,
                    SensorManager.SENSOR_DELAY_NORMAL);
        }

        // 磁気センサーを取得して登録する
        for (Sensor sensor : sensorManager
                .getSensorList(Sensor.TYPE_MAGNETIC_FIELD)) {
            sensorManager.registerListener(sensorEventListener, sensor,
                    SensorManager.SENSOR_DELAY_NORMAL);
        }
    }

    @Override
    public void onPause() {
        super.onPause();

        // すべてのリスナーを解除する
        sensorManager.unregisterListener(sensorEventListener);
    }
}
---

(1) 方位センサーのリスナー・オブジェクトを生成する

    sensorEventListener = new OrientationSensorListener(orientationTextView); // (1)

そして、このオブジェクトを、加速度センサー、磁気センサーに登録しておく。


◆ センサーのリスナーのクラス。

センサーに対するリスナーのクラス。
これは、加速度センサーと磁気センサーの 2 つのセンサーに登録される。
方位は、この 2 つのセンサーの値を基に取得する。



□ AcceleraterSensorListener.java
---
package jp.marunomaruno.android.orientationsensor;

import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.widget.TextView;
import jp.marunomaruno.android.orientationsensor.R;

/**
 * 方位センサーのリスナー
 * @author maruno
 * @version 1.0, 2011-12-01
 * @since 1.0
 */
public class OrientationSensorListener implements SensorEventListener {

    private TextView textView; // 表示域
    private float[] accelerometerValues; // 加速度センサーの値    // (1)
    private float[] magneticFieldValues; // 磁気センサーの値    // (2)

    public OrientationSensorListener(TextView textView) {
        this.textView = textView;
    }

    @Override
    public void onSensorChanged(SensorEvent event) {

        float[] orientationValues = new float[3]; // 方位の値    // (3)

        switch (event.sensor.getType()) {
        case Sensor.TYPE_ACCELEROMETER: // 加速度センサーが変化したとき
            accelerometerValues = clone(event.values);    // (4)
            break;

        case Sensor.TYPE_MAGNETIC_FIELD: // 磁気センサーが変化したとき
            magneticFieldValues = clone(event.values);    // (5)
            break;
        }

        if (accelerometerValues != null && magneticFieldValues != null) {

            float[] inRotationMatrix = new float[16]; // 入力用の回転行列  // (6)
            float[] outRotationMatrix = new float[16]; // 出力用の回転行列 // (7)

            SensorManager.getRotationMatrix(inRotationMatrix, null,
                    accelerometerValues, magneticFieldValues);           // (8)
            SensorManager.remapCoordinateSystem(inRotationMatrix,
                    SensorManager.AXIS_X, SensorManager.AXIS_Z,
                    outRotationMatrix);                                  // (9)
            SensorManager.getOrientation(outRotationMatrix, orientationValues); // (10)

            // TextViewに表示する
            textView.setText("");

            // ラジアンを表示
            textView.append(String
                    .format(textView.getResources().getString(
                            R.string.sensor_value_format), textView
                            .getResources().getString(R.string.orientation),
                            orientationValues[0], orientationValues[1],
                            orientationValues[2], textView.getResources()
                                    .getString(R.string.OrientationUnit)));
            textView.append("¥n");

            // 度を表示
            textView.append(String.format(textView.getResources().getString(
                    R.string.sensor_value_format), textView.getResources()
                    .getString(R.string.orientation),
                    toOrientationDegrees(orientationValues[0]),
                    toOrientationDegrees(orientationValues[1]),
                    toOrientationDegrees(orientationValues[2]), textView
                            .getResources()
                            .getString(R.string.OrientationUnit2)));
            textView.append("¥n");

            // 方位を表示
            textView.append(String.format(textView.getResources().getString(
                    R.string.orientation_value_format),
                    toOrientationString(orientationValues[0]),
                    toOrientationDegrees(orientationValues[0]), textView
                            .getResources()
                            .getString(R.string.OrientationUnit2)));
            textView.append("¥n");
        }
    }

    /**
     * 配列の深いコピーを行う。
     * ※Android2.2 の Arrays クラスには copyOf() メソッドがない。
     * @param source ソースの配列
     * @return 深いコピーを行った結果
     */
    private float[] clone(float[] source) {                        // (11)
        float[] target = new float[source.length];
        for (int i = 0; i < target.length; i++) {
            target[i] = source[i];
        }
        return target;
//        return Arrays.copyOf(sourse, sourse.length);    // 本来はこれでOK
    }

    /**
     * 方位の角度にする。方位の角度は、0以上360未満。
     * @param rad ラジアン。ただし、-π以上π未満。
     * @return 方位の角度
     */
    private float toOrientationDegrees(float rad) {                // (12)

        return (float) (rad >= 0 ? Math.toDegrees(rad) : 360 + Math
                .toDegrees(rad));    // (13)
    }

    /**
     * 方位をあらわす文字列を取得する。 
     *         北 0 rad 
     *         東 π/2 rad 
     *         南 π rad 
     *         西 -π/2 rad
     * @param azimuth 方位
     * @return 方位をあらわす文字列
     */
    private String toOrientationString(float azimuth) {            // (14)
        double[] ORIENTATION_RANGE = {
                -(Math.PI * 3 / 4), // 南
                -(Math.PI * 1 / 4), // 西
                +(Math.PI * 1 / 4), // 北
                +(Math.PI * 3 / 4), // 東
                // Math.PI, // 南
        };    // (15)

        for (int i = 0; i < ORIENTATION_RANGE.length; i++) {
            if (azimuth < ORIENTATION_RANGE[i]) {
                return textView.getResources().getStringArray(
                        R.array.orientationNames)[i];    // (16)
            }
        }

        return textView.getResources().getStringArray(R.array.orientationNames)[0];
    }

    @Override
    public void onAccuracyChanged(Sensor sensor, int accuracy) {
        assert true; // 何もしない
    }
}
---

(1)(2) 加速度センサー・磁気センサーの値を保持

それぞれ、3 次元の値なので、配列で保持する。

    private float[] accelerometerValues; // 加速度センサーの値    // (1)
    private float[] magneticFieldValues; // 磁気センサーの値    // (2)


(3) 方位の値

方位も、3 次元の値になるので、これを確保する変数を用意する。

    float[] orientationValues = new float[3]; // 方位の値    // (3)


(4)(5)(6)(7) 加速度センサー・磁気センサーの値から方位を取得するための準備

計算に使うので、配列の深いコピーで、値をコピーする。

    accelerometerValues = clone(event.values);    // (4)
    magneticFieldValues = clone(event.values);    // (5)

※Android2.2 では、深いコピーを行う Arrays.copyOf() メソッドがないので、自分で実
装する。本来は、
    accelerometerValues = Arrays.copyOf(event.values, event.values.length); // (4)
    magneticFieldValues = Arrays.copyOf(event.values, event.values.length); // (5)
でよい。

4x4 の回転行列を用意する。実際には、要素数 16 の 1 次元配列で作る。

    float[] inRotationMatrix = new float[16]; // 入力用の回転行列    // (6)
    float[] outRotationMatrix = new float[16]; // 出力用の回転行列    // (7)


(8)(9)(10) 加速度センサー・磁気センサーの値から方位を取得する

この一連の操作によって、accelerometerValues、magneticFieldValues の値は、最終的
に orientationValues として方位が取得できる。

加速度センサー・磁気センサーの値から、回転行列 inRotationMatrix を取得する。

    SensorManager.getRotationMatrix(inRotationMatrix, null,
            accelerometerValues, magneticFieldValues);           // (8)

座標系を回転行列 inRotationMatrix を使って変換し、outRotationMatrix に入れる。
今回は、画面をみるときに、たて画面で、垂直に近い形(手前に傾いている)で見ること
を想定。
画面の傾き具合は、Display.getRotation() を使って取得するが、この部分はかなり複雑
になるので、今回はこのまま。
※他のサイトでは、引数(..., SensorManager.AXIS_X, SensorManager.AXIS_Y, ...) が
よいともあるが、わたしの実機では下記の引数の方がよい感じ。

    SensorManager.remapCoordinateSystem(inRotationMatrix,
            SensorManager.AXIS_X, SensorManager.AXIS_Z,
            outRotationMatrix);                                  // (9)

回転行列 outRotationMatrix からデバイスの向き orientationValues を取得する。

    SensorManager.getOrientation(outRotationMatrix, orientationValues);  // (10)

※回転行列などの詳細は現在調査中。


・SensorManager クラスの static な取得系のメソッド
---
static float    getAltitude(float p0, float p)    
                大気圧と海面の気圧から高度(m)を取得

static void     getAngleChange(float[] angleChange, float[] R, float[] prevR)  
                2 つの回転行列の間の角度を取得

static float    getInclination(float[] I)    
                getRotationMatrix() で取得した傾き行列から傾きを取得

static float[]  getOrientation(float[] R, float[] values)    
                回転行列からデバイスの向きを取得

static void     getQuaternionFromVector(float[] Q, float[] rv)    
                回転ベクトルから 4 元数を取得(ヘルパー)

static boolean  getRotationMatrix(float[] R, float[] I, float[] gravity, 
                                                        float[] geomagnetic)   
                回転行列を取得

static void     getRotationMatrixFromVector(float[] R, float[] rotationVector) 
                回転ベクトルから回転行列を取得(ヘルパー)

static boolean  remapCoordinateSystem(float[] inR, int X, int Y, float[] outR) 
                座標系を回転行列を使って変換
---
※ただし、使い方(訳も)についてはまだ、不明な部分が大半。


・remapCoordinateSystem() で使う定数
---
int     AXIS_MINUS_X
int     AXIS_MINUS_Y
int     AXIS_MINUS_Z
int     AXIS_X
int     AXIS_Y
int     AXIS_Z
---


(11) 配列の深いコピーを行う

    private float[] clone(float[] source) {                        // (11)

※Android2.2 では、まだ、これを実現する Arrays.copyOf() メソッドがないので、自分
で実装する。


(12)(13) 方位の角度にする。方位の角度は、0以上360未満。

    private float toOrientationDegrees(float rad) {                // (12)

rad は、-π ~ +πの範囲なので、これを度に変換し、値の範囲が 0 ~ 360 になるよう
に調整する。

    return (float) (rad >= 0 ? Math.toDegrees(rad) : 360 + Math
            .toDegrees(rad));    // (13)


(14)(15)(16) 方位をあらわす文字列を取得する。 

    private String toOrientationString(float azimuth) {            // (14)

真北、真東、真南、真西がそれぞれ 0, π/2, -π, -π/2 なので、北、東、南、西のそ
れぞれの範囲は、以下のようになる。
    北    -π/4 ~ π/4
    東     π/4 ~  3π/4
    南    -π   ~ -3π/4, 3π/4 ~ π
    西    -π/4 ~ π/4

    double[] ORIENTATION_RANGE = {
            -(Math.PI * 3 / 4), // 南
            -(Math.PI * 1 / 4), // 西
            +(Math.PI * 1 / 4), // 北
            +(Math.PI * 3 / 4), // 東
            // Math.PI, // 南
    };    // (15)

この範囲で、配列を作って、文字を割り当てる。

    return textView.getResources().getStringArray(
            R.array.orientationNames)[i];    // (16)

リソースでは、string-array 要素を使って、配列として、東西南北の文字列を持つよう
にしている。
    <string-array name="orientationNames">
        <item>@string/south</item>
        <item>@string/west</item>
        <item>@string/north</item>
        <item>@string/east</item>
    </string-array>

これは、getStringArray() メソッドで、ここで記述した順番の String[] 型の配列で値
が返ってくる。


■ レイアウト

センサー(1) のサンプルと同じ。

□ res/layout/main.xml
---
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    >
    
<!-- センサー値 -->    
<TextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="@string/orientation"
    android:id="@+id/sensor_value"
    />
    
</LinearLayout>
---


■ 文字列の定数

プログラム中で配列を使っているので、それに対する string-array 要素を設定している
ファイル arrays.xml も準備する。string.xml の中に入れてもよいが、配列として文字
列を 2 次的に利用しているので、別ファイルにした。

□ res/values/string.xml
---
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string name="app_name">OrientationSensor01</string>
    <string name="orientation">方位: (方位角(azimuth), ピッチ(Pitch), 回転(Roll))</string>
    <string name="OrientationUnit">ラジアン</string>
    <string name="OrientationUnit2">度</string>
    <string name="sensor_value_format">%1$s = (%2$.1f, %3$.1f, %4$.1f) (%5$s)</string>
    <string name="orientation_value_format">方位: %1$s (%2$.1f %3$s)</string>
    <string name="east">東(45 ~ 135)</string>
    <string name="west">西(225 ~ 315)</string>
    <string name="south">南(135 ~ 225)</string>
    <string name="north">北(0 ~ 45, 315 ~ 360)</string>
</resources>
---

□ res/values/arrays.xml
---
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <string-array name="orientationNames">
        <item>@string/south</item>
        <item>@string/west</item>
        <item>@string/north</item>
        <item>@string/east</item>
    </string-array>
</resources>
---

東西南北の文字列を配列で持っておく。
プログラム中でも配列を使うので、リソースも、string-array 要素を使って、配列とし
て管理する。