文章目录
1.简述
小伙伴们在做一些空中、陆地、水下机器人时,通常会为其配套开发一个岸基监控软件,用于显示机器人状态、配置一些参数、下达任务等。通常为了让这个软件更加便于使用(看起来更加高B格),通常会嵌入一个地理地图在其中。在监控软件中嵌入地图有下述好处:
- 可视化机器人的运动轨迹;
- 在地图上选取位置(航点)用于设置机器人的任务。
那么,如何简单而优雅的实现上述好处呢。本文将结合自己的研究经历,描述和分享出我设计出的小玩意,希望对有需求的小伙伴以帮助。
开发岸基监控软件的软件有很多Qt、C#、MFC,目前最流行的应该是Qt了,本人使用的Qt版本为5.12.1。(QWebEngine组件好像要用MSVC编译器,我使用的是MSVC2017)Qt开发的岸基监控软件通过QWebEngine组件加载html形式地图,两者使用QWebChannel进行交互,地图上的功能通过html/JavaScript来实现。地图可以选择百度地图、高德地图、谷歌地图,本人认为百度地图提高的API开发资料是最好的,因此选用百度地图。
最后结合百度地图和Qt写了个轨迹回放软件,并将源码分享给大家。
先看效果:
2.百度地图
2.1百度地图准备
以嵌入百度在线地图为例进行描述。我们使用的是百度地图开放平台的Web开发中的JavaScript API。
点进去后是百度地图的三维GL版本,我们选择平面的图就行。使用百度在线地图,首先需要申请账号和密钥,按照开发指南中一步一步进行下去就可以使用自己的密钥来加载百度在线地图地图。然后非常重要的是示例Demo,里面提供了很多很好的示例。可以直接复制示例代码,在本地新建一个html文件,然后粘贴进行,修改密钥,就可以用浏览器打开了(如果打不开,可以使用下面提供的html模板代码)。示例中地图基础和覆盖物对我们入门有很大的帮助,想要使用更多的用法,还得去看类参考。完成这一步可以先跳到
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="initial-scale=1.0, user-scalable=no" />
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>BDMAP</title>
<style type="text/css">
html {height: 100%}
body {height: 100%;margin: 0px;padding: 0px}
#container {height: 100%}
</style>
<script type="text/javascript" src="http://api.map.baidu.com/getscript?v=3.0&ak=您的密钥"></script>
</head>
<body>
<div id="container"></div>
</body>
</html>
<script type="text/javascript">
var map = new BMap.Map("container"); // 创建地图实例
map.centerAndZoom(new BMap.Point(100.939631,33.078577), 5); // 初始化地图,设置中心点坐标和地图级别
map.enableScrollWheelZoom(); //开启滚轮缩放
map.disableDoubleClickZoom();
map.addControl(new BMap.NavigationControl({ type: BMAP_NAVIGATION_CONTROL_LARGE, showZoomInfo: true }));
map.addControl(new BMap.MapTypeControl({ mapTypes: [BMAP_NORMAL_MAP, BMAP_HYBRID_MAP], anchor: BMAP_ANCHOR_TOP_LEFT, offset: new BMap.Size(80, 50)}));
map.addControl(new BMap.ScaleControl({ anchor: BMAP_ANCHOR_TOP_LEFT }));
</script>
2.2 百度地图开发
本节只针对岸基监控软件的需求开发进行描述。一个是接收岸基监控软件传来的机器人位置,实时显示在地图上,并将其绘制出轨迹。另一个是在地图上标出航点,将航点的经纬度传递给岸基监控软件用以设置机器人任务。
2.2.1实时绘制轨迹
首先声明一个BMap.Marker的覆盖物变量(本人专业领域是海洋机器人,所以就命名了boatMarker)由于实时表示机器人的位置。Marker的图标Icon可以是矢量图标BMap.Symbol,可以形象表示机器人的艏向。矢量图标有多种选择。然后初始化了一个Label跟随Marker显示一些附加信息。boatPoints用于保存实时轨迹的航点序列。boatTrackPolyline则是表示轨迹的折线。
var boatMarker = new BMap.Marker; //艇的marker
var boatIcon = new BMap.Symbol(BMap_Symbol_SHAPE_FORWARD_CLOSED_ARROW, { //BMap_Symbol_SHAPE_PLANE
scale: 1.5,
strokeWeight: 1,
fillColor: "red",
fillOpacity: 0.8
});
var boatLabel = new BMap.Label("0mD,0mH", { offset: new BMap.Size(20, -20) });
boatLabel.setStyle({ color: "black", fontSize: "15px" });
var boatPoints = new Array(); //保存艇的轨迹点
var boatTrackPolyline = new BMap.Polyline([], { strokeColor: "green", strokeWeight: 2, strokeOpacity: 0.5 });
实现接收经纬度,更新机器人位置和绘制轨迹的代码见下。接收到经纬度后,首先通过wgs84tobd09()函数将wgs84坐标系下的位置转换到bd09坐标系下(详细见第4章)。然后使用course来选择矢量图标,最后更新boatMarker,机器人位置更新就实现了。将机器人当前位置push到boatPoints中,然后将最新的轨迹点序列通过setPath覆给折线boatTrackPolyline,然后显示出来就实现了绘制轨迹功能。
function showBoatPosition(lng, lat, course) {
var currentPosition = wgs84tobd09(lng, lat);
var currentPoint = new BMap.Point(currentPosition[0], currentPosition[1]);
boatIcon.setRotation(course);
//boatLabel.setContent(depth.toFixed(1) + "mD,"+ height.toFixed(1) + "mH");
boatMarker.setIcon(boatIcon);
//boatMarker.setLabel(boatLabel);
boatMarker.setPosition(currentPoint);
map.addOverlay(boatMarker);
boatPoints.push(currentPoint);
if (boatPoints.length == 1) {
boatTrackPolyline.setPath(boatPoints);
map.addOverlay(boatTrackPolyline);
}else if(boatPoints.length > 1)
boatTrackPolyline.setPath(boatPoints);
}
那么函数showBoatPosition()由谁调用呢?
首先注册一个通道,passId为mapWidgetInit()函数里这行代码注册的通道明相同channel -> registerObject(“passId”,mapChannel);
boatPosUpdated()是类MapChannel定义的信号成员;
showBoatPosition()是上述html里写的javascript函数。
connect将MapChannel的信号与html里的函数连接起来,就实现了qt->html的数据传递。
new QWebChannel(qt.webChannelTransport,
function(channel){
window.bridge = channel.objects.passId;//注册一个通道,将window.bridge连接到qt下的passId,形成通路
channel.objects.passId.boatPosUpdated.connect(showBoatPosition);
channel.objects.passId.clearTrackClicked.connect(clearTrack);
});
当然,html本身不具备这样的属性,需要辅助工具实现,该工具由qt公司提供:qwebchannel.js可以在 QT安装目录/Examples/Qt-5.x.x/webchannel/shared内找到。将qwebchannel.js放到html同目录下,然后添加下面代码。
<script type="text/javascript" src="qwebchannel.js"></script>
在开始绘制新轨迹前,对之前的轨迹进行清除。
function clearTrack() {
boatPoints = [];
map.removeOverlay(boatTrackPolyline);
}
2.2.2设定航点
给机器人设置任务,总得给他几个经纬度形式的航点,那么在地图上点出航点,然后把这些点的经纬度获取到再发给机器人岂不美哉。下面咱们就实现起来。
添加航点我们使用鼠标绘制工具条库,首先head部分添加下列引用代码。
<script type="text/javascript" src="http://api.map.baidu.com/library/DrawingManager/1.4/src/DrawingManager_min.js"></script>
<link rel="stylesheet" href="http://api.map.baidu.com/library/DrawingManager/1.4/src/DrawingManager_min.css" />
首先定义一些变量来保存绘制出的东西,实例化鼠标绘制工具。最后效果见下图。
var wayPoints = new Array();
var wayPointsPolyline = new BMap.Polyline([], { strokeColor: "red", strokeWeight: 2, strokeOpacity: 0.5, strokeStyle: "dashed"});
var wayPointsMarker = new Array(); //保存点覆盖物
var polylines = new Array(); //保存折线覆盖物
var circles = new Array(); //保存圆覆盖物
var polygon = new Array(); //保存多边形覆盖物
var styleOptions = {
strokeColor: "red", //边线颜色。
fillColor: "red", //填充颜色。当参数为空时,圆形将没有填充效果。
strokeWeight: 2, //边线的宽度,以像素为单位。
strokeOpacity: 0.8, //边线透明度,取值范围0 - 1。
fillOpacity: 0.2, //填充的透明度,取值范围0 - 1。
strokeStyle: 'solid' //边线的样式,solid或dashed。
}
var dashedStyleOptions = {
strokeColor: "red", //边线颜色。
fillColor: "red", //填充颜色。当参数为空时,圆形将没有填充效果。
strokeWeight: 2, //边线的宽度,以像素为单位。
strokeOpacity: 0.5, //边线透明度,取值范围0 - 1。
fillOpacity: 0.2, //填充的透明度,取值范围0 - 1。
strokeStyle: 'dashed' //边线的样式,solid或dashed。
}
//实例化鼠标绘制工具
var drawingManager = new BMapLib.DrawingManager(map, {
isOpen: false, //是否开启绘制模式
enableDrawingTool: true, //是否显示工具栏
enableCalculate: true,
drawingToolOptions: {
anchor: BMAP_ANCHOR_TOP_RIGHT, //位置
offset: new BMap.Size(5, 5), //偏离值
},
circleOptions: styleOptions, //圆的样式
polylineOptions: dashedStyleOptions, //线的样式
polygonOptions: styleOptions, //多边形的样式
rectangleOptions: styleOptions //矩形的样式
});
我们下达航点,最适合用点覆盖物表示,默认的点覆盖物过于单调,做一些优化:对标出的点做编号0,1,2…;将航点依次用虚线连接起来,表示航线;已经标出的航点,可以支持拖拽修改位置;为每个航点设置一个title,内容为该航点的序号+wgs84经纬度。
实现方案:监听DrawingManager的overlaycomplete()事件,绑定响应函数taskOverlaysComplete(),在函数里判断绘制了什么东西,然后做相应的处理。主要优化点绘制:
- 将Marker先push到wayPointsMarker数组中,为每个Marker设置一个Label,Label里写编号;
- 通过调用bd09towgs84函数将bd09坐标转化为wgs84坐标,然后设置为title内容,那么鼠标移动到航点上是,会弹出title内容;
- 启用Marker的拖拽功能,并监听拖拽事件,绑定函数markerDragendEvent()
- 将Marker的bd09位置push到wayPoints的数组中,然后绘制折线。
航点拖拽相应函数markerDragendEvent主要更新wayPoints里存的被拖拽航点的坐标和进而更新虚线。
其他绘制完成就只是将绘制出的覆盖物push到对应的数组里,并绑定了一个“click”相应函数来显示覆盖物的相关信息。
function overlay_style(e) {
var p = e.target;
if (p instanceof BMap.Marker) {
alert("该覆盖物是点,点的坐标是:" + p.getPosition().lng + "," + p.getPosition().lat);
}
else if (p instanceof BMap.Circle) {
var bdCenter = p.getCenter();
var wgsCenter = bd09towgs84(bdCenter.lng, bdCenter.lat);
alert("圆心: " + wgsCenter[0].toFixed(7) + ", " + wgsCenter[1].toFixed(7) + "\n" + "半径: " + p.getRadius().toFixed(1) + "m");
}
else if (p instanceof BMap.Polyline) {
var polylinePoints = p.getPath();
var str = "航线路径点:\n";
for (var i = 0; i < polylinePoints.length; i++) {
var bdPos = polylinePoints[i];
var wgsPos = bd09towgs84(bdPos.lng, bdPos.lat);
str += ("ID" + i + ": " + wgsPos[0].toFixed(7) + ", " + wgsPos[1].toFixed(7) + "\n");
}
alert(str);
} else if (p instanceof BMap.Polygon){
var polygonPoints = p.getPath();
var str = "区域边界点:\n";
for (var i = 0; i < polygonPoints.length; i++) {
var bdPos = polygonPoints[i];
var wgsPos = bd09towgs84(bdPos.lng, bdPos.lat);
str += ("ID" + i + ": " + wgsPos[0].toFixed(7) + ", " + wgsPos[1].toFixed(7) + "\n");
}
alert(str);
}
}
function markerDragendEvent(e) {
var bdPos = e.point;
var wgsPos = bd09towgs84(bdPos.lng, bdPos.lat);
var ID = e.target.getLabel().content;
e.target.setTitle("ID" + ID + ":" + wgsPos[0].toFixed(7) + "," + wgsPos[1].toFixed(7));
wayPoints.splice(ID,1,bdPos);
if(wayPoints.length > 1)
wayPointsPolyline.setPath(wayPoints);
loadWaypoints();
}
function taskOverlaysComplete(e) {
if (e.overlay instanceof BMap.Marker) {
wayPointsMarker.push(e.overlay);
var bdPos = e.overlay.getPosition();
var wgsPos = bd09towgs84(bdPos.lng, bdPos.lat);
var lastIndex = wayPointsMarker.length - 1;
wayPointsMarker[lastIndex].setTitle("ID" + lastIndex + ":" + wgsPos[0].toFixed(7) + "," + wgsPos[1].toFixed(7));
var label = new BMap.Label(wayPointsMarker.length - 1, { offset: new BMap.Size(5, 3) });
label.setStyle({
color: "white",
backgroundColor: "0",
border: "0",
fontsize: "13px"
});
wayPointsMarker[lastIndex].setLabel(label);
wayPointsMarker[lastIndex].enableDragging();
wayPointsMarker[lastIndex].addEventListener("dragend", markerDragendEvent);
wayPoints.push(bdPos);
if(wayPoints.length == 1)
{
wayPointsPolyline.setPath(wayPoints);
map.addOverlay(wayPointsPolyline);
}else if(wayPoints.length > 1)
wayPointsPolyline.setPath(wayPoints);
}
else if (e.overlay instanceof BMap.Polyline) {
e.overlay.addEventListener("click", overlay_style);
e.overlay.enableEditing();
polylines.push(e.overlay);
}
else if (e.overlay instanceof BMap.Circle) {
e.overlay.addEventListener("click", overlay_style);
//e.overlay.enableEditing();
circles.push(e.overlay);
}
else if (e.overlay instanceof BMap.Polygon) {
e.overlay.addEventListener("click", overlay_style);
//e.overlay.enableEditing();
polygon.push(e.overlay);
}
}
drawingManager.addEventListener("overlaycomplete", taskOverlaysComplete);
可以发现,地图上点击的航点都被push到wayPointsMarker中,那么怎么将其传递给qt呢?
下面函函数实现将wayPoints传递给qt。先将航点的个数通过transTask()传递出去,然后再每个航点都通过transPoints()传递出去。
function loadWaypoints() {
window.bridge.transTask(0, wayPointsMarker.length);
for (var i = 0; i < wayPointsMarker.length; i++) {
var wgsPos = bd09towgs84(wayPointsMarker[i].getPosition().lng, wayPointsMarker[i].getPosition().lat);
window.bridge.transPoints(i, wgsPos[0], wgsPos[1]);
}
}
谁来调用loadWaypoints这个函数呢?
有两种方案:1.qt界面上布置一个按钮,然后将触发事件传递过来,就像绘制轨迹那样;2.可以再html上布置按钮或右键菜单来点击触发。显然后者更加方便,那就搞起来。
下面为添加的右键菜单代码,其中loadWaypoints为上述定义的函数。如果在测试该功能是用浏览器打开发现点击鼠标右键出现不了菜单,可以使用qt打开测试,(我的浏览器出现不了,但是qt上嵌入功能正常)。
var menu = new BMap.ContextMenu();
var toolMenuItem = [
{
text: '加载航点',
callback: loadWaypoints
},
{
text: '清除航点',
callback: clearWaypoints
},
{
text: '清除轨迹',
callback: clearTrack
}
];
for (var i = 0; i < toolMenuItem.length; i++) {
menu.addItem(new BMap.MenuItem(toolMenuItem[i].text, toolMenuItem[i].callback, 100));
}
map.addContextMenu(menu);
function clearWaypoints() {
var len = wayPointsMarker.length;
for (var i = 0; i < len; i++)
map.removeOverlay(wayPointsMarker.shift());
wayPoints = [];
map.removeOverlay(wayPointsPolyline);
}
html准备好了,qt那边怎么办呢?
上面提到的transTask()和transPoints()函数是在MapChannel中定义的public slots,可以由html/js直接调用。请移步3.2。
3 QT软件开发
Qt开发的岸基监控软件通过QWebEngine组件加载html形式地图,两者使用QWebChannel进行交互,地图上的功能通过html/JavaScript来实现。
3.1使用QWebEngineView显示Html
使用QWebEngineView需要现在.pro文件中添加webenginewidgets组件。
QT += webenginewidgets
然后再mainwindow.ui拖出一个Widget控件,提升为QWebEngineView(先添加,再提升)。
然后在构造函数中添加出事化代码。其中MapChannel为继承自QObject的类,用于和实现Qt和html/js的交互,该类的实现详细见。
void MainWindow::mapWidgetInit()
{
channel = new QWebChannel(this);
mapChannel = new MapChannel(this);
channel -> registerObject("passId",mapChannel);
this -> ui -> widget_map -> page() -> setWebChannel(channel);
this -> ui -> widget_map -> load(QUrl("file:///./onlinemap/map.html"));
connect(mapChannel,&MapChannel::reloadMapClicked,this,&MainWindow::reloadMap);
}
3.2 MapChannel实现两者交互
两者使用QWebChannel进行交互,地图上的功能通过html/JavaScript来实现。使用QWebChannel时,用来向JS注册的类应该额外编写一个继承自QObject的类,使其只包含交互必要的属性和方法,否则虽然也可以编译运行,但是会在控制台出现很多类似Property xxxx of object xxxx has no notify signal and is not constant…的信息。意思大概是xxxx类的xxxx属性没有通知信号,当属性值在HTML中被修改时qt将无法更新属性值。
可以认为QWebChannel会默认所注册的类中的所有属性都需要进行交互,对其是否有合适的信号和槽函数进行了检测并给出了警告。从这个角度来说也应该让负责交互的类只包含必要的内容,否则可能会在交互时传递很多无用的信息增加程序负担。
下面提供了本人编写的比较全面的一个MapChannel,传递的参数都是double和int基本类型,也可满足应用需求。当然还可以传递Json复杂数据。
以绘制机器人实时轨迹为例,MapChannel定义了一个public属性的updateBoatPos()函数供岸基监控软件应用调用;updateBoatPos()函数里只进行唯一的动作:emit boatPosUpdated(),发射信号;boatPosUpdated()信号在html中被绑定到showBoatPosition()函数上,是不是很绕?因此在监控软件中调用updateBoatPos()函数,就相当于html执行了showBoatPosition()函数,数据也从qt传递到了地图里。
#ifndef MAPCHANNEL_H
#define MAPCHANNEL_H
#include <QObject>
class MapChannel : public QObject
{
Q_OBJECT
public:
explicit MapChannel(QObject *parent = nullptr);
/*MainWindow 调用*/
void addPoint(double lng, double lat);
void movePoint(int id, double lng, double lat);
void updateBoatPos(double lng, double lat, double course);
void setOrigin(double lng, double lat);
void addFencePoint(double,double);
void addFence();
void clearFence();
void clearWaypoints();
void clearTrack();
void panTo(double,double);
signals:
/*MapChannel -> MainWindow*/
void mousePointChanged(double, double);
void reloadMapClicked();
void taskCome(int,int);
void pointsCome(int,double,double);
/*MapChannel -> html*/
void addPointClicked(double,double);
void movePointClicked(int,double,double);
void setOriginPoint(double,double);
void boatPosUpdated(double,double,double);
void addFencePointClicked(double,double);
void addFenceClicked();
void clearFenceClicked();
void clearWaypointsClicked();
void clearAllClicked();
void clearTrackClicked();
void panToClicked(double,double);
public slots:
/*html调用*/
void getMousePoint(double lng, double lat);
void reloadMap();
void transTask(int type, int len);
void transPoints(int id, double lng, double lat);
/*MainWindow 调用*/
void clearAll();
};
#endif // MAPCHANNEL_H
#include "mapchannel.h"
MapChannel::MapChannel(QObject *parent) : QObject(parent)
{
}
void MapChannel::getMousePoint(double lng, double lat)
{
emit mousePointChanged(lng,lat);
}
void MapChannel::addPoint(double lng, double lat)
{
emit addPointClicked(lng,lat);
}
void MapChannel::movePoint(int id, double lng, double lat)
{
emit movePointClicked(id,lng,lat);
}
void MapChannel::transTask(int type, int len)
{
emit taskCome(type,len);
}
void MapChannel::transPoints(int id, double lng, double lat)
{
emit pointsCome(id,lng,lat);
}
void MapChannel::updateBoatPos(double lng, double lat, double course)
{
emit boatPosUpdated(lng,lat,course);
}
void MapChannel::reloadMap()
{
emit reloadMapClicked();
}
void MapChannel::setOrigin(double lng, double lat)
{
emit setOriginPoint(lng,lat);
}
void MapChannel::clearWaypoints()
{
emit clearWaypointsClicked();
}
void MapChannel::clearAll()
{
emit clearAllClicked();
}
void MapChannel::addFencePoint(double lng, double lat)
{
emit addFencePointClicked(lng,lat);
}
void MapChannel::addFence()
{
emit addFenceClicked();
}
void MapChannel::clearFence()
{
emit clearFenceClicked();
}
void MapChannel::panTo(double lng, double lat)
{
emit panToClicked(lng,lat);
}
void MapChannel::clearTrack()
{
emit clearTrackClicked();
}
qt接收html航点,MapChannel了几个public slots,可以直接在html/js中直接以window.bridge.XXX的形式调用。transPoints()函数里只做一件事:emit pointsCome(id,lng,lat),发射信号。因此在外部可以连接&MapChannel::pointsCome和定义好的槽函数来接收由html传来的经纬度。实现了由html->qt的数据传递。
connect(mapChannel,&MapChannel::pointsCome,[](int index, double lng, double lat){
qDebug()<<index<<QString::number(lng,'f',6)<<QString::number(lat,'f',6);
});
4.关于地图坐标
常用的gnss定位模块得到的经纬度都是WGS84系(地心坐标系,GPS原始坐标体系)下的坐标。国内,任何一个地图产品都不允许直接使用GPS坐标,据说是为了保密。
国家规定,国内地图坐标必须至少使用GCJ-02(国测局02年发布的坐标体系,它是一种对经纬度数据的加密算法,即加入随机的偏差)进行首次加密。高德、谷歌地图在国内采用的为GCJ-02加密后的坐标。
而国内的百度地图在GCJ-02的基础上又进行了二次非线性加偏得到BD09坐标。
因此需要WGS84与BD09之间相互转换。百度API提供精确的WGS84->BD09的转换服务,但是逆向不提供。该服务有请求次数,且需要联网。
因此网上搜索了一些大佬的资料,总结出以下转换公式,放在CoordinateTransform.js文件中,经测试精度在5m以内。
// CoordinateTransform.js
var x_PI = 3.14159265358979324 * 3000.0 / 180.0;
var PI = 3.1415926535897932384626;
var a = 6378245.0;
var ee = 0.00669342162296594323;
function wgs84tobd09(lng, lat) {
var mid = wgs84togcj02(lng, lat);
mid = gcj02tobd09(mid[0], mid[1]);
return mid
}
function bd09towgs84(lng, lat) {
var mid = bd09togcj02(lng, lat);
mid = gcj02towgs84(mid[0], mid[1]);
return mid
}
function bd09togcj02(bd_lon, bd_lat) {
var x_pi = 3.14159265358979324 * 3000.0 / 180.0;
var x = bd_lon - 0.0065;
var y = bd_lat - 0.006;
var z = Math.sqrt(x * x + y * y) - 0.00002 * Math.sin(y * x_pi);
var theta = Math.atan2(y, x) - 0.000003 * Math.cos(x * x_pi);
var gg_lng = z * Math.cos(theta);
var gg_lat = z * Math.sin(theta);
return [gg_lng, gg_lat]
}
function gcj02towgs84(lng, lat) {
if (out_of_china(lng, lat)) {
return [lng, lat]
}
else {
var dlat = transformlat(lng - 105.0, lat - 35.0);
var dlng = transformlng(lng - 105.0, lat - 35.0);
var radlat = lat / 180.0 * PI;
var magic = Math.sin(radlat);
magic = 1 - ee * magic * magic;
var sqrtmagic = Math.sqrt(magic);
dlat = (dlat * 180.0) / ((a * (1 - ee)) / (magic * sqrtmagic) * PI);
dlng = (dlng * 180.0) / (a / sqrtmagic * Math.cos(radlat) * PI);
mglat = lat + dlat;
mglng = lng + dlng;
return [lng * 2 - mglng, lat * 2 - mglat]
}
}
function wgs84togcj02(lng, lat) {
if (out_of_china(lng, lat)) {
return [lng, lat]
}
else {
var dlat = transformlat(lng - 105.0, lat - 35.0);
var dlng = transformlng(lng - 105.0, lat - 35.0);
var radlat = lat / 180.0 * PI;
var magic = Math.sin(radlat);
magic = 1 - ee * magic * magic;
var sqrtmagic = Math.sqrt(magic);
dlat = (dlat * 180.0) / ((a * (1 - ee)) / (magic * sqrtmagic) * PI);
dlng = (dlng * 180.0) / (a / sqrtmagic * Math.cos(radlat) * PI);
var mglat = lat + dlat;
var mglng = lng + dlng;
return [mglng, mglat]
}
}
function gcj02tobd09(lng, lat) {
var z = Math.sqrt(lng * lng + lat * lat) + 0.00002 * Math.sin(lat * x_PI);
var theta = Math.atan2(lat, lng) + 0.000003 * Math.cos(lng * x_PI);
var bd_lng = z * Math.cos(theta) + 0.0065;
var bd_lat = z * Math.sin(theta) + 0.006;
return [bd_lng, bd_lat]
}
function transformlat(lng, lat) {
var ret = -100.0 + 2.0 * lng + 3.0 * lat + 0.2 * lat * lat + 0.1 * lng * lat + 0.2 * Math.sqrt(Math.abs(lng));
ret += (20.0 * Math.sin(6.0 * lng * PI) + 20.0 * Math.sin(2.0 * lng * PI)) * 2.0 / 3.0;
ret += (20.0 * Math.sin(lat * PI) + 40.0 * Math.sin(lat / 3.0 * PI)) * 2.0 / 3.0;
ret += (160.0 * Math.sin(lat / 12.0 * PI) + 320 * Math.sin(lat * PI / 30.0)) * 2.0 / 3.0;
return ret
}
function transformlng(lng, lat) {
var ret = 300.0 + lng + 2.0 * lat + 0.1 * lng * lng + 0.1 * lng * lat + 0.1 * Math.sqrt(Math.abs(lng));
ret += (20.0 * Math.sin(6.0 * lng * PI) + 20.0 * Math.sin(2.0 * lng * PI)) * 2.0 / 3.0;
ret += (20.0 * Math.sin(lng * PI) + 40.0 * Math.sin(lng / 3.0 * PI)) * 2.0 / 3.0;
ret += (150.0 * Math.sin(lng / 12.0 * PI) + 300.0 * Math.sin(lng / 30.0 * PI)) * 2.0 / 3.0;
return ret
}
function out_of_china(lng, lat) {
return (lng < 72.004 || lng > 137.8347) || ((lat < 0.8293 || lat > 55.8271) || false);
}
5.轨迹回放软件
使用上面开发的地图,和测试用的qt程序,最后整理整理写了个轨迹回放软件,实际效果为本页最上方的gif。
目前这个软件只能回放csv格式的机器人航行数据。
1.点击“打开文件”按钮进行CSV文件选择;
2.在“数据结构”中调整时间戳、经纬度、艏向数据所在的列索引,索引从0开始;
3.在“实时数据”中可观察对应数字是否正确;
4.点击“开始回放”可在地图上动态显示轨迹;
5.播放速度在回放过程中可随时调整,默认1秒一行;
实现起来非常简单,如果有需要可以自行下载源码。
6.其他事项
6.1源码
在线百度地图map.html、坐标转换文件CoordinateTransform.js、qwebchannel.js和mapchannel.cpp和mapchannel.h上传到的github上,点击此处。里面已经实现的功能比文中描述的要丰富些,可自行探索。
轨迹回放软件,github点击此处。
大家觉得有用给个star或fork,谢谢!
同时,放一份在csdn上。
6.2离线地图
在线地图需要在可访问公网时才能可用,有时外场试验不方便联网,在线地图就不可行了。但是,网上已有大佬将百度地图API的功能全部离线化,做成离线的包,仅需简单替换上述功能都可实现。
6.3测试数据
期望轨迹的json文件:pointsNum为航点总数,waypoints里依次添加航点信息。
{
"pointsNum": 5,
"taskType": 1,
"waypoints": {
"0": {
"lat": 22.02006017515486,
"lng": 113.69963058258398
},
"1": {
"lat": 22.01925886690893,
"lng": 113.69904159627043
},
"2": {
"lat": 22.019942999974397,
"lng": 113.69885366284181
},
"3": {
"lat": 22.01974295857208,
"lng": 113.69811509010164
},
"4": {
"lat": 22.02050379155471,
"lng": 113.69851990734364
}
}
}
航行数据csv文件,视频中测试用的csv一行是这样的。列编号从0开始。第1列是经度,第2列是纬度,第5列是艏向(0-360度)。
如果你的航行数据不一样,在界面中选中对应的列就行。
2,113.699745,22.020905,93.986214,11.537592,149.738998,0.054351,0.7,0.520241,0,0,0,3.26374,16: 1: 29
7.参考资料
在学习过程中,浏览到很了多大佬总结的经验和资料,受益匪浅,有些直接交流为我答疑解惑。受到他们无私奉献精神的鼓舞,我也写了这篇博客。再次感谢他们: