騰訊位置服務GPS軌跡回放-安卓篇

前言

當我們使用地圖進行開發時,利用已經錄製好的軌跡進行軌跡回放來檢查導航的準確性是十分常用的手段,並且上一篇已經講完了關於地圖使用時GPS軌跡文件的錄製,現在對於安卓系統下使用騰訊導航SDK進行軌跡回放做一個分享

前期準備

騰訊導航SDK依賴於騰訊地圖SDK、騰訊定位SDK,具體權限的開通需要去lbs.qq.com 的官網控制檯去操作,另外導航SDK的權限可以聯繫小助手諮詢(如下圖所示),這裏就不多做探討

16222560693250.jpg

軌跡回放正片

系統架構

16224265311888.jpg

GPS回放系統分成兩部分:GPSPlaybackActivity 和 GPSPlaybackEngine。 GPSPlayback負責和外界的交互,主要是信息的傳遞和導航SDK的交互,而GPSPlaybackEngine負責具體的讀取文件和將定位點通過多線程runnable機制灌入listener。

開始軌跡回放

BaseNaviActivity.java

baseNaviActivity 主要是對於導航SDK naviView部分的生命週期的管理,必須實現,否則不能進行導航!


/**
 * 導航 SDK {@link CarNaviView} 初始化與週期管理類。
 */
public abstract class BaseNaviActivity {

    private static Context mApplicationContext;

    protected CarNaviView mCarNaviView;

    // 建立了TencentCarNaviManager 單例模式,也可以直接調用TencentCarNaviManager來建立自己的carNaviManager
    public static final Singleton<TencentCarNaviManager> mCarManagerSingleton =
            new Singleton<TencentCarNaviManager>() {
                @Override
                protected TencentCarNaviManager create() {
                    return new TencentCarNaviManager(mApplicationContext);
                }
            };

    public static TencentCarNaviManager getCarNaviManager(Context appContext) {
        mApplicationContext = appContext;
        return mCarManagerSingleton.get();
    }

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(getLayoutID());
        super.getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
        mApplicationContext = getApplicationContext();
        mCarNaviView = findViewById(R.id.tnk_car_navi_view);
        mCarManagerSingleton.get().addNaviView(mCarNaviView);
    }

    public int getLayoutID() {
        return R.layout.tnk_activity_navi_base;
    }

    protected View getCarNaviViewChaild() {
        final int count = mCarNaviView.getChildCount();
        if (0 >= count) {
            return mCarNaviView;
        }
        return mCarNaviView.getChildAt(count - 1);
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        if (!isDestoryMap()) {
            return;
        }
        mCarManagerSingleton.get().removeAllNaviViews();
        if (mCarNaviView != null) {
            mCarNaviView.onDestroy();
        }
//        mCarManagerSingleton.destory();
    }

    @Override
    protected void onStart() {
        super.onStart();
        if (mCarNaviView != null) {
            mCarNaviView.onStart();
        }
    }

    @Override
    protected void onRestart() {
        super.onRestart();
        if (mCarNaviView != null) {
            mCarNaviView.onRestart();
        }
    }

    @Override
    protected void onResume() {
        super.onResume();
        if (mCarNaviView != null) {
            mCarNaviView.onResume();
        }
    }

    @Override
    protected void onPause() {
        super.onPause();
        if (mCarNaviView != null) {
            mCarNaviView.onPause();
        }
    }

    @Override
    protected void onStop() {
        super.onStop();
        if (mCarNaviView != null) {
            mCarNaviView.onStop();
        }
    }

    protected boolean isDestoryMap() {
        return true;
    }
}

GPSPlaybackActivity.java

這一部分主要是對於導航 SDK的交互和添加導航UI部分初始化工作。注意導航sdk一定要先算路,再開始導航。算路可以取得GPS文件的首行爲起點,末行爲終點。

用到的fields

    private static final String LOG_TAG = "[GpsPlayback]";

    // gps 文件路徑
    private String mGpsTrackPath;
    // gps 軌跡的起終點
    private NaviPoi mFrom, mTo;

    // 是否是84座標系
    private boolean isLocation84 = true;

因爲在GPSPlaybackEngine已經進行了listener監聽,所以需要對於導航SDK進行灌點

// 騰訊定位sdk的listener
    private TencentLocationListener listener = new TencentLocationListener() {
        @Override
        public void onLocationChanged(TencentLocation tencentLocation, int error, String reason) {
            if (error != TencentLocation.ERROR_OK || tencentLocation == null) {
                return;
            }
            Log.d(LOG_TAG, "onLocationChanged : "
                    + ", latitude" + tencentLocation.getLatitude()
                    + ", longitude: " + tencentLocation.getLongitude()
                    + ", provider: " + tencentLocation.getProvider()
                    + ", accuracy: " + tencentLocation.getAccuracy());

            // 將定位點灌入導航SDK
            // mCarManagerSingleton是使用導航SDK的carNaviManager創建的單例,開發者可以自己實現
            mCarManagerSingleton.get().updateLocation(ConvertHelper
                    .convertToGpsLocation(tencentLocation), error, reason);
        }

        @Override
        public void onStatusUpdate(String provider, int status, String description) {
            Log.d(LOG_TAG, "onStatusUpdate provider: " + provider
                    + ", status: " + status
                    + ", desc: " + description);

            // 更新GPS狀態.
            mCarManagerSingleton.get().updateGpsStatus(provider, status, description);
        }
    };

onCreate方法初始化UI和添加callback

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // 獲取GPS文件軌跡路徑,這裏可以由開發者自己獲取
        mGpsTrackPath = getIntent().getStringExtra("gpsTrackPath");
        if (mGpsTrackPath == null || mGpsTrackPath.isEmpty()) {
            return;
        }

        initUi();
        addTencentCallback();

        new Handler().post(() -> {
        
            // 目的獲取每條軌跡的arraylist
            ArrayList<String> gpsLineStrs = readGpsFile(mGpsTrackPath);
            if (gpsLineStrs == null || gpsLineStrs.isEmpty()) {
                return;
            }
            // 獲取起終點
            getFromAndTo(gpsLineStrs);
            if (mFrom == null || mTo == null) {
                return;
            }
            final Handler handlerUi = new Handler(Looper.getMainLooper());
            handlerUi.post(() -> searchAndStartNavigation());
        });

    }
    

private void initUi() {
        mCarManagerSingleton.get().setInternalTtsEnabled(true);

        final int margin = CommonUtils.dip2px(this, 36);
        // 全覽模式的路線邊距
        mCarNaviView.setVisibleRegionMargin(margin, margin, margin, margin);
        mCarNaviView.setAutoScaleEnabled(true);
        mCarManagerSingleton.get().setMulteRoutes(true);
        mCarNaviView.setNaviMapActionCallback(mCarManagerSingleton.get());

        // 使用默認UI
        CarNaviInfoPanel carNaviInfoPanel = mCarNaviView.showNaviInfoPanel();
        carNaviInfoPanel.setOnNaviInfoListener(() -> {
            mCarManagerSingleton.get().stopNavi();
            finish();
        });
        CarNaviInfoPanel.NaviInfoPanelConfig config = new CarNaviInfoPanel.NaviInfoPanelConfig();
        config.setRerouteViewEnable(true);             // 重算按鈕
        carNaviInfoPanel.setNaviInfoPanelConfig(config);
    }

    private void addTencentCallback() {
        mCarManagerSingleton.get().addTencentNaviCallback(mTencentCallback);
    }
    
    private TencentNaviCallback mTencentCallback = new TencentNaviCallback() {
        @Override
        public void onStartNavi() { }

        @Override
        public void onStopNavi() { }

        @Override
        public void onOffRoute() { }

        @Override
        public void onRecalculateRouteSuccess(int recalculateType,
                                              ArrayList<RouteData> routeDataList) { }
        @Override
        public void onRecalculateRouteSuccessInFence(int recalculateType) { }

        @Override
        public void onRecalculateRouteFailure(int recalculateType,
                                              int errorCode, String errorMessage) { }

        @Override
        public void onRecalculateRouteStarted(int recalculateType) { }

        @Override
        public void onRecalculateRouteCanceled() { }

        @Override
        public int onVoiceBroadcast(NaviTts tts) {
            return 0;
        }

        @Override
        public void onArrivedDestination() { }

        @Override
        public void onPassedWayPoint(int passPointIndex) { }

        @Override
        public void onUpdateRoadType(int roadType) { }

        @Override
        public void onUpdateParallelRoadStatus(ParallelRoadStatus parallelRoadStatus) {

        }

        @Override
        public void onUpdateAttachedLocation(AttachedLocation location) { }

        @Override
        public void onFollowRouteClick(String routeId, ArrayList<LatLng> latLngArrayList) { }
    };

readGpsFile方法

private ArrayList<String> readGpsFile(String fileName) {
        ArrayList<String> gpsLineStrs = new ArrayList<>();
        BufferedReader reader = null;
        try {
            File file = new File(fileName);
            InputStream is = new FileInputStream(file);
            reader = new BufferedReader(new InputStreamReader(is));

            String line;
            while ((line = reader.readLine()) != null) {
                gpsLineStrs.add(line);
            }
            return gpsLineStrs;
        } catch (Exception e) {
            Log.e(LOG_TAG, "startMockTencentLocation Exception", e);
            e.printStackTrace();
        } finally {
            try {
                if (reader != null) {
                    reader.close();
                }
            } catch (Exception e) {
                Log.e(LOG_TAG, "startMockTencentLocation Exception", e);
                e.printStackTrace();
            }
        }
        return null;
    }

getFromAndTo方法,獲取起終點爲進行算路

    private void getFromAndTo(ArrayList<String> gpsLineStrs) {
        final int size;
        if ((size = gpsLineStrs.size()) < 2) {
            return;
        }
        final String firstLine = gpsLineStrs.get(0);
        final String endLine = gpsLineStrs.get(size - 1);
        try {
            final String[] fromParts = firstLine.split(",");
            mFrom = new NaviPoi(Double.valueOf(fromParts[1]), Double.valueOf(fromParts[0]));
            final String[] endParts = endLine.split(",");
            mTo = new NaviPoi(Double.valueOf(endParts[1]), Double.valueOf(endParts[0]));
        } catch (Exception e) {
            mFrom = null;
            mTo = null;
        }
    }

算路searchAndStartNavigation()

可以使用導航SDK的算路方法並且獲取算路成功和失敗的回調


    private void searchAndStartNavigation() {
        mCarManagerSingleton.get()
                .searchRoute(new TencentRouteSearchCallback() {
                    @Override
                    public void onRouteSearchFailure(int i, String s) {
                        toast("路線規劃失敗");
                    }

                    @Override
                    public void onRouteSearchSuccess(ArrayList<RouteData> arrayList) {
                        if (arrayList == null || arrayList.isEmpty()) {
                            toast("未能召回路線");
                            return;
                        }
                        handleGpsPlayback();
                    }
                });
    }
    

調用GpsPlaybackEngine方法,進行listen定位,然後開始導航

    private void handleGpsPlayback() {

// 與GpsPlaybackEngine 進行交互, 添加locationListener
GpsPlaybackEngine.getInstance().addTencentLocationListener(listener);

//與GpsPlaybackEngine 進行交互,開始定位
        GpsPlaybackEngine.getInstance().startMockTencentLocation(mGpsTrackPath, isLocation84);
        try {
            mCarManagerSingleton.get().startNavi(0);
        } catch (Exception e) {
            toast(e.getMessage());
        }
    }

結束導航

    @Override
    protected void onDestroy() {
// 與GpsPlaybackEngine 進行交互, removelocationListener
mCarManagerSingleton.get().removeTencentNaviCallback(mTencentCallback);
//與GpsPlaybackEngine 進行交互,結束定位GpsPlaybackEngine.getInstance().removeTencentLocationListener(listener);
        GpsPlaybackEngine.getInstance().stopMockLocation();
        if (mCarManagerSingleton.get().isNavigating()) {
        // 結束導航
            mCarManagerSingleton.get().stopNavi();
        }
        super.onDestroy();
    }

GPSPlaybackEngine.java

這一部分主要是對於GPS文件進行讀取並且提供外界可用的add/removelistener方法,start/stopMockLocation方法 因爲要讓engine運行在自己的線程,所以使用runnable機制

public class GpsPlaybackEngine implements Runnable{

            // 代碼在下方
}

而使用到的fields

// Tencent軌跡Mock, TencentLocationListener需要利用騰訊定位SDK獲取
private ArrayList<TencentLocationListener> mTencentLocationListeners = new ArrayList<>();
    
// 獲取的location數據
private List<String> mDatas = new ArrayList<String>();
     
private boolean mIsReplaying = false;

private boolean mIsMockTencentLocation = true;

private Thread mMockGpsProviderTask = null;

// 是否已經暫停
private boolean mPause = false;

private double lastPointTime = 0;
private double sleepTime = 0;

關鍵方法

  • listener相關
    // 添加listener
    public void addTencentLocationListener(TencentLocationListener listener) {
        if (listener != null) {
            mTencentLocationListeners.add(listener);
        }
    }

    // 移除listener 
    public void removeTencentLocationListener(TencentLocationListener listener) {
        if (listener != null) {
            mTencentLocationListeners.remove(listener);
        }
    }
  • 開始/關閉模擬軌跡
    /*
     * 模擬軌跡
     * @param context
     * @param fileName 軌跡文件絕對路徑
     */
    public void startMockTencentLocation(String fileName, boolean is84) {

       // 首先清除以前的data
        mDatas.clear();
        // 判斷是否是84座標系
        mIsMockTencentLocation = !is84;
        BufferedReader reader = null;
        try {
            File file = new File(fileName);
            InputStream is = new FileInputStream(file);
            reader = new BufferedReader(new InputStreamReader(is));

            String line;
            while ((line = reader.readLine()) != null) {
                mDatas.add(line);
            }
            if (mDatas.size() > 0) {
                mIsReplaying = true;
                synchronized (this) {
                    mPause = false;
                }
                // 開啓異步線程
                mMockGpsProviderTask = new Thread(this);
                mMockGpsProviderTask.start();
            }
        } catch (Exception e) {
            Log.e(TAG, "startMockTencentLocation Exception", e);
            e.printStackTrace();
        } finally {
            try {
                if (reader != null) {
                    reader.close();
                }
            } catch (Exception e) {
                Log.e(TAG, "startMockTencentLocation Exception", e);
                e.printStackTrace();
            }
        }
    }
    /**
     * 退出應用前也需要調用停止模擬位置,否則手機的正常GPS定位不會恢復
     */
    public void stopMockTencentLocation() {
        try {
            mIsReplaying = false;
            mMockGpsProviderTask.join();
            mMockGpsProviderTask = null;
            lastPointTime = 0;
        } catch (Exception e) {
            Log.e(TAG, "stopMockTencentLocation Exception", e);
            e.printStackTrace();
        }
    }
  • runnable相關
 @Override
    public void run() {
        for (String line : mDatas) {
            if (!mIsReplaying) {
                Log.e(TAG, "stop gps replay");
                break;
            }
            if (TextUtils.isEmpty(line)) {
                continue;
            }

            try {
                Thread.sleep(getSleepTime(line) * 1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            boolean mockResult;
            mockResult = mockTencentLocation(line);
            if (!mockResult) {
                break;
            }

            try {
                checkToPauseThread();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

使用到的private方法

private void checkToPauseThread() throws InterruptedException {
        synchronized (this) {
            while (mPause) {
                wait();
            }
        }
}

private int getSleepTime(String line) {
    try {
        String[] parts = line.split(",");
        double time = Double.valueOf(parts[6]);
        time = (int) Math.floor(time);
        if(lastPointTime != 0) {
            sleepTime = time  - lastPointTime; // 單位s,取整數
        }
        lastPointTime = time;
    }catch (Exception e) {
    
    }
    return (int)sleepTime;
}

private boolean mockTencentLocation(String line) {
    try {
        String[] parts = line.split(",");

        double latitude = Double.valueOf(parts[1]);
        double longitude = Double.valueOf(parts[0]);
        float accuracy = Float.valueOf(parts[2]);
        float bearing = Float.valueOf(parts[3]);
        float speed = Float.valueOf(parts[4]);
        double altitude = Double.valueOf(parts[7]);
        double time = Double.valueOf(parts[6]);

        String buildingId;
        String floorName;
        if (parts.length >= 10) {
            buildingId = parts[8];
            floorName = parts[9];
        } else {
            buildingId = "";
            floorName = "";
        }

        if (!mIsMockTencentLocation) {
            double[] result = CoordinateConverter.wgs84togcj02(longitude, latitude);
            longitude = result[0];
            latitude = result[1];
        }

        GpsPlaybackEngine.MyTencentLocation location = new GpsPlaybackEngine.MyTencentLocation();
        location.setProvider("gps");
        location.setLongitude(longitude);
        location.setLatitude(latitude);
        location.setAccuracy(accuracy);
        location.setDirection(bearing);
        location.setVelocity(speed);
        location.setAltitude(altitude);
        location.setBuildingId(buildingId);
        location.setFloorName(floorName);
        location.setRssi(4);
        location.setTime(System.currentTimeMillis());
//			location.setTime((long) time * 1000);

        for (TencentLocationListener listener : mTencentLocationListeners) {
            if (listener != null) {
                String curTime;
                if (location != null && location.getTime() != 0) {
                    long millisecond = location.getTime();
                    Date date = new Date(millisecond);
                    SimpleDateFormat format = new SimpleDateFormat("yyyy.MM.dd hh:mm:ss");
                    curTime = format.format(date);
                } else {
                    curTime = "null";
                }
                Log.e(TAG, "time : " + curTime
                        + ", longitude : " + longitude
                        + " , latitude : " + latitude);

                listener.onLocationChanged(location, 0, "");
                listener.onStatusUpdate(LocationManager.GPS_PROVIDER, mMockGpsStatus, "");
            }
        }
    } catch(Exception e) {
        Log.e(TAG, "Mock Location Exception", e);
        // 如果未開位置模擬,這裏可能出異常
        e.printStackTrace();
        return false;
    }
    return true;
}

CoordinateConverter.wg84togcj02

	/**
	 * WGS84轉GCJ02(火星座標系)
	 * 
	 * @param lng WGS84座標系的經度
	 * @param lat WGS84座標系的緯度
	 * @return 火星座標數組
	 */
	public static double[] wgs84togcj02(double lng, double lat) {
		if (out_of_china(lng, lat)) {
			return new double[] { lng, lat };
		}
		double dlat = transformlat(lng - 105.0, lat - 35.0);
		double dlng = transformlng(lng - 105.0, lat - 35.0);
		double radlat = lat / 180.0 * pi;
		double magic = Math.sin(radlat);
		magic = 1 - ee * magic * magic;
		double sqrtmagic = Math.sqrt(magic);
		dlat = (dlat * 180.0) / ((a * (1 - ee)) / (magic * sqrtmagic) * pi);
		dlng = (dlng * 180.0) / (a / sqrtmagic * Math.cos(radlat) * pi);
		double mglat = lat + dlat;
		double mglng = lng + dlng;
		return new double[] { mglng, mglat };
	}

內部類MyTencentLocation implements 定位sdk的接口

class MyTencentLocation implements TencentLocation {
        /**
         * 緯度
         */
        private double latitude = 0;
        /**
         * 經度
         */
        private double longitude = 0;
        /**
         * 精度
         */
        private float accuracy = 0;
        /**
         * gps方向
         */
        private float direction = -1;
        /**
         * 速度
         */
        private float velocity = 0;
        /**
         * 時間
         */
        private long time = 0;
        /**
         * 海拔高度
         */
        private double altitude = 0;
        /**
         * 定位來源
         */
        private String provider = "";
        /**
         * GPS信號等級
         */
        private int rssi = 0;

        /**
         * 手機的機頭方向
         */
        private float phoneDirection = -1;

        private String buildingId = "";

        private String floorName = "";

        private  String fusionProvider = "";

        @Override
        public String getProvider() {
            return provider;
        }

        @Override
        public String getSourceProvider() {
            return null;
        }

        @Override
        public String getFusionProvider() {
            return fusionProvider;
        }

        @Override
        public String getCityPhoneCode() {
            return null;
        }

        @Override
        public double getLatitude() {
            return latitude;
        }

        @Override
        public double getLongitude() {
            return longitude;
        }

        @Override
        public double getAltitude() {
            return latitude;
        }

        @Override
        public float getAccuracy() {
            return accuracy;
        }

        @Override
        public String getName() {
            return null;
        }

        @Override
        public String getAddress() {
            return null;
        }

        @Override
        public String getNation() {
            return null;
        }

        @Override
        public String getProvince() {
            return null;
        }

        @Override
        public String getCity() {
            return null;
        }

        @Override
        public String getDistrict() {
            return null;
        }

        @Override
        public String getTown() {
            return null;
        }

        @Override
        public String getVillage() {
            return null;
        }

        @Override
        public String getStreet() {
            return null;
        }

        @Override
        public String getStreetNo() {
            return null;
        }

        @Override
        public Integer getAreaStat() {
            return null;
        }

        @Override
        public List<TencentPoi> getPoiList() {
            return null;
        }

        @Override
        public float getBearing() {
            return direction;
        }

        @Override
        public float getSpeed() {
            return velocity;
        }

        @Override
        public long getTime() {
            return time;
        }

        @Override
        public long getElapsedRealtime() {
            return time;
        }

        @Override
        public int getGPSRssi() {
            return rssi;
        }

        @Override
        public String getIndoorBuildingId() {
            return buildingId;
        }

        @Override
        public String getIndoorBuildingFloor() {
            return floorName;
        }

        @Override
        public int getIndoorLocationType() {
            return 0;
        }

        @Override
        public double getDirection() {
            return phoneDirection;
        }

        @Override
        public String getCityCode() {
            return null;
        }

        @Override
        public TencentMotion getMotion() {
            return null;
        }

        @Override
        public int getGpsQuality() {
            return 0;
        }

        @Override
        public float getDeltaAngle() {
            return 0;
        }

        @Override
        public float getDeltaSpeed() {
            return 0;
        }

        @Override
        public int getCoordinateType() {
            return 0;
        }

        @Override
        public int getFakeReason() {
            return 0;
        }

        @Override
        public int isMockGps() {
            return 0;
        }

        @Override
        public Bundle getExtra() {
            return null;
        }

        @Override
        public int getInOutStatus() {
            return 0;
        }

        public void setLatitude(double latitude) {
            this.latitude = latitude;
        }

        public void setLongitude(double longitude) {
            this.longitude = longitude;
        }

        public void setAccuracy(float accuracy) {
            this.accuracy = accuracy;
        }

        public void setDirection(float direction) {
            this.direction = direction;
        }

        public void setVelocity(float velocity) {
            this.velocity = velocity;
        }

        public void setTime(long time) {
            this.time = time;
        }

        public void setAltitude(double altitude) {
            this.altitude = altitude;
        }

        public void setProvider(String provider) {
            this.provider = provider;
        }

        public void setFusionProvider(String fusionProvider) { this.fusionProvider = fusionProvider; }

        public void setRssi(int rssi) {
            this.rssi = rssi;
        }

        public void setPhoneDirection(float phoneDirection) {
            this.phoneDirection = phoneDirection;
        }

        public void setBuildingId(String buildingId) {
            this.buildingId = buildingId;
        }

        public void setFloorName(String floorName) {
            this.floorName = floorName;
        }
    }

效果展示

最終根據已經錄製好的軌跡(具體錄製方法可以參見上期騰訊位置服務軌跡錄製-安卓篇),從中國技術交易大廈到北京西站的gps軌跡進行回放,並通過導航sdk進行展示如下

tutieshi_640x1386_65s.gif

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章