當咱們使用地圖進行開發時,利用已經錄製好的軌跡進行軌跡回放來檢查導航的準確性是十分經常使用的手段,而且上一篇已經講完了關於地圖使用時GPS軌跡文件的錄製,如今對於安卓系統下使用騰訊導航SDK進行軌跡回放作一個分享java
騰訊導航SDK依賴於騰訊地圖SDK、騰訊定位SDK,具體權限的開通須要去lbs.qq.com 的官網控制檯去操做,另外導航SDK的權限能夠聯繫小助手諮詢(以下圖所示),這裏就很少作探討android
GPS回放系統分紅兩部分:GPSPlaybackActivity 和 GPSPlaybackEngine。
GPSPlayback負責和外界的交互,主要是信息的傳遞和導航SDK的交互,而GPSPlaybackEngine負責具體的讀取文件和將定位點經過多線程runnable機制灌入listener。git
baseNaviActivity 主要是對於導航SDK naviView部分的生命週期的管理,必須實現,不然不能進行導航!segmentfault
/** * 導航 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; } }
這一部分主要是對於導航 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和添加callbackapp
@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方法,獲取起終點爲進行算路ide
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(); }
這一部分主要是對於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 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(); } }
@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進行展現以下