简体   繁体   English

静止时用TYPE_ROTATION_VECTOR实现当前的基本方向法?

[英]Implementing current cardinal direction method with TYPE_ROTATION_VECTOR while standing still?

There appear to be many old examples of getting the current cardinal direction on Android devices, but an official solution provided by Google does not appear to be in their documentation. 似乎有许多旧的例子表明在Android设备上获得当前的主要方向,但Google提供的官方解决方案似乎并未出现在他们的文档中。

The oldest reference Sensor.TYPE_ORIENTATION which is deprecated, more recent ones mention Sensor.TYPE_ACCELEROMETER and Sensor.TYPE_MAGNETIC_FIELD (which I have tried with little success - accuracy shifts rapidly depending on device orientation). 不推荐使用的最旧参考Sensor.TYPE_ORIENTATION ,最新的参考文献提到了Sensor.TYPE_ACCELEROMETERSensor.TYPE_MAGNETIC_FIELD (我试过但收效甚微 - 准确性根据设备方向快速变化)。 I've been experimenting with implementations using those two like this. 我一直在尝试使用这两个这样的实现 I've even seen some with TYPE.GRAVITY . 我甚至见过一些TYPE.GRAVITY

The most recent seem to suggest TYPE_ROTATION_VECTOR which apparently is a fused sensor( reference ), but example implementations do not seem to be readily available. 最近似乎建议 TYPE_ROTATION_VECTOR显然是一个融合传感器( 参考 ),但示例实现似乎并不容易获得。

I need to use these position/motion sensors, and not GPS, because the user will not be moving during the time when this measurement is needed. 我需要使用这些位置/运动传感器,而不是GPS,因为在需要进行此测量时,用户不会移动。 Also need the measurement to be stable regardless of whether phone is flat, or vertical(as if you are taking a photo) 无论手机是平的还是垂直的(如果你正在拍照),也需要测量稳定

After we pull the degree measurement somehow, converting to cardinal direction seems to be the easy part.( https://stackoverflow.com/a/25349774/1238737 ) 在我们以某种方式拉出度数测量之后,转换到基数方向似乎很容易。( https://stackoverflow.com/a/25349774/1238737

Previous solutions 以前的方案

  1. How to get Direction in Android (Such as North, West) 如何在Android中获取方向(如北,西)
  2. https://stackoverflow.com/a/11068878/1238737 https://stackoverflow.com/a/11068878/1238737

I was working on open source map projects such as OsmAnd, MapsWithMe and MapBox before. 我之前正在研究开源地图项目,如OsmAnd,MapsWithMe和MapBox。 I think these projects are the best available android open sources in the field of map and navigation. 我认为这些项目是地图和导航领域中最好的Android开源。 I've checked their codes and found that MapBox approach to show compass is stable when the phone is vertical then rotating around vertical axis (y). 我检查了他们的代码,发现当手机垂直然后围绕垂直轴(y)旋转时,显示罗盘的MapBox方法是稳定的。 It uses TYPE_ROTATION_VECTOR if the rotation vector sensor is available. 如果旋转矢量传感器可用,它使用TYPE_ROTATION_VECTOR Otherwise it uses TYPE_ORIENTATION sensor or combination of TYPE_ACCELEROMETER and TYPE_MAGNETIC_FIELD . 否则,它使用TYPE_ORIENTATION传感器或TYPE_ACCELEROMETERTYPE_MAGNETIC_FIELD组合。 In case of using TYPE_ACCELEROMETER and TYPE_MAGNETIC_FIELD , it is possible to reduce the oscillation in result by a low-pass filter to achieve smoother values. 在使用TYPE_ACCELEROMETERTYPE_MAGNETIC_FIELD情况下,可以通过低通滤波器减少结果的振荡,以实现更平滑的值。

在此输入图像描述

Here is the compass engine of MapBox and its usage. 这是MapBox的指南针引擎及其用法。
.

LocationComponentCompassEngine.java: LocationComponentCompassEngine.java:

import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.os.SystemClock;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.view.Surface;
import android.view.WindowManager;
import timber.log.Timber;

import java.util.ArrayList;
import java.util.List;

/**
 * This manager class handles compass events such as starting the tracking of device bearing, or
 * when a new compass update occurs.
 */
public class LocationComponentCompassEngine implements SensorEventListener {

    // The rate sensor events will be delivered at. As the Android documentation states, this is only
    // a hint to the system and the events might actually be received faster or slower then this
    // specified rate. Since the minimum Android API levels about 9, we are able to set this value
    // ourselves rather than using one of the provided constants which deliver updates too quickly for
    // our use case. The default is set to 100ms
    private static final int SENSOR_DELAY_MICROS = 100 * 1000;
    // Filtering coefficient 0 < ALPHA < 1
    private static final float ALPHA = 0.45f;

    // Controls the compass update rate in milliseconds
    private static final int COMPASS_UPDATE_RATE_MS = 500;

    private final WindowManager windowManager;
    private final SensorManager sensorManager;
    private final List<CompassListener> compassListeners = new ArrayList<>();

    // Not all devices have a compassSensor
    @Nullable
    private Sensor compassSensor;
    @Nullable
    private Sensor gravitySensor;
    @Nullable
    private Sensor magneticFieldSensor;

    private float[] truncatedRotationVectorValue = new float[4];
    private float[] rotationMatrix = new float[9];
    private float[] rotationVectorValue;
    private float lastHeading;
    private int lastAccuracySensorStatus;

    private long compassUpdateNextTimestamp;
    private float[] gravityValues = new float[3];
    private float[] magneticValues = new float[3];

    /**
     * Construct a new instance of the this class. A internal compass listeners needed to separate it
     * from the cleared list of public listeners.
     */
    LocationComponentCompassEngine(WindowManager windowManager, SensorManager sensorManager) {
        this.windowManager = windowManager;
        this.sensorManager = sensorManager;
        compassSensor = sensorManager.getDefaultSensor(Sensor.TYPE_ROTATION_VECTOR);
        if (compassSensor == null) {
            if (isGyroscopeAvailable()) {
                Timber.d("Rotation vector sensor not supported on device, falling back to orientation.");
                compassSensor = sensorManager.getDefaultSensor(Sensor.TYPE_ORIENTATION);
            } else {
                Timber.d("Rotation vector sensor not supported on device, falling back to accelerometer and magnetic field.");
                gravitySensor = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
                magneticFieldSensor = sensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD);
            }
        }
    }

    public void addCompassListener(@NonNull CompassListener compassListener) {
        if (compassListeners.isEmpty()) {
            onStart();
        }
        compassListeners.add(compassListener);
    }

    public void removeCompassListener(@NonNull CompassListener compassListener) {
        compassListeners.remove(compassListener);
        if (compassListeners.isEmpty()) {
            onStop();
        }
    }

    public int getLastAccuracySensorStatus() {
        return lastAccuracySensorStatus;
    }

    public float getLastHeading() {
        return lastHeading;
    }

    public void onStart() {
        registerSensorListeners();
    }

    public void onStop() {
        unregisterSensorListeners();
    }

    @Override
    public void onSensorChanged(SensorEvent event) {
        // check when the last time the compass was updated, return if too soon.
        long currentTime = SystemClock.elapsedRealtime();
        if (currentTime < compassUpdateNextTimestamp) {
            return;
        }
        if (lastAccuracySensorStatus == SensorManager.SENSOR_STATUS_UNRELIABLE) {
            Timber.d("Compass sensor is unreliable, device calibration is needed.");
            return;
        }
        if (event.sensor.getType() == Sensor.TYPE_ROTATION_VECTOR) {
            rotationVectorValue = getRotationVectorFromSensorEvent(event);
            updateOrientation();

            // Update the compassUpdateNextTimestamp
            compassUpdateNextTimestamp = currentTime + COMPASS_UPDATE_RATE_MS;
        } else if (event.sensor.getType() == Sensor.TYPE_ORIENTATION) {
            notifyCompassChangeListeners((event.values[0] + 360) % 360);
        } else if (event.sensor.getType() == Sensor.TYPE_ACCELEROMETER) {
            gravityValues = lowPassFilter(getRotationVectorFromSensorEvent(event), gravityValues);
            updateOrientation();
        } else if (event.sensor.getType() == Sensor.TYPE_MAGNETIC_FIELD) {
            magneticValues = lowPassFilter(getRotationVectorFromSensorEvent(event), magneticValues);
            updateOrientation();
        }
    }

    @Override
    public void onAccuracyChanged(Sensor sensor, int accuracy) {
        if (lastAccuracySensorStatus != accuracy) {
            for (CompassListener compassListener : compassListeners) {
                compassListener.onCompassAccuracyChange(accuracy);
            }
            lastAccuracySensorStatus = accuracy;
        }
    }

    private boolean isGyroscopeAvailable() {
        return sensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE) != null;
    }

    @SuppressWarnings("SuspiciousNameCombination")
    private void updateOrientation() {
        if (rotationVectorValue != null) {
            SensorManager.getRotationMatrixFromVector(rotationMatrix, rotationVectorValue);
        } else {
            // Get rotation matrix given the gravity and geomagnetic matrices
            SensorManager.getRotationMatrix(rotationMatrix, null, gravityValues, magneticValues);
        }

        final int worldAxisForDeviceAxisX;
        final int worldAxisForDeviceAxisY;

        // Remap the axes as if the device screen was the instrument panel,
        // and adjust the rotation matrix for the device orientation.
        switch (windowManager.getDefaultDisplay().getRotation()) {
            case Surface.ROTATION_90:
                worldAxisForDeviceAxisX = SensorManager.AXIS_Z;
                worldAxisForDeviceAxisY = SensorManager.AXIS_MINUS_X;
                break;
            case Surface.ROTATION_180:
                worldAxisForDeviceAxisX = SensorManager.AXIS_MINUS_X;
                worldAxisForDeviceAxisY = SensorManager.AXIS_MINUS_Z;
                break;
            case Surface.ROTATION_270:
                worldAxisForDeviceAxisX = SensorManager.AXIS_MINUS_Z;
                worldAxisForDeviceAxisY = SensorManager.AXIS_X;
                break;
            case Surface.ROTATION_0:
            default:
                worldAxisForDeviceAxisX = SensorManager.AXIS_X;
                worldAxisForDeviceAxisY = SensorManager.AXIS_Z;
                break;
        }

        float[] adjustedRotationMatrix = new float[9];
        SensorManager.remapCoordinateSystem(rotationMatrix, worldAxisForDeviceAxisX,
                worldAxisForDeviceAxisY, adjustedRotationMatrix);

        // Transform rotation matrix into azimuth/pitch/roll
        float[] orientation = new float[3];
        SensorManager.getOrientation(adjustedRotationMatrix, orientation);

        // The x-axis is all we care about here.
        notifyCompassChangeListeners((float) Math.toDegrees(orientation[0]));
    }

    private void notifyCompassChangeListeners(float heading) {
        for (CompassListener compassListener : compassListeners) {
            compassListener.onCompassChanged(heading);
        }
        lastHeading = heading;
    }

    private void registerSensorListeners() {
        if (isCompassSensorAvailable()) {
            // Does nothing if the sensors already registered.
            sensorManager.registerListener(this, compassSensor, SENSOR_DELAY_MICROS);
        } else {
            sensorManager.registerListener(this, gravitySensor, SENSOR_DELAY_MICROS);
            sensorManager.registerListener(this, magneticFieldSensor, SENSOR_DELAY_MICROS);
        }
    }

    private void unregisterSensorListeners() {
        if (isCompassSensorAvailable()) {
            sensorManager.unregisterListener(this, compassSensor);
        } else {
            sensorManager.unregisterListener(this, gravitySensor);
            sensorManager.unregisterListener(this, magneticFieldSensor);
        }
    }

    private boolean isCompassSensorAvailable() {
        return compassSensor != null;
    }

    /**
     * Helper function, that filters newValues, considering previous values
     *
     * @param newValues      array of float, that contains new data
     * @param smoothedValues array of float, that contains previous state
     * @return float filtered array of float
     */
    private float[] lowPassFilter(float[] newValues, float[] smoothedValues) {
        if (smoothedValues == null) {
            return newValues;
        }
        for (int i = 0; i < newValues.length; i++) {
            smoothedValues[i] = smoothedValues[i] + ALPHA * (newValues[i] - smoothedValues[i]);
        }
        return smoothedValues;
    }

    /**
     * Pulls out the rotation vector from a SensorEvent, with a maximum length
     * vector of four elements to avoid potential compatibility issues.
     *
     * @param event the sensor event
     * @return the events rotation vector, potentially truncated
     */
    @NonNull
    private float[] getRotationVectorFromSensorEvent(@NonNull SensorEvent event) {
        if (event.values.length > 4) {
            // On some Samsung devices SensorManager.getRotationMatrixFromVector
            // appears to throw an exception if rotation vector has length > 4.
            // For the purposes of this class the first 4 values of the
            // rotation vector are sufficient (see crbug.com/335298 for details).
            // Only affects Android 4.3
            System.arraycopy(event.values, 0, truncatedRotationVectorValue, 0, 4);
            return truncatedRotationVectorValue;
        } else {
            return event.values;
        }
    }

    public static float shortestRotation(float heading, float previousHeading) {
        double diff = previousHeading - heading;
        if (diff > 180.0f) {
            heading += 360.0f;
        } else if (diff < -180.0f) {
            heading -= 360.f;
        }
        return heading;
    }

}

CompassListener.java: CompassListener.java:

/**
 * Callbacks related to the compass
 */
public interface CompassListener {

    /**
     * Callback's invoked when a new compass update occurs. You can listen into the compass updates
     * using {@link LocationComponent#addCompassListener(CompassListener)} and implementing these
     * callbacks. Note that this interface is also used internally to to update the UI chevron/arrow.
     *
     * @param userHeading the new compass heading
     */
    void onCompassChanged(float userHeading);

    /**
     * This gets invoked when the compass accuracy status changes from one value to another. It
     * provides an integer value which is identical to the {@code SensorManager} class constants:
     * <ul>
     * <li>{@link android.hardware.SensorManager#SENSOR_STATUS_NO_CONTACT}</li>
     * <li>{@link android.hardware.SensorManager#SENSOR_STATUS_UNRELIABLE}</li>
     * <li>{@link android.hardware.SensorManager#SENSOR_STATUS_ACCURACY_LOW}</li>
     * <li>{@link android.hardware.SensorManager#SENSOR_STATUS_ACCURACY_MEDIUM}</li>
     * <li>{@link android.hardware.SensorManager#SENSOR_STATUS_ACCURACY_HIGH}</li>
     * </ul>
     *
     * @param compassStatus the new accuracy of this sensor, one of
     *                      {@code SensorManager.SENSOR_STATUS_*}
     */
    void onCompassAccuracyChange(int compassStatus);

}

MainActivity.java: MainActivity.java:

import android.content.Context;
import android.hardware.SensorManager;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v7.app.AppCompatActivity;
import android.view.WindowManager;
import android.widget.TextView;

import java.util.Locale;

public class MainActivity extends AppCompatActivity {

    private LocationComponentCompassEngine compassEngine;
    private float previousCompassBearing = -1f;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        final TextView textView = findViewById(R.id.textView);

        CompassListener compassListener = new CompassListener() {

            @Override
            public void onCompassChanged(float targetCompassBearing) {
                if (previousCompassBearing < 0) {
                    previousCompassBearing = targetCompassBearing;
                }
                float normalizedBearing =
                        LocationComponentCompassEngine.shortestRotation(targetCompassBearing, previousCompassBearing);
                previousCompassBearing = targetCompassBearing;

                String status = "NO_CONTACT";
                switch (compassEngine.getLastAccuracySensorStatus()) {
                    case SensorManager.SENSOR_STATUS_NO_CONTACT:
                        status = "NO_CONTACT";
                        break;
                    case SensorManager.SENSOR_STATUS_UNRELIABLE:
                        status = "UNRELIABLE";
                        break;
                    case SensorManager.SENSOR_STATUS_ACCURACY_LOW:
                        status = "ACCURACY_LOW";
                        break;
                    case SensorManager.SENSOR_STATUS_ACCURACY_MEDIUM:
                        status = "ACCURACY_MEDIUM";
                        break;
                    case SensorManager.SENSOR_STATUS_ACCURACY_HIGH:
                        status = "ACCURACY_HIGH";
                        break;
                }

                textView.setText(String.format(Locale.getDefault(),
                        "CompassBearing: %f\nAccuracySensorStatus: %s", normalizedBearing, status));
            }

            @Override
            public void onCompassAccuracyChange(int compassStatus) {
            }

        };

        WindowManager windowManager = (WindowManager) getSystemService(Context.WINDOW_SERVICE);
        SensorManager sensorManager = (SensorManager) getSystemService(Context.SENSOR_SERVICE);
        compassEngine = new LocationComponentCompassEngine(windowManager, sensorManager);
        compassEngine.addCompassListener(compassListener);

        compassEngine.onStart();
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        compassEngine.onStop();
    }

}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM