前言
當(dāng)我們使用地圖進(jìn)行開(kāi)發(fā)時(shí)杈湾,利用已經(jīng)錄制好的軌跡進(jìn)行軌跡回放來(lái)檢查導(dǎo)航的準(zhǔn)確性是十分常用的手段,并且上一篇已經(jīng)講完了關(guān)于地圖使用時(shí)GPS軌跡文件的錄制紊遵,現(xiàn)在對(duì)于安卓系統(tǒng)下使用騰訊導(dǎo)航SDK進(jìn)行軌跡回放做一個(gè)分享
前期準(zhǔn)備
騰訊導(dǎo)航SDK依賴于騰訊地圖SDK蝠检、騰訊定位SDK,具體權(quán)限的開(kāi)通需要去lbs.qq.com 的官網(wǎng)控制臺(tái)去操作,另外導(dǎo)航SDK的權(quán)限可以聯(lián)系小助手咨詢(如下圖所示)肝箱,這里就不多做探討
軌跡回放正片
系統(tǒng)架構(gòu)
GPS回放系統(tǒng)分成兩部分:GPSPlaybackActivity 和 GPSPlaybackEngine。
GPSPlayback負(fù)責(zé)和外界的交互稀蟋,主要是信息的傳遞和導(dǎo)航SDK的交互煌张,而GPSPlaybackEngine負(fù)責(zé)具體的讀取文件和將定位點(diǎn)通過(guò)多線程runnable機(jī)制灌入listener。
開(kāi)始軌跡回放
BaseNaviActivity.java
baseNaviActivity 主要是對(duì)于導(dǎo)航SDK naviView部分的生命周期的管理退客,必須實(shí)現(xiàn)骏融,否則不能進(jìn)行導(dǎo)航!
/**
* 導(dǎo)航 SDK {@link CarNaviView} 初始化與周期管理類萌狂。
*/
public abstract class BaseNaviActivity {
private static Context mApplicationContext;
protected CarNaviView mCarNaviView;
// 建立了TencentCarNaviManager 單例模式档玻,也可以直接調(diào)用TencentCarNaviManager來(lái)建立自己的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
這一部分主要是對(duì)于導(dǎo)航 SDK的交互和添加導(dǎo)航UI部分初始化工作。注意導(dǎo)航sdk一定要先算路茫藏,再開(kāi)始導(dǎo)航误趴。算路可以取得GPS文件的首行為起點(diǎn),末行為終點(diǎn)务傲。
用到的fields
private static final String LOG_TAG = "[GpsPlayback]";
// gps 文件路徑
private String mGpsTrackPath;
// gps 軌跡的起終點(diǎn)
private NaviPoi mFrom, mTo;
// 是否是84坐標(biāo)系
private boolean isLocation84 = true;
因?yàn)樵贕PSPlaybackEngine已經(jīng)進(jìn)行了listener監(jiān)聽(tīng)凉当,所以需要對(duì)于導(dǎo)航SDK進(jìn)行灌點(diǎn)
// 騰訊定位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());
// 將定位點(diǎn)灌入導(dǎo)航SDK
// mCarManagerSingleton是使用導(dǎo)航SDK的carNaviManager創(chuàng)建的單例枣申,開(kāi)發(fā)者可以自己實(shí)現(xiàn)
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狀態(tài).
mCarManagerSingleton.get().updateGpsStatus(provider, status, description);
}
};
onCreate方法初始化UI和添加callback
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 獲取GPS文件軌跡路徑,這里可以由開(kāi)發(fā)者自己獲取
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;
}
// 獲取起終點(diǎn)
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());
// 使用默認(rèn)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方法看杭,獲取起終點(diǎn)為進(jìn)行算路
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()
可以使用導(dǎo)航SDK的算路方法并且獲取算路成功和失敗的回調(diào)
private void searchAndStartNavigation() {
mCarManagerSingleton.get()
.searchRoute(new TencentRouteSearchCallback() {
@Override
public void onRouteSearchFailure(int i, String s) {
toast("路線規(guī)劃失敗");
}
@Override
public void onRouteSearchSuccess(ArrayList<RouteData> arrayList) {
if (arrayList == null || arrayList.isEmpty()) {
toast("未能召回路線");
return;
}
handleGpsPlayback();
}
});
}
調(diào)用GpsPlaybackEngine方法忠藤,進(jìn)行l(wèi)isten定位,然后開(kāi)始導(dǎo)航
private void handleGpsPlayback() {
// 與GpsPlaybackEngine 進(jìn)行交互, 添加locationListener
GpsPlaybackEngine.getInstance().addTencentLocationListener(listener);
//與GpsPlaybackEngine 進(jìn)行交互泊窘,開(kāi)始定位
GpsPlaybackEngine.getInstance().startMockTencentLocation(mGpsTrackPath, isLocation84);
try {
mCarManagerSingleton.get().startNavi(0);
} catch (Exception e) {
toast(e.getMessage());
}
}
結(jié)束導(dǎo)航
@Override
protected void onDestroy() {
// 與GpsPlaybackEngine 進(jìn)行交互, removelocationListener
mCarManagerSingleton.get().removeTencentNaviCallback(mTencentCallback);
//與GpsPlaybackEngine 進(jìn)行交互熄驼,結(jié)束定位GpsPlaybackEngine.getInstance().removeTencentLocationListener(listener);
GpsPlaybackEngine.getInstance().stopMockLocation();
if (mCarManagerSingleton.get().isNavigating()) {
// 結(jié)束導(dǎo)航
mCarManagerSingleton.get().stopNavi();
}
super.onDestroy();
}
GPSPlaybackEngine.java
這一部分主要是對(duì)于GPS文件進(jìn)行讀取并且提供外界可用的add/removelistener方法像寒,start/stopMockLocation方法
因?yàn)橐宔ngine運(yùn)行在自己的線程烘豹,所以使用runnable機(jī)制
public class GpsPlaybackEngine implements Runnable{
// 代碼在下方
}
而使用到的fields
// Tencent軌跡Mock, TencentLocationListener需要利用騰訊定位SDK獲取
private ArrayList<TencentLocationListener> mTencentLocationListeners = new ArrayList<>();
// 獲取的location數(shù)據(jù)
private List<String> mDatas = new ArrayList<String>();
private boolean mIsReplaying = false;
private boolean mIsMockTencentLocation = true;
private Thread mMockGpsProviderTask = null;
// 是否已經(jīng)暫停
private boolean mPause = false;
private double lastPointTime = 0;
private double sleepTime = 0;
關(guān)鍵方法
- listener相關(guān)
// 添加listener
public void addTencentLocationListener(TencentLocationListener listener) {
if (listener != null) {
mTencentLocationListeners.add(listener);
}
}
// 移除listener
public void removeTencentLocationListener(TencentLocationListener listener) {
if (listener != null) {
mTencentLocationListeners.remove(listener);
}
}
- 開(kāi)始/關(guān)閉模擬軌跡
/*
* 模擬軌跡
* @param context
* @param fileName 軌跡文件絕對(duì)路徑
*/
public void startMockTencentLocation(String fileName, boolean is84) {
// 首先清除以前的data
mDatas.clear();
// 判斷是否是84坐標(biāo)系
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;
}
// 開(kāi)啟異步線程
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();
}
}
}
/**
* 退出應(yīng)用前也需要調(diào)用停止模擬位置,否則手機(jī)的正常GPS定位不會(huì)恢復(fù)
*/
public void stopMockTencentLocation() {
try {
mIsReplaying = false;
mMockGpsProviderTask.join();
mMockGpsProviderTask = null;
lastPointTime = 0;
} catch (Exception e) {
Log.e(TAG, "stopMockTencentLocation Exception", e);
e.printStackTrace();
}
}
- runnable相關(guān)
@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诺祸,取整數(shù)
}
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);
// 如果未開(kāi)位置模擬携悯,這里可能出異常
e.printStackTrace();
return false;
}
return true;
}
CoordinateConverter.wg84togcj02
/**
* WGS84轉(zhuǎn)GCJ02(火星坐標(biāo)系)
*
* @param lng WGS84坐標(biāo)系的經(jīng)度
* @param lat WGS84坐標(biāo)系的緯度
* @return 火星坐標(biāo)數(shù)組
*/
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 };
}
內(nèi)部類MyTencentLocation implements 定位sdk的接口
class MyTencentLocation implements TencentLocation {
/**
* 緯度
*/
private double latitude = 0;
/**
* 經(jīng)度
*/
private double longitude = 0;
/**
* 精度
*/
private float accuracy = 0;
/**
* gps方向
*/
private float direction = -1;
/**
* 速度
*/
private float velocity = 0;
/**
* 時(shí)間
*/
private long time = 0;
/**
* 海拔高度
*/
private double altitude = 0;
/**
* 定位來(lái)源
*/
private String provider = "";
/**
* GPS信號(hào)等級(jí)
*/
private int rssi = 0;
/**
* 手機(jī)的機(jī)頭方向
*/
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;
}
}
效果展示
最終根據(jù)已經(jīng)錄制好的軌跡(具體錄制方法可以參見(jiàn)上期騰訊位置服務(wù)軌跡錄制-安卓篇),從中國(guó)技術(shù)交易大廈到北京西站的gps軌跡進(jìn)行回放筷笨,并通過(guò)導(dǎo)航sdk進(jìn)行展示如下
作者:騰訊位置服務(wù)
鏈接:https://my.oschina.net/u/4209404/blog/5048899
來(lái)源:開(kāi)源中國(guó)
著作權(quán)歸作者所有憔鬼。商業(yè)轉(zhuǎn)載請(qǐng)聯(lián)系作者獲得授權(quán),非商業(yè)轉(zhuǎn)載請(qǐng)注明出處胃夏。