Skip to content
Midori edited this page Jan 29, 2024 · 30 revisions

True Heading

magneticHeading and trueHeading are two different types of orientations used to indicate direction.

magnetic heading: This is an orientation based on the geomagnetic field. That is, it is the azimuth angle with respect to north as indicated by the earth's magnetic field. This value is affected by the geomagnetic field and therefore depends on geographic location and variations in the geomagnetic field.

true heading: This is the azimuth relative to the geographic north pole (true north). Since true north is based on the earth's axis of rotation, it is not affected by the geomagnetic field. However, to accurately determine true north, the deviation between the device's current position and the geomagnetic field must be taken into account.

Calculation formula: true heading = magnetic heading + magnetic declination

Magnetic declination shifts with location and time. For example, the magnetic declination in Tokyo, Japan around 2024 is about -7.3 degrees, which means there is a difference of 7.3 degrees between magnetic heading and true heading.

CompassX uses true heading to obtain a more accurate value from north.

Why does Android require location permission?

While iOS has long had a standard API that provides true heading, Android has only recently implemented it since API 33, mostly because it requires getting the declination from the current location and calculating it manually.

Calibration

How to calibrate compass

Hold the smartphone and draw the number "8" Repeat a couple of times. You can see more details on google maps.

It is impossible to get perfect values with the physical compass sensor on the phone. However, calibration can improve accuracy.
In situations where high accuracy is required, it is recommended that users be prompted to calibrate each time.

Checking the accuracy of CompassX

Prepare an actual iOS device.

  1. Go to Settings -> Privacy & Security -> Location Service -> System Service -> Calibrate Compass = On
  2. Go to Settings -> Compass app -> Use true north = On

After calibrating the device sufficiently, compare the CompassX heading with the iOS default compass app.

Android will be compared to the iOS device prepared here. Unfortunately, Google does not provide a standard compass app, we do not recommend using third-party apps from the Google Play Store for measuring accuracy. You don't know if they really provide true heading. In Android, the sensors on each device are different, so there are individual differences.

How does it work

True heading

- iOS: Use CoreLocation library's CLLocationManager. Obtain using CLHeading.trueHeading.

public func locationManager(_ manager: CLLocationManager, didUpdateHeading newHeading: CLHeading) {
    eventSink?(newHeading.trueHeading)
}

- Android: Use Sensor.TYPE_HEADING from the android.hardware.Sensor library.

public void onAttachedToEngine(FlutterPlugin.FlutterPluginBinding flutterPluginBinding) {
    context = flutterPluginBinding.getApplicationContext();
    sensorManager = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE);
    headingSensor = sensorManager.getDefaultSensor(Sensor.TYPE_HEADING);
}

However, Sensor.TYPE_HEADING was added from API 33 and is still rare in Android devices. Therefore, in most cases, it is necessary to calculate using Sensor.TYPE_ROTATION_VECTOR adjusted with declination.

Calculate the true heading based on the true north by adding the magnetic declination to the azimuth based on the geomagnetic north.

Calculation formula: trueHeading = azimuth (magnetic heading) + declination (magnetic declination)

Azimuth (Magnetic heading): This is the angle based on geomagnetic north representing the direction to a target from a specific point.

Declination (Magnetic declination): Magnetic declination is the angular difference between magnetic north (geomagnetic north) and geographic north (true north) at a location. This value varies by geographic location and changes over time.

Add sensor

public void onAttachedToEngine(FlutterPlugin.FlutterPluginBinding flutterPluginBinding) {
    ///
    rotationVectorSensor = sensorManager.getDefaultSensor(Sensor.TYPE_ROTATION_VECTOR);
    ///
}

Calculate azimuth with ROTATION_VECTOR sensor

public void onSensorChanged(SensorEvent event) {
    if (event.sensor.getType() == Sensor.TYPE_ROTATION_VECTOR) {
        float[] rotationMatrix = new float[9];
        SensorManager.getRotationMatrixFromVector(rotationMatrix, event.values);
        float[] orientationAngles = new float[3];
        SensorManager.getOrientation(rotationMatrix, orientationAngles);
        float azimuth = (float) Math.toDegrees(orientationAngles[0]);
        float trueHeading = calculateTrueHeading(azimuth);
        ///
    } 
}

Obtain location and calculate trueHeading from azimuth and declination

public CompassXPlugin() {
    locationListener = new LocationListener() {
        @Override
        public void onLocationChanged(Location location) {
            currentLocation = location;
        }
     ///
    };
}
private float calculateTrueHeading(float azimuth) {
    float declination =
            currentLocation != null
                    ? new GeomagneticField((float) currentLocation.getLatitude(),
                            (float) currentLocation.getLongitude(),
                            (float) currentLocation.getAltitude(),
                            System.currentTimeMillis()).getDeclination()
                    : 0f;
    float trueHeading = (azimuth + declination + 360) % 360;
    return trueHeading;
}

Accuracy

Due to the use of physical sensors, results can be unstable. Calibration may be necessary in situations where accuracy is crucial. On Android, the type of sensors varies by device, leading to significant differences.

- iOS: Obtain using CLHeading.headingAccuracy.

Estimated error. Larger values indicate greater deviation. Negative values are returned when estimation is not possible. (degrees)

public func locationManager(_ manager: CLLocationManager, didUpdateHeading newHeading: CLHeading) {
    eventSink?(newHeading.headingAccuracy)
}

- Android: Acquired through SensorEventListener.

Accuracy can be obtained with SensorManager.SENSOR_STATUS_ACCURACY

public void onAccuracyChanged(Sensor sensor, int accuracy) {
    if (sensor != rotationVectorSensor && sensor != headingSensor) return;
    shouldCalibrate = accuracy != SensorManager.SENSOR_STATUS_ACCURACY_HIGH;
}

For Rotation Vector Sensor, event.values[4] can provide the estimated error. However, many devices lack the necessary sensors for estimation, in which case -1 is returned. (radians)

public void onSensorChanged(SensorEvent event) {
    if (event.sensor.getType() == Sensor.TYPE_ROTATION_VECTOR) {
        ///
        float accuracyRadian = event.values[4];
        float accuracy =
                accuracyRadian != -1 ? (float) Math.toDegrees(accuracyRadian) : -1;
        ///
    }
}

For Heading Sensor, event.values[1] can provide the estimated error. (degrees)

} else if (event.sensor.getType() == Sensor.TYPE_HEADING) {
    float heading = event.values[0];
    float accuracy = event.values[1];
    ///
}

References