Preface
When we use the map for development, it is a very common method to check the accuracy of the navigation by using the recorded track to replay the track, and the previous article has already finished the recording of the GPS track file when the map is used. Now for the Android system Use Tencent Navigation SDK for track playback to share
Preliminary preparation
Tencent Navigation SDK relies on Tencent Map SDK and Tencent Location SDK. The specific permissions need to go to the official website console of lbs.qq.com to operate. In addition, the permissions of the navigation SDK can be contacted by the assistant (as shown in the figure below). Not much discussion here
Track playback feature film
system structure
The GPS playback system is divided into two parts: GPSPlaybackActivity and GPSPlaybackEngine.
GPSPlayback is responsible for the interaction with the outside world, mainly the transmission of information and the interaction of the navigation SDK, while GPSPlaybackEngine is responsible for the specific reading of files and injecting positioning points into the listener through the multithreaded runnable mechanism.
Start track playback
BaseNaviActivity.java
baseNaviActivity is mainly for the management of the life cycle of the naviView part of the navigation SDK. It must be implemented, otherwise the navigation cannot be performed!
/**
* 导航 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
This part is mainly for the interaction of the navigation SDK and the initialization of the navigation UI part. Note that the navigation sdk must first calculate the road, and then start the navigation. Calculating the path can get the first line of the GPS file as the starting point and the end line of the end point.
Used fields
private static final String LOG_TAG = "[GpsPlayback]";
// gps 文件路径
private String mGpsTrackPath;
// gps 轨迹的起终点
private NaviPoi mFrom, mTo;
// 是否是84坐标系
private boolean isLocation84 = true;
Because the listener has been monitored in GPSPlaybackEngine, it is necessary to fill in the navigation 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);
}
};
The onCreate method initializes the UI and adds 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 method
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 method, get the start and end points for calculating the way
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;
}
}
Calculate the road 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();
}
});
}
Call the GpsPlaybackEngine method, perform listen positioning, and then start navigation
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());
}
}
End navigation
@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
This part is mainly to read GPS files and provide externally available add/removelistener methods, start/stopMockLocation methods
Because we want the engine to run on its own thread, we use the runnable mechanism
public class GpsPlaybackEngine implements Runnable{
// 代码在下方
}
And the fields used
// 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;
Key method
- listener related
// 添加listener
public void addTencentLocationListener(TencentLocationListener listener) {
if (listener != null) {
mTencentLocationListeners.add(listener);
}
}
// 移除listener
public void removeTencentLocationListener(TencentLocationListener listener) {
if (listener != null) {
mTencentLocationListeners.remove(listener);
}
}
- Start/close simulation track
/*
* 模拟轨迹
* @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 related
@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 method used
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 };
}
The internal class MyTencentLocation implements the interface to locate the 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;
}
}
Show results
Finally, according to the recorded track (for the specific recording method, please refer to the last issue of Tencent Location Service Track Recording-Android ), play back the gps track from China Technology Exchange Building to Beijing West Railway Station, and display it through the navigation sdk as follows
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。