Calculate compass bearing / heading to location in Android

129,888

Solution 1

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)

Solution 2

@Damian - The idea is very good and I agree with answer, but when I used your code I had wrong values, so I wrote this on my own (somebody told the same in your comments). Counting heading with the declination is good, I think, but later I used something like that:

heading = (bearing - heading) * -1;

instead of Damian's code:

heading = myBearing - (myBearing + heading); 

and changing -180 to 180 for 0 to 360:

      private float normalizeDegree(float value){
          if(value >= 0.0f && value <= 180.0f){
              return value;
          }else{
              return 180 + (180 + value);
          }

and then when you want to rotate your arrow you can use code like this:

      private void rotateArrow(float angle){

            Matrix matrix = new Matrix();
            arrowView.setScaleType(ScaleType.MATRIX);
            matrix.postRotate(angle, 100f, 100f);
            arrowView.setImageMatrix(matrix);
      }

where arrowView is ImageView with arrow picture and 100f parameters in postRotate is pivX and pivY).

I hope I will help somebody.

Solution 3

In this an arrow on compass shows the direction from your location to Kaaba(destination Location)

you can simple use bearingTo in this way.bearing to will give you the direct angle from your location to destination location

  Location userLoc=new Location("service Provider");
    //get longitudeM Latitude and altitude of current location with gps class and  set in userLoc
    userLoc.setLongitude(longitude); 
    userLoc.setLatitude(latitude);
    userLoc.setAltitude(altitude);

   Location destinationLoc = new Location("service Provider");
  destinationLoc.setLatitude(21.422487); //kaaba latitude setting
  destinationLoc.setLongitude(39.826206); //kaaba longitude setting
  float bearTo=userLoc.bearingTo(destinationLoc);

bearingTo will give you a range from -180 to 180, which will confuse things a bit. We will need to convert this value into a range from 0 to 360 to get the correct rotation.

This is a table of what we really want, comparing to what bearingTo gives us

+-----------+--------------+
| bearingTo | Real bearing |
+-----------+--------------+
| 0         | 0            |
+-----------+--------------+
| 90        | 90           |
+-----------+--------------+
| 180       | 180          |
+-----------+--------------+
| -90       | 270          |
+-----------+--------------+
| -135      | 225          |
+-----------+--------------+
| -180      | 180          |
+-----------+--------------+

so we have to add this code after bearTo

// If the bearTo is smaller than 0, add 360 to get the rotation clockwise.

  if (bearTo < 0) {
    bearTo = bearTo + 360;
    //bearTo = -100 + 360  = 260;
}

you need to implements the SensorEventListener and its functions(onSensorChanged,onAcurracyChabge) and write all the code inside onSensorChanged

Complete code is here for Direction of Qibla compass

 public class QiblaDirectionCompass extends Service implements SensorEventListener{
 public static ImageView image,arrow;

// record the compass picture angle turned
private float currentDegree = 0f;
private float currentDegreeNeedle = 0f;
Context context;
Location userLoc=new Location("service Provider");
// device sensor manager
private static SensorManager mSensorManager ;
private Sensor sensor;
public static TextView tvHeading;
   public QiblaDirectionCompass(Context context, ImageView compass, ImageView needle,TextView heading, double longi,double lati,double alti ) {

    image = compass;
    arrow = needle;


    // TextView that will tell the user what degree is he heading
    tvHeading = heading;
    userLoc.setLongitude(longi);
    userLoc.setLatitude(lati);
    userLoc.setAltitude(alti);

  mSensorManager =  (SensorManager) context.getSystemService(SENSOR_SERVICE);
    sensor = mSensorManager.getDefaultSensor(Sensor.TYPE_ORIENTATION);
    if(sensor!=null) {
        // for the system's orientation sensor registered listeners
        mSensorManager.registerListener(this, sensor, SensorManager.SENSOR_DELAY_GAME);//SensorManager.SENSOR_DELAY_Fastest
    }else{
        Toast.makeText(context,"Not Supported", Toast.LENGTH_SHORT).show();
    }
    // initialize your android device sensor capabilities
this.context =context;
@Override
public void onCreate() {
    // TODO Auto-generated method stub
    Toast.makeText(context, "Started", Toast.LENGTH_SHORT).show();
    mSensorManager.registerListener(this, sensor, SensorManager.SENSOR_DELAY_GAME); //SensorManager.SENSOR_DELAY_Fastest
    super.onCreate();
}

@Override
public void onDestroy() {
    mSensorManager.unregisterListener(this);
Toast.makeText(context, "Destroy", Toast.LENGTH_SHORT).show();

    super.onDestroy();

}
@Override
public void onSensorChanged(SensorEvent sensorEvent) {


Location destinationLoc = new Location("service Provider");

destinationLoc.setLatitude(21.422487); //kaaba latitude setting
destinationLoc.setLongitude(39.826206); //kaaba longitude setting
float bearTo=userLoc.bearingTo(destinationLoc);

  //bearTo = The angle from true north to the destination location from the point we're your currently standing.(asal image k N se destination taak angle )

  //head = The angle that you've rotated your phone from true north. (jaise image lagi hai wo true north per hai ab phone jitne rotate yani jitna image ka n change hai us ka angle hai ye)



GeomagneticField geoField = new GeomagneticField( Double.valueOf( userLoc.getLatitude() ).floatValue(), Double
        .valueOf( userLoc.getLongitude() ).floatValue(),
        Double.valueOf( userLoc.getAltitude() ).floatValue(),
        System.currentTimeMillis() );
head -= geoField.getDeclination(); // converts magnetic north into true north

if (bearTo < 0) {
    bearTo = bearTo + 360;
    //bearTo = -100 + 360  = 260;
}

//This is where we choose to point it
float direction = bearTo - head;

// If the direction is smaller than 0, add 360 to get the rotation clockwise.
if (direction < 0) {
    direction = direction + 360;
}
 tvHeading.setText("Heading: " + Float.toString(degree) + " degrees" );

RotateAnimation raQibla = new RotateAnimation(currentDegreeNeedle, direction, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f);
raQibla.setDuration(210);
raQibla.setFillAfter(true);

arrow.startAnimation(raQibla);

currentDegreeNeedle = direction;

// create a rotation animation (reverse turn degree degrees)
RotateAnimation ra = new RotateAnimation(currentDegree, -degree, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f);

// how long the animation will take place
ra.setDuration(210);


// set the animation after the end of the reservation status
ra.setFillAfter(true);

// Start the animation
image.startAnimation(ra);

currentDegree = -degree;
}
@Override
public void onAccuracyChanged(Sensor sensor, int i) {

}
@Nullable
@Override
public IBinder onBind(Intent intent) {
    return null;
}

xml code is here

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/flag_pakistan">
<TextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:id="@+id/heading"
    android:textColor="@color/colorAccent"
    android:layout_centerHorizontal="true"
    android:layout_marginBottom="100dp"
    android:layout_marginTop="20dp"
    android:text="Heading: 0.0" />
<RelativeLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/heading"
android:scaleType="centerInside"
android:layout_centerVertical="true"
android:layout_centerHorizontal="true">

<ImageView
    android:id="@+id/imageCompass"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:scaleType="centerInside"
    android:layout_centerVertical="true"
    android:layout_centerHorizontal="true"
    android:src="@drawable/images_compass"/>

<ImageView
    android:id="@+id/needle"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_centerVertical="true"
    android:layout_centerHorizontal="true"
    android:scaleType="centerInside"
    android:src="@drawable/arrow2"/>
</RelativeLayout>
</RelativeLayout>

Solution 4

I know this is a little old but for the sake of folks like myself from google who didn't find a complete answer here. Here are some extracts from my app which put the arrows inside a custom listview....

Location loc;   //Will hold lastknown location
Location wptLoc = new Location("");    // Waypoint location 
float dist = -1;
float bearing = 0;
float heading = 0;
float arrow_rotation = 0;

LocationManager lm = (LocationManager) getSystemService(Context.LOCATION_SERVICE);
loc = lm.getLastKnownLocation(LocationManager.GPS_PROVIDER);

if(loc == null) {   //No recent GPS fix
    Criteria criteria = new Criteria();
    criteria.setAccuracy(Criteria.ACCURACY_FINE);
    criteria.setAltitudeRequired(false);
    criteria.setBearingRequired(true);
    criteria.setCostAllowed(true);
    criteria.setSpeedRequired(false);
    loc = lm.getLastKnownLocation(lm.getBestProvider(criteria, true));
}

if(loc != null) {
    wptLoc.setLongitude(cursor.getFloat(2));    //Cursor is from SimpleCursorAdapter
    wptLoc.setLatitude(cursor.getFloat(3));
    dist = loc.distanceTo(wptLoc);
    bearing = loc.bearingTo(wptLoc);    // -180 to 180
    heading = loc.getBearing();         // 0 to 360
    // *** Code to calculate where the arrow should point ***
    arrow_rotation = (360+((bearing + 360) % 360)-heading) % 360;
}

I willing to bet it could be simplified but it works! LastKnownLocation was used since this code was from new SimpleCursorAdapter.ViewBinder()

onLocationChanged contains a call to notifyDataSetChanged();

code also from new SimpleCursorAdapter.ViewBinder() to set image rotation and listrow colours (only applied in a single columnIndex mind you)...

LinearLayout ll = ((LinearLayout)view.getParent());
ll.setBackgroundColor(bc); 
int childcount = ll.getChildCount();
for (int i=0; i < childcount; i++){
    View v = ll.getChildAt(i);
    if(v instanceof TextView) ((TextView)v).setTextColor(fc);
    if(v instanceof ImageView) {
        ImageView img = (ImageView)v;
        img.setImageResource(R.drawable.ic_arrow);
        Matrix matrix = new Matrix();
        img.setScaleType(ScaleType.MATRIX);
        matrix.postRotate(arrow_rotation, img.getWidth()/2, img.getHeight()/2);
        img.setImageMatrix(matrix); 
}

In case you're wondering I did away with the magnetic sensor dramas, wasn't worth the hassle in my case. I hope somebody finds this as useful as I usually do when google brings me to stackoverflow!

Solution 5

I'm no expert in map-reading / navigation and so on but surely 'directions' are absolute and not relative or in reality, they are relative to N or S which themselves are fixed/absolute.

Example: Suppose an imaginary line drawn between you and your destination corresponds with 'absolute' SE (a bearing of 135 degrees relative to magnetic N). Now suppose your phone is pointing NW - if you draw an imaginary line from an imaginary object on the horizon to your destination, it will pass through your location and have an angle of 180 degrees. Now 180 degrees in the sense of a compass actually refers to S but the destination is not 'due S' of the imaginary object your phone is pointing at and, moreover, if you travelled to that imaginary point, your destination would still be SE of where you moved to.

In reality, the 180 degree line actually tells you the destination is 'behind you' relative to the way the phone (and presumably you) are pointing.

Having said that, however, if calculating the angle of a line from the imaginary point to your destination (passing through your location) in order to draw a pointer towards your destination is what you want...simply subtract the (absolute) bearing of the destination from the absolute bearing of the imaginary object and ignore a negation (if present). e.g., NW - SE is 315 - 135 = 180 so draw the pointer to point at the bottom of the screen indicating 'behind you'.

EDIT: I got the Maths slightly wrong...subtract the smaller of the bearings from the larger then subtract the result from 360 to get the angle in which to draw the pointer on the screen.

Share:
129,888
Damian
Author by

Damian

Updated on July 05, 2022

Comments

  • Damian
    Damian almost 2 years

    I want to display an arrow at my location on a google map view that displays my direction relative to a destination location (instead of north).

    a) I have calculated north using the sensor values from the magnetometer and accelerometer. I know this is correct because it lines up with the compass used on the Google Map view.

    b) I have calculated the initial bearing from my location to the destination location by using myLocation.bearingTo(destLocation);

    I'm missing the last step; from these two values (a & b) what formula do I use to get the direction in which the phone is pointing relative to the destination location?

    Appreciate any help for an addled mind!

  • Damian
    Damian over 13 years
    Thanks for your help. I managed to figure it out and produced the answer below for anybody else facing the same problem.
  • Henry
    Henry about 13 years
    is this a typo in your line? heading +- geoField.getDeclination(); or what you mean with +- ??
  • Admin
    Admin about 13 years
    when you say "heading = myBearing - (myBearing + heading); " is there a mistake there?? It's like saying heading = heading.
  • Ilya Saunkin
    Ilya Saunkin over 12 years
    @Nikolas actually it's like saying heading = -heading.
  • dhesse
    dhesse over 12 years
    Sorry i don't get the point of your code @Damien. Your initial heading is equivalent to the given azimuth from onOrientationChanged in degrees. To get the direction to your target location you just change azimuth with *-1? How should that work?
  • datWooWoo
    datWooWoo almost 12 years
    Could somebody explain this please? Does this work? Because I always get a heading of 180 degrees using this method. Also, dhesse is right, why are we doing that when it's just heading = -heading? I feel like something is wrong here, but 14 people have approved it.
  • tricknology
    tricknology over 11 years
    Terminology aside, Android's getDeclination() method returns: "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)."
  • Mister Smith
    Mister Smith over 11 years
    I'm sorry but magnetic declination is the correct term. Check ngdc.noaa.gov/geomagmodels/Declination.jsp. Or are the guys at NOAA also wrong?
  • Nick
    Nick over 11 years
    @lespommes Did you figure this out? I also always get a heading of 180 degrees using this code :-/!
  • CookieMonssster
    CookieMonssster about 11 years
    Your idea is very good and it was very helpful for me, but I used different code for counting values for rotating arrow. My codes below in my answer.
  • edoardotognoni
    edoardotognoni almost 11 years
    I don't understand why you use * -1 when calculating the heading
  • Alexander
    Alexander almost 10 years
    normalizeDegree is unnecessarily complex. You can do the same with just return (value + 360) % 360
  • Price
    Price almost 10 years
    How exactly do you get heading? Is it the same as the value retrieved from location.getBearing()?
  • brainmurphy1
    brainmurphy1 over 9 years
    "return 180 + (180 + value);"
  • cjm
    cjm about 9 years
    Only 2 points vs. the 37 which the (incorrect) accepted answer got? Oh well, thanks for posting Squonk, helped me out a lot.
  • Wasim Ahmed
    Wasim Ahmed about 9 years
    whats the diffrence between bearing and mybearing?
  • WindRider
    WindRider about 9 years
    why "heading = (bearing - heading) * -1" instead of just "heading -= bearing"
  • CookieMonssster
    CookieMonssster about 9 years
    I made this code two years ago and exackly I can't remember why I made it in this way, but it was working correctly
  • portfoliobuilder
    portfoliobuilder over 7 years
    This code always only returns 180 degrees. This is not correct.
  • NamNH
    NamNH about 7 years
    @Damian It's longtime but could you help to answer Price comment above "How exactly do you get heading? Is it the same / approximate as the value retrieved from location.getBearing() or oldLocation.bearingTo(newLocation) ?"
  • Tim Cooper
    Tim Cooper about 6 years
    How do I get the initial value of 'heading'? I can't see that from your code or Damian's. Do I get it out of the geoField object somehow?
  • TheTallWitch
    TheTallWitch about 6 years
    Thank you for this answer, but on some locations like in Canada or Argentina, the head for Qibla is showing the wrong direction
  • Muhammad Kashif Arif
    Muhammad Kashif Arif about 6 years
    I am in Pakistan and it is working fine. To empirical check all over the world impossible. May be you have missed something.
  • TheTallWitch
    TheTallWitch about 6 years
    I followed your code exactly, and it works great in Iran, and most of the places I checked, I gave a position in Canada just to test, and the direction was not correct. I also checked in a lot of existing Qibla Finder applications available, they also had this problem with some particular locations.
  • Muhammad Kashif Arif
    Muhammad Kashif Arif almost 6 years
    There will be a bug in builtin function or you may have taken some mistake. if there will be a bug then it will be resolve in latest version. because bring to is builtin function.
  • ngmir
    ngmir almost 6 years
    I have not investigated this thoroughly, but a careful read of the docs tells: "Returns the approximate initial bearing". This is something like the direction you have to start walking in to get to your destination on the shortest path across the globe (that is a sphere).
  • ngmir
    ngmir almost 6 years
    See here for an explanation of what Initial bearing is. This might explain why you get good-looking results from one and not-so-good results from another location. bearingTo is probably not what you are looking for when you want to have a straight direction on a map.
  • sheko
    sheko about 4 years
    where is degree here tvHeading.setText("Heading: " + Float.toString(degree) + " degrees" ); and where you declare/assign it ?????????????????????????
  • Abhinav Saxena
    Abhinav Saxena over 2 years
    Hello, Great! I will use this solution and let you know. Meanwhile you can put the ending curly brace inside the code area!! ;-)