Titanium JIRA Archive
Titanium SDK/CLI (TIMOB)

[TIMOB-9434] Android: compass event "trueHeading" not generated for recurring compass events

GitHub Issuen/a
TypeBug
PriorityMedium
StatusClosed
ResolutionFixed
Resolution Date2012-07-12T00:43:29.000+0000
Affected Version/sRelease 1.8.0
Fix Version/sRelease 2.1.0, Sprint 2012-12 Core
ComponentsAndroid
Labelscore, module_geolocation, qe-testadded
ReporterArthur Evans
AssigneeArthur Evans
Created2012-06-06T18:51:11.000+0000
Updated2013-01-17T15:59:30.000+0000

Description

When calling Ti.Geolocation.getCurrentHeading, the trueHeading property is defined in the callback object. However, when registering for heading updates, the trueHeading property is not defined in the event -- **unless you first call getCurrentHeading**. When we encode the event into a hash map, we do this:
        if (geomagneticField != null) {
            float trueHeading = x - geomagneticField.getDeclination();
            if (trueHeading < 0) {
                trueHeading = (360 - trueHeading) % 360;
            }
However, geomagneticField is only initialized in getCurrentHeading.
            String provider = tiLocation.locationManager.getBestProvider(criteria, true);
            if (provider != null) {
                Location location = tiLocation.locationManager.getLastKnownLocation(provider);
                if (location != null) {
                    geomagneticField = new GeomagneticField((float)location.getLatitude(), (float)location.   getLongitude(), (float)(location.getAltitude()), System.currentTimeMillis());
                }
            }
If we were to initialize geomagneticField when a listener is added, it might fix the immediate problem, although this wouldn't address the case when no last known location is available. Even if the user registers for compass updates and location updates at the same time, a location may not be immediately available. Also, I think this should be reevaluated in light of the new Android Geo code. Instead of calling getLastKnownLocation(provider), it might be preferable to call tiLocation.getLastKnownLocation, which will check all providers.

Comments

  1. Arthur Evans 2012-06-07

    Think I have a fix for this issue, need to test it on device.
  2. Arthur Evans 2012-06-08

    Fix didn't work on Nexus 1. Booting this bug back to the big boys. After discussing this with Josh, I think we really should be recalculating the declination periodically, but I'm not sure of the best approach to take. - Since we generate several heading events a second, generating a new GeomagneticField with every heading event is going to be too expensive. - Calculating the distance between the last fix and latest fix is also somewhat expensive. - Might be sufficient to just check the age of the last location fix, which would be a cheap comparison. Recalculating declination every few minutes should be plenty accurate enough, and fairly cheap. Here was my attempt:
       --- a/android/modules/geolocation/src/java/ti/modules/titanium/geolocation/TiCompass.java
       +++ b/android/modules/geolocation/src/java/ti/modules/titanium/geolocation/TiCompass.java
       @@ -32,6 +32,8 @@ public class TiCompass
        {
               private static final String LCAT = "TiCompass";
               private static final boolean DBG = TiConfig.LOGD;
       +       // recalculate the magnetic declination if we've traveled the specified number of meters
       +       private static final int GEOMAGNETIC_FIELD_THRESHOLD = 10000;
        
               private GeolocationModule geolocationModule;
               private TiLocation tiLocation;
       @@ -40,6 +42,7 @@ public class TiCompass
               private long lastEventInUpdate;
               private float lastHeading = 0.0f;
               private GeomagneticField geomagneticField;
       +       private Location geomagneticFieldLocation;
        
        
               public TiCompass(GeolocationModule geolocationModule, TiLocation tiLocation)
       @@ -50,6 +53,7 @@ public class TiCompass
        
               public void registerListener()
               {
       +               updateDeclination();
                       TiSensorHelper.registerListener(Sensor.TYPE_ORIENTATION, this, SensorManager.SENSOR_DELAY_UI);
               }
        
       @@ -88,6 +92,18 @@ public class TiCompass
                       }
               }
        
       +       private void updateDeclination()
       +       {
       +               Location location = tiLocation.getLastKnownLocation();
       +               if (location != null) {
       +                       if (geomagneticFieldLocation == null ||
       +                               location.distanceTo(geomagneticFieldLocation) > GEOMAGNETIC_FIELD_THRESHOLD) {
       +                               geomagneticField = new GeomagneticField((float)location.getLatitude(), (float)location.getLongitude(), (float)(location.getAltitude()), System.currentTimeMillis());
       +                               geomagneticFieldLocation = location;
       +                       }
       +               }
       +       }
       +
               private Object eventToHashMap(SensorEvent event, long timestamp)
               {
                       float x = event.values[0];
       @@ -170,16 +186,7 @@ public class TiCompass
                                       }
                               };
        
       -                       Criteria criteria = new Criteria();
       -                       
       -                       String provider = tiLocation.locationManager.getBestProvider(criteria, true);
       -                       if (provider != null) {
       -                               Location location = tiLocation.locationManager.getLastKnownLocation(provider);
       -                               if (location != null) {
       -                                       geomagneticField = new GeomagneticField((float)location.getLatitude(), (float)location.getLongitude(), (float)(location.getAltitude()), System.currentTimeMillis());
       -                               }
       -                       }
       -
       +                       updateDeclination();
                               TiSensorHelper.registerListener(Sensor.TYPE_ORIENTATION, oneShotHeadingListener, SensorManager.SENSOR_DELAY_UI);
                       }
               }
       
    I'm guessing maybe the tiLocation.getLastKnownLocation() is not working if the user hasn't registered any location providers.
  3. Arthur Evans 2012-06-08

    Weekend project.
  4. Arthur Evans 2012-06-12

    Testing

    Note that this code must be tested on device with compass and GPS support. It can't be tested on emulator. Use the following app.js:
       var win1 = Titanium.UI.createWindow({
       	title : 'Tab 1',
       	backgroundColor : '#fff',
       	fullscreen : false,
       	exitOnClose : true
       });
       var hasCompass = false;
       var haveLocationFix = false;
       var locationHandlerRegistered = false;
       
       var label1 = Titanium.UI.createLabel({
       	color : '#999',
       	text : 'Checking for Compass Support',
       	font : {
       		fontSize : 20,
       		fontFamily : 'Helvetica Neue'
       	},
       	textAlign : 'center',
       	width : '80%',
       	height : 160,
       	top : 20
       });
       
       win1.add(label1);
       var label2 = Ti.UI.createLabel({
       	text : 'Checking for location fix',
       	textAlign : 'center',
       	width : '80%',
       	top : 200
       });
       win1.add(label2);
       
       var getLocationButton = Ti.UI.createButton({
       	title : "Get Location Fix",
       	bottom : 0,
       	left : 0,
       	width : '50%'
       });
       var getHeadingButton = Ti.UI.createButton({
       	title : "Get Heading",
       	bottom : 0,
       	right : 0,
       	width : '50%'
       });
       win1.add(getLocationButton);
       win1.add(getHeadingButton);
       win1.open();
       
       var locationHandler = function(e) {
       	Ti.API.info('In locationHandler, event = ' + JSON.stringify(e));
       	if (e.success) {
       		if (haveLocationFix == false) {
       			haveLocationFix = true;
       			label2.text = 'Have location fix.';
       		}
       		if (locationHandlerRegistered) {
       			Ti.Geolocation.removeEventListener("location", locationHandler);
       			locationHandlerRegistered = false;
       		}
       	} else {
       		label2.text = 'No location fix.';
       	}
       
       }
       // Check for location fix--on Android, this will NOT activate the radios, just
       // return the cached location fix, so we use it to figure out whether we have a current fix.
       Ti.Geolocation.getCurrentPosition(locationHandler);
       getLocationButton.addEventListener('click', function(e) {
       	if (!locationHandlerRegistered) {
       		label2.text = 'Trying to get location fix.',
       		// Get one-shot location fix.
       		Ti.Geolocation.accuracy = Ti.Geolocation.ACCURACY_HIGH;
       		Ti.Geolocation.addEventListener("location", locationHandler);
       		locationHandlerRegistered = true;
       	}
       });
       
       var headingHandler = function(e) {
       	if (e.success === undefined || e.success) {
       		label1.setText("Mag: " + e.heading.magneticHeading + "\nTrue: " + e.heading.trueHeading);
       	}
       }
       
       // Without the fix, calling getCurrentHeading is required in order to get a trueHeading value.
       getHeadingButton.addEventListener('click', function(e) {
       	Ti.Geolocation.getCurrentHeading(headingHandler);
       });
       
       Ti.API.info("Got this far.");
       if (Ti.Geolocation.hasCompass !== undefined) {
       	Ti.API.info("Not undefined.");
       	if (Ti.Geolocation.hasCompass.call) {
       		hasCompass = Ti.Geolocation.hasCompass();
       		Ti.API.info("Is method, hasCompass = " + hasCompass);
       	} else {
       		hasCompass = Ti.Geolocation.hasCompass;
       		Ti.API.info("Is property, hasCompass = " + hasCompass);
       	}
       }
       if (hasCompass) {
       	Ti.Geolocation.addEventListener("heading", headingHandler);
       } else {
       	label1.text = "Compass not supported";
       }
       
    When testing _without_ the fix, note that the Mag value shows up (magnetic heading) but the True value (true heading) stays undefined, even if a location fix is available. (If no location fix is available, press the Get Location Fix button.) When you've satisfied yourself that true heading isn't showing up, press the Get Heading button, and note that the true heading shows up. When testing with the fix, start the application and note that if a location fix is available when the app starts, the true heading shows up immediately. If there is no location fix, press the Get Location Fix button (but NOT the Get Heading button). After the location label updates to "Have location fix", wait for true heading to show up. This should show up in one minute (or less).
  5. Arthur Evans 2012-06-12

    Addressed in PR: https://github.com/appcelerator/titanium_mobile/pull/2373
  6. Arthur Evans 2012-06-14

    Updated test code:
       
       var win1 = Titanium.UI.createWindow({
       	title : 'Tab 1',
       	backgroundColor : '#fff',
       	fullscreen : false,
       	exitOnClose : true
       });
       var hasCompass = false;
       var haveLocationFix = false;
       var locationHandlerRegistered = false;
       var lastFix =-1;
       
       var label1 = Titanium.UI.createLabel({
       	color : '#999',
       	text : 'Checking for Compass Support',
       	font : {
       		fontSize : 20,
       		fontFamily : 'Helvetica Neue'
       	},
       	textAlign : 'center',
       	width : '80%',
       	height : 160,
       	top : 20
       });
       
       win1.add(label1);
       var label2 = Ti.UI.createLabel({
       	text : 'Checking for location fix',
       	textAlign : 'center',
       	width : '80%',
       	top : 200
       });
       win1.add(label2);
       
       var getLocationButton = Ti.UI.createButton({
       	title : "Get Location Fix",
       	bottom : 0,
       	left : 0,
       	width : '50%'
       });
       var getHeadingButton = Ti.UI.createButton({
       	title : "Get Heading",
       	bottom : 0,
       	right : 0,
       	width : '50%'
       });
       win1.add(getLocationButton);
       win1.add(getHeadingButton);
       win1.open();
       
       var locationHandler = function(e) {
       	Ti.API.info('In locationHandler, event = ' + JSON.stringify(e));
       	if (e.success) {
       		if (haveLocationFix == false) {
       			haveLocationFix = true;
       		}
       		lastFix = (new Date().getTime() - e.coords.timestamp)/60000;
       		label2.text = String.format('Have location fix, age=%.2f minutes.', lastFix);
       		if (locationHandlerRegistered) {
       			Ti.Geolocation.removeEventListener("location", locationHandler);
       			locationHandlerRegistered = false;
       		}
       	} else {
       		label2.text = String.format('No location fix, last fix=%.2f minutes', lastfix);
       	}
       
       }
       // Check for location fix--on Android, this will NOT activate the radios, just
       // return the cached location fix, so we use it to figure out whether we have a current fix.
       setInterval(function() {
       	Ti.Geolocation.getCurrentPosition(locationHandler);
       }, 10000);
       getLocationButton.addEventListener('click', function(e) {
       	if (!locationHandlerRegistered) {
       		label2.text = 'Trying to get location fix.',
       		// Get one-shot location fix.
       		Ti.Geolocation.accuracy = Ti.Geolocation.ACCURACY_HIGH;
       		Ti.Geolocation.addEventListener("location", locationHandler);
       		locationHandlerRegistered = true;
       	}
       });
       
       var headingHandler = function(e) {
       	if (e.success === undefined || e.success) {
       		label1.setText("Mag: " + e.heading.magneticHeading + "\nTrue: " + e.heading.trueHeading);
       	}
       }
       // Without the fix, calling getCurrentHeading is required in order to get a trueHeading value.
       getHeadingButton.addEventListener('click', function(e) {
       	Ti.Geolocation.getCurrentHeading(headingHandler);
       });
       
       Ti.API.info("Got this far.");
       if (Ti.Geolocation.hasCompass.call) {
       	hasCompass = Ti.Geolocation.hasCompass();
       } else {
       	hasCompass = Ti.Geolocation.hasCompass;
       }
       
       if (hasCompass) {
       	Ti.Geolocation.addEventListener("heading", headingHandler);
       } else {
       	label1.text = "Compass not supported";
       }
       
    Updated testing instructions: - test on a device hooked up to DDMS. - To test without location fixes, turn airplane mode on and leave the app to run for 10 minutes. Updates will continue to show trueHeading, but a warning message will be logged 1/minute showing that location is stale. - If you start the app when no location fixes are available, you'll see trueHeading is undefined, and a warning message will be logged 1/minute showing that no location fix is available. - To test restoring location fixes, turn airplane mode back off and wait for a few minutes. You probably won't even need to request a location explicitly. If the device doesn't show a fix after a few minutes, you can press the Get Location Fix button. (Within ~1 minute of getting a fix, the device should start showing a true heading value.)
  7. Arthur Evans 2012-06-15

    After discussions with Opie, updated the fix so that we use the stored location, even if it's stale, but we log a message for stale location/no location. Test instructions above are updated. Key points for this issue are that we get trueHeading values on 'heading' events (assuming that a location is available). Without the fix, you should see true heading == undefined.
  8. Eric Merriman 2012-06-18

    Verified fixed with SDK 2.1.0.v20120618102300 using a droid 1
  9. Anshu Mittal 2012-07-12

    Reopening to update labels.

JSON Source