Calculate Compass Bearing/Heading to Location in Android

Calculate compass bearing / heading to location in Android

Ok I figured this out. For anyone else trying to do this you need:

a) heading: your heading from the hardware compass. This is in degrees east of magnetic north

b) bearing: the bearing from your location to the destination location. This is in degrees east of true north.

myLocation.bearingTo(destLocation);

c) declination: the difference between true north and magnetic north

The heading that is returned from the magnetometer + accelermometer is in degrees east of true (magnetic) north (-180 to +180) so you need to get the difference between north and magnetic north for your location. This difference is variable depending where you are on earth. You can obtain by using GeomagneticField class.

GeomagneticField geoField;

private final LocationListener locationListener = new LocationListener() {
public void onLocationChanged(Location location) {
geoField = new GeomagneticField(
Double.valueOf(location.getLatitude()).floatValue(),
Double.valueOf(location.getLongitude()).floatValue(),
Double.valueOf(location.getAltitude()).floatValue(),
System.currentTimeMillis()
);
...
}
}

Armed with these you calculate the angle of the arrow to draw on your map to show where you are facing in relation to your destination object rather than true north.

First adjust your heading with the declination:

heading += geoField.getDeclination();

Second, you need to offset the direction in which the phone is facing (heading) from the target destination rather than true north. This is the part that I got stuck on. The heading value returned from the compass gives you a value that describes where magnetic north is (in degrees east of true north) in relation to where the phone is pointing. So e.g. if the value is -10 you know that magnetic north is 10 degrees to your left. The bearing gives you the angle of your destination in degrees east of true north. So after you've compensated for the declination you can use the formula below to get the desired result:

heading = myBearing - (myBearing + heading); 

You'll then want to convert from degrees east of true north (-180 to +180) into normal degrees (0 to 360):

Math.round(-heading / 360 + 180)

Determine compass direction

Checkout Compass class in this project: https://github.com/iutinvg/compass

I have used it succsesfully in this app: https://play.google.com/store/apps/details?id=com.gps.build

Regards.

UPDATE:

After reading your question again, i have realized that I did not give you enough details for "pointing to marker" part. Please check full class below. To calculate pointing direction, us startBearing and stopBearing methods.

Note that bearing degrees are changing with device rotation in BringMeBack class with setBearingDegrees method. Thats is not what you need, so you just have to remove locationManager and to put static bearing coordinate. And call that method only once.

Extended compass class:

package com.gps.bitlab;

import android.content.Context;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.os.Bundle;
import android.support.v4.app.FragmentActivity;
import android.util.Log;
import android.view.animation.Animation;
import android.view.animation.RotateAnimation;
import android.widget.ImageView;

import com.gps.bitlab.fragment.MessageDialogFragment;
import com.gps.bitlab.util.Utility;

public class Compass implements SensorEventListener {
private static final String TAG = "Compass";

private SensorManager sensorManager;
private Sensor gsensor;
private Sensor msensor;
private float[] mGravity = new float[3];
private float[] mGeomagnetic = new float[3];
private float azimuth = 0f;
private float currectAzimuth = 0;

private boolean bearing = false;
private float bearingDegrees = -1;

// compass arrow to rotate
public ImageView arrowView = null;

FragmentActivity activity;

public Compass(FragmentActivity activity) {

this.activity = activity;

sensorManager = (SensorManager) activity.getApplicationContext()
.getSystemService(Context.SENSOR_SERVICE);
gsensor = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
msensor = sensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD);
}

public void start() {

boolean deviceSensorCompatible = true;

if(!sensorManager.registerListener(this, gsensor, SensorManager.SENSOR_DELAY_GAME))
deviceSensorCompatible = false;

if(!sensorManager.registerListener(this, msensor, SensorManager.SENSOR_DELAY_GAME))
deviceSensorCompatible = false;

if(!deviceSensorCompatible) {
Utility.ShowMessage(activity, activity.getString(R.string.erroroccured), activity.getString(R.string.deviceIncompatible), 1);
stop();
}
}

public void startBearing()
{
bearing = true;
start();
}

public void setBearingDegrees(float bearingDegrees)
{
this.bearingDegrees = bearingDegrees;
}

public void stop() {
sensorManager.unregisterListener(this);
}

public void stopBearing()
{
bearing = false;
stop();
}

private void adjustArrow() {
if (arrowView == null) {
Log.i(TAG, "arrow view is not set");
return;
}

Animation an = new RotateAnimation(-currectAzimuth, -azimuth,
Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF,
0.5f);
currectAzimuth = azimuth;

an.setDuration(250);
an.setRepeatCount(0);
an.setFillAfter(true);

arrowView.startAnimation(an);
}

@Override
public void onSensorChanged(SensorEvent event) {
final float alpha = 0.97f;

synchronized (this) {
if (event.sensor.getType() == Sensor.TYPE_ACCELEROMETER) {

mGravity[0] = alpha * mGravity[0] + (1 - alpha)
* event.values[0];
mGravity[1] = alpha * mGravity[1] + (1 - alpha)
* event.values[1];
mGravity[2] = alpha * mGravity[2] + (1 - alpha)
* event.values[2];

// mGravity = event.values;

// Log.e(TAG, Float.toString(mGravity[0]));
}

if (event.sensor.getType() == Sensor.TYPE_MAGNETIC_FIELD) {
// mGeomagnetic = event.values;

mGeomagnetic[0] = alpha * mGeomagnetic[0] + (1 - alpha)
* event.values[0];
mGeomagnetic[1] = alpha * mGeomagnetic[1] + (1 - alpha)
* event.values[1];
mGeomagnetic[2] = alpha * mGeomagnetic[2] + (1 - alpha)
* event.values[2];
// Log.e(TAG, Float.toString(event.values[0]));

}

float R[] = new float[9];
float I[] = new float[9];
boolean success = SensorManager.getRotationMatrix(R, I, mGravity,
mGeomagnetic);
if (success) {
float orientation[] = new float[3];
SensorManager.getOrientation(R, orientation);
// Log.d(TAG, "azimuth (rad): " + azimuth);
azimuth = (float) Math.toDegrees(orientation[0]); // orientation
azimuth = (azimuth + 360) % 360;

if(bearing) {
if(bearingDegrees != -1) {
azimuth -= bearingDegrees;
adjustArrow();
}
}
else
adjustArrow();

// Log.d(TAG, "azimuth (deg): " + azimuth);

}
}
}

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

Class that use pointing to location functionality:

package com.gps.bitlab;

import android.content.Intent;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.support.v7.app.ActionBarActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.MenuItem;
import android.view.View;
import android.view.WindowManager;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.LinearLayout;

import com.gps.bitlab.R;
import com.gps.bitlab.fragment.OnDialogClickListener;
import com.gps.bitlab.util.Utility;

public class BringMeBack extends ActionBarActivity implements LocationListener, OnDialogClickListener {

LocationManager locMng;

Location location;
double lat;
double lon;
double alt;
String name;
Compass compass;

FrameLayout bearingParentLayout;
LinearLayout BaseLayout;
ImageView arrow;

boolean layoutReplaced = false;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Utility.SetLocalization(this);
setContentView(R.layout.activity_bring_me_back);

getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
getActionBar().setDisplayHomeAsUpEnabled(true);

bearingParentLayout = (FrameLayout)findViewById(R.id.bearingParentLayout);
BaseLayout = Utility.GetLoadingView(getLayoutInflater(), getString(R.string.waitingForLocation));

if(savedInstanceState != null)
{
lat = savedInstanceState.getDouble("lat");
lon = savedInstanceState.getDouble("lon");
alt = savedInstanceState.getDouble("alt");
name = savedInstanceState.getString("name");
}
else {
lat = getIntent().getExtras().getDouble("lat");
lon = getIntent().getExtras().getDouble("lon");
alt = getIntent().getExtras().getDouble("alt");
name = getIntent().getExtras().getString("name");
}

if(name != null && !name.equals(""))
getActionBar().setTitle(name);

locMng = (LocationManager)getSystemService(LOCATION_SERVICE);
location = new Location(LocationManager.GPS_PROVIDER);
location.setLongitude(lon);
location.setLatitude(lat);
location.setAltitude(alt);


arrow = (ImageView)findViewById(R.id.bearingArrow);
compass = new Compass(this);
compass.arrowView = arrow;

arrow.setVisibility(View.GONE);
bearingParentLayout.addView(BaseLayout);
}



@Override
public boolean onOptionsItemSelected(MenuItem item) {
// Handle action bar item clicks here. The action bar will
// automatically handle clicks on the Home/Up button, so long
// as you specify a parent activity in AndroidManifest.xml.
int id = item.getItemId();
if (id == android.R.id.home) {
BringMeBack.this.finish();
}

return super.onOptionsItemSelected(item);
}

@Override
protected void onPause() {
super.onPause();
compass.stopBearing();
locMng.removeUpdates(this);
}

@Override
protected void onResume() {
super.onResume();
compass.startBearing();
RequestLocationUpdates();
}


@Override
public void onLocationChanged(Location currentLocation) {

float bearing = currentLocation.bearingTo(location);
Log.d("Location bearing", String.valueOf(bearing));
compass.setBearingDegrees(bearing);

if(!layoutReplaced) {
bearingParentLayout.removeView(BaseLayout);
arrow.setVisibility(View.VISIBLE);
layoutReplaced = true;
}
}

@Override
public void onStatusChanged(String s, int i, Bundle bundle) {

}

@Override
public void onProviderEnabled(String s) {
Log.d("GPS", "Service enabled");
RequestLocationUpdates();
}

@Override
public void onProviderDisabled(String s) {
locMng.removeUpdates(this);
Utility.ShowMessage(BringMeBack.this, getString(R.string.locationServiceDisabledMessage), getString(R.string.locationServiceDisabled), 0);
}

@Override
public void OnPositiveClick(int key, Object... args) {
startActivity(new Intent(android.provider.Settings.ACTION_LOCATION_SOURCE_SETTINGS));
}

@Override
public void OnNegativeClick(int key, Object... args) {

}

private void RequestLocationUpdates()
{
if(!locMng.isProviderEnabled(LocationManager.GPS_PROVIDER))
Utility.ShowMessage(BringMeBack.this, getString(R.string.locationServiceDisabledMessage), getString(R.string.locationServiceDisabled), 0);
else
locMng.requestLocationUpdates(LocationManager.GPS_PROVIDER, 500, 0, this);
}
}

why is my headingValue such diffrent from bearingValue while looking at targetLocation?

Heading is the direction where you look, e.g a tank in which direction it would shoot, while bearing is the direction this vehicle moves. So that should answer why bearing is not heading.
They have different names, and meanings, they are different caluclated, they could not be expected to deliver the same value.

More details

You can move North (bearing = North) , but look at NE. (heading)

  • Gps delivers bearing (or course (over ground)), the direction the vehicle moves (altough some Api wrongly call it heading)

  • Compass (=magnetometer) delivers the direction in which you hold the device = (heading)

When you calculate the bearing between the two locations defined as coordinates in lat,lon , as you do in targetLocation (location.bearingTo(targetLocation)). then this is bearing! It is not heading!

And neither the compass not the accelrometer will deliver a decent heading value.
Some android device are very wrong in their magnetomter ( I saw +-20 degrees compared to +/- 2 degrees of my iPhone., Always use a traditional high quality compass as reference)
The ios devices shows the heading well within +/- 2 degress when well calibrated, (you have to calibrate each time before looking at the decice value, not only when you are asked by the operating system to calibrate).

GPS when moving > 10 km(h delives goot bearing results, but not heading.

Magnetometer can be off by some degree even when calibrated.
And usually the declination is smaller than the error.
Declination is nearly nothing in europe, 3 degress very north (europe), only a few places have a high declination >6-7°(north alaska)

Update to your further explantion in your graphic:

You have placed two points with a distance of only 15m, while GPS will not be much more acurate than 3-6m.
So imagine 6m offset of start or destination: such a triangle where a = 6m, b = 15, has an angle of atan2(6 / 15.0) = 21°. So you have an offset of 21° only by inacuracy of location. However still think at the differnce of heading by compass and bearing by line of sight between two locations.



Related Topics



Leave a reply



Submit