A Day In The Life

とあるプログラマの備忘録

iPhoneとAndroidで真北の取得方法を比較する

この前書いた日経ソフトウエア2010年1月の記事の中でiPhoneAndroid電子コンパスについて書いたのですが、紙面の都合上Androidで真北をとる方法を載せることができませんでした。せっかくなので補足の意味も込めてAndroidで真北をとる方法を紹介しiPhoneの実装と比較してみます。

そもそも磁北と真北の違いは?

磁北は文字通り方位磁石が指す北のことで、真北とは地球の地軸の北のことです。この磁北と真北には場所によってズレがあります。たとえば沖縄だと約4度とか、東京だと約7度とか。このズレのことを磁気偏角といいます。日本では真北は磁北から西にずれる(アメリカなど地域によっては東にずれる)ので磁北から磁気偏角を引いてやれば真北を求めることが出来ます。

真北 = 磁北 - 磁気偏角

AndroidiPhoneで真北をとるために位置情報が必要なのはこの磁気偏角を計算するためです。

Androidで磁気偏角を取得するには?

磁気偏角を取得するためにはandroid.hardwareパッケージにあるGeomagneticFieldというクラスのgetDeclinationメソッドを使います。APIドキュメントには以下のような説明が描いてあります。

The declination of the horizontal component of the magnetic field from true north, in degrees (i.e. positive means the magnetic field is rotated east that much from true north).

GeomagneticField#getDeclination()

東にずれる場合はプラス、西にずれる場合はマイナスの値が取得できるようです。ちなみに東京でテストしてみたところ-6.89263となりました。

処理手順は以下のようになります。

  1. LocationManagerのインスタンス生成
  2. LocationListenerの登録
  3. onLocationChangedメソッド内でGeomagneticFieldクラスのインスタンス生成
  4. GeomagneticFieldインスタンスのgetDeclinationメソッドを使って磁気偏角取得

GeomagneticFieldクラスをインスタンス化するにはコンストラクタにLocationManagerから取得した緯度、経度、高度に加え現在時刻を渡してやる必要があります(なぜ現在時刻が必要なのかは今のところ不明です)。

public class CompassActivity extends Activity
    implements LocationListener {

  private LocationManager manager;

  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main);
    manager = (LocationManager) getSystemService(Context.LOCATION_SERVICE);
  }
  @Override
  protected void onResume() {
    super.onResume();
    String provider = manager.getBestProvider(new Criteria(), true);
    manager.requestLocationUpdates(provider, 0, 0, this);
  }
  public void onLocationChanged(Location location) {
    float latitude = new Double(location.getLatitude()).floatValue();
    float longitude = new Double(location.getLongitude()).floatValue();
    float altitude = new Double(location.getAltitude()).floatValue();
    GeomagneticField geomagnetic = new GeomagneticField(
        latitude, longitude, altitude, new Date().getTime());
    // 磁気偏角を取得する
    geomagnetic.getDeclination();
  }
  ・・・以下省略
}

Androidで真北を取得する

あとはSenserManagerを使って傾きを取得し、GeoMagneticFieldインスタンスのgetDeclinationメソッドの値をたしてやれば真北を取得することができます(getDeclinationメソッドの値がマイナスのため)。

import java.util.Date;
import java.util.List;

import android.app.Activity;
import android.content.Context;
import android.hardware.GeomagneticField;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.location.Criteria;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.os.Bundle;
import android.widget.TextView;

public class CompassActivity extends Activity 
		implements LocationListener, SensorEventListener {

  private LocationManager locationManager;
  private SensorManager sensorManager;
  private TextView trueNorthView;
  private TextView magneticNorthView;
  private GeomagneticField geomagnetic;

  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main);
    trueNorthView = (TextView)findViewById(R.id.trueNorth);
    magneticNorthView = (TextView)findViewById(R.id.magneticNorth);
    sensorManager = (SensorManager)getSystemService(SENSOR_SERVICE);
    locationManager = (LocationManager) getSystemService(Context.LOCATION_SERVICE);
  }

  @Override
  protected void onResume() {
    super.onResume();
    List<Sensor> sensors = sensorManager.getSensorList(Sensor.TYPE_ORIENTATION);
    for (Sensor s : sensors) {
      sensorManager.registerListener(this, s, SensorManager.SENSOR_DELAY_NORMAL);
    }
    String provider = locationManager.getBestProvider(new Criteria(), true);
    locationManager.requestLocationUpdates(provider, 0, 0, this);
  }

  public void onLocationChanged(Location location) {
    float latitude = new Double(location.getLatitude()).floatValue();
    float longitude = new Double(location.getLongitude()).floatValue();
    float altitude = new Double(location.getAltitude()).floatValue();
    geomagnetic = new GeomagneticField(
        latitude, longitude, altitude, new Date().getTime());
  }

  public void onProviderDisabled(String provider) {
  }

  public void onProviderEnabled(String provider) {
  }

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

  public void onAccuracyChanged(Sensor sensor, int accuracy) {
  }

  public void onSensorChanged(SensorEvent e) {
    switch(e.sensor.getType()) {
    // 傾き
    case Sensor.TYPE_ORIENTATION:
      float magneticNorth = e.values[SensorManager.DATA_X];
      if (geomagnetic != null) {
        // 真北
        float trueNorth = magneticNorth + geomagnetic.getDeclination();
        trueNorthView.setText(String.valueOf(trueNorth));
      }
      // 磁北
      magneticNorthView.setText(String.valueOf(magneticNorth));
      break;
    }
  }
	
  @Override
  protected void onPause() {
    locationManager.removeUpdates(this);
    sensorManager.unregisterListener(this);
    super.onPause();
  }
}

iPhoneで磁北、真北を取得する方法

iPhoneの場合はCLLocationManagerを使えば磁北、真北ともに取れるのでAndroidに比べて簡単です。位置情報を使って磁気偏角を計算しているのはiPhoneAndroidも同じなのですが、iPhoneは磁気偏角プログラマが意識しなくてもいいようになっています。

#import <UIKit/UIKit.h>
#import <CoreLocation/CoreLocation.h>
@interface CompassViewController : UIViewController
    <CLLocationManagerDelegate> {
  CLLocationManager *locationManager;
}
@end

@implementation CompassViewController
- (void)viewDidLoad {
  [super viewDidLoad];
  self.locationManager = [[CLLocationManager alloc] init];
  locationManager.headingFilter = kCLHeadingFilterNone;
  locationManager.delegate = self;
  [locationManager startUpdatingHeading];
}
- (void)locationManager:(CLLocationManager *)manager
    didUpdateHeading:(CLHeading *)heading {
  NSLog([NSString stringWithFormat:@"%f", heading.trueHeading]);
  NSLog([NSString stringWithFormat:@"%f", heading.magneticHeading]);
}
・・・deallocメソッド省略
@end