• 98阅读
  • 0回复

有难度哦/Qt基于通用地图组件实现航迹规划和模拟/动态标注轨迹线/带序号和方向箭头指示 [复制链接]

上一主题 下一主题
离线liudianwu
 



## 一、前言说明
### 1、功能概述
航迹规划功能允许用户在地图上通过单击操作逐个添加航线途经点,系统自动生成带有方向指示的连续航迹线,并支持对航线进行动态编辑。主要功能包括:

- 支持在地图上单击添加标注点,点位按添加顺序自动递增编号;
- 自动生成带箭头方向指示的航迹线,清晰展示航行方向;
- 实现航线标注点的拖曳编辑,实时更新航迹与箭头方向;
- 支持删除指定航点或清空全部航点,删除后自动重排序并重绘航线;
- 地图上的标注点与表格数据联动:点击地图点可选中对应表格行,选中表格行时地图点自动高亮;
- 航点支持带序号图标的可视化显示,提升可读性;
- 航线数据支持自动加载与持久化保存,保障用户体验连续性。

### 2、核心难点与技术实现

#### 2.1 **通用化方向箭头绘制**

尽管部分主流地图平台(如百度地图、高德地图)提供了原生的箭头线绘制接口,但其样式固定、扩展性差,难以满足多样化业务场景的需求。为此,系统采用**基于标注点的通用化箭头实现方案**,确保兼容所有支持标注点(Marker)的地图组件。

具体实现思路如下:
- 在每两个相邻航点之间,计算其连线的方位角(即方向角);
- 根据计算出的角度,动态生成一个代表箭头方向的标注点;
- 该箭头标注点使用自定义图标(如SVG箭头图像),并通过设置旋转属性(rotation)与航向保持一致;
- 箭头图标可灵活替换,仅需更换图片资源即可适配不同视觉风格,极大增强了系统的可维护性与一致性。

此方法避免了依赖特定地图厂商的高级绘图接口,实现了“一次开发,多平台适配”的目标。

#### 2.2 **航点拖曳编辑与实时更新机制**

为提升交互体验,系统支持用户直接在地图上拖动航点以调整航线。此功能的核心挑战在于:**拖动一个航点时,需同步更新与其相邻的两条航段及其对应的箭头方向**。

实现方案如下:
- 为每个航点标注绑定“拖曳开始”与“拖曳结束”事件监听;
- 拖曳过程中,实时获取当前鼠标位置,更新该航点坐标;
- 动态重新计算受影响的前后两段航迹线的坐标数据,并刷新对应的折线(Polyline)对象;
- 同时重新计算相邻两段航迹的方向角,更新前后两个箭头标注点的旋转角度;
- 拖曳结束后,触发数据持久化,保存最新航线状态。

得益于前期地图组件良好的事件封装机制,各类覆盖物的事件监听可按需绑定与解绑,保证了系统性能与稳定性。



## 二、效果图



## 三、代码使用
```cpp
#include "frmmapdrawmarkerline.h"
#include "ui_frmmapdrawmarkerline.h"
#include "qthelper.h"
#include "maphelper.h"
#include "webview.h"
#include "mapdrawmarkerline.h"

frmMapDrawMarkerLine::frmMapDrawMarkerLine(QWidget *parent) : QWidget(parent), ui(new Ui::frmMapDrawMarkerLine)
{
    ui->setupUi(this);
    this->initForm();
    this->initTable();
    this->loadTable();

    //mapObj->load();
    QTimer::singleShot(500, mapObj, SLOT(load()));
    //QMetaObject::invokeMethod(mapObj, "load", Qt::QueuedConnection);
}

frmMapDrawMarkerLine::~frmMapDrawMarkerLine()
{
    delete ui;
}

void frmMapDrawMarkerLine::initForm()
{
    //设置右侧固定宽度
    ui->frameRight->setFixedWidth(AppData::RightWidth);

    flag = "moveMarker";
    center = "121.56358,31.11566";

    //实例化浏览器控件并加入到布局
    webView = new WebView(this);
    webView->setLayout(ui->gridLayout);
    connect(webView, SIGNAL(loadSuccess()), this, SLOT(on_btnDrawMarkerLine_clicked()));

    //实例化地图类
    MapCore mapCore = (MapCore)AppConfig::MapDrawCore;    
    int zoom = MapHelper::getMapZoom(mapCore, this->objectName());
    mapObj = MapHelper::getMapObj(this, mapCore);
    mapObj->setWebView(webView);
    mapObj->setSaveFile(SaveFile);
    mapObj->setMapLocal(AppConfig::MapDrawLocal);
    mapObj->setMapType(AppConfig::MapDrawType);
    mapObj->setCenterPoint(center);
    mapObj->setZoom(zoom);

    //实例化封装类
    drawMarkerLine = new MapDrawMarkerLine(this);
    connect(drawMarkerLine, SIGNAL(updatePoints()), this, SLOT(on_btnGetMarkerLine_clicked()));
    connect(drawMarkerLine, SIGNAL(markerClick(QString)), this, SLOT(markerClick(QString)));
    connect(drawMarkerLine, SIGNAL(receivePoints(QStringList)), this, SLOT(receivePoints(QStringList)));
    connect(webView, SIGNAL(receiveDataFromJs(QString, QVariant)), drawMarkerLine, SLOT(receiveDataFromJs(QString, QVariant)));
}

void frmMapDrawMarkerLine::initTable()
{
    QList<QString> columnNames;
    QList<int> columnWidths;
    columnNames << "经纬度";
    columnWidths << 50;

    int columnCount = columnNames.count();
    ui->tableWidget->setColumnCount(columnCount);
    ui->tableWidget->setHorizontalHeaderLabels(columnNames);
    for (int i = 0; i < columnCount; ++i) {
        ui->tableWidget->setColumnWidth(i, columnWidths.at(i));
    }

    //通用函数设置表格控件
    QtHelper::initTableView(ui->tableWidget, 25, true);
}

void frmMapDrawMarkerLine::loadTable()
{
    int count = AppConfig::MarkerPoints.count();
    ui->tableWidget->setRowCount(count);
    for (int i = 0; i < count; ++i) {
        QString point = AppConfig::MarkerPoints.at(i);
        ui->tableWidget->setItem(i, 0, new QTableWidgetItem(point));
    }
}

void frmMapDrawMarkerLine::loadPoint()
{
    QStringList points;
    int count = ui->tableWidget->rowCount();
    for (int i = 0; i < count; ++i) {
        points << ui->tableWidget->item(i, 0)->text();
    }

    //设置默认中心点/没有数据的时候取一个/有的话从坐标点集合取第一个
    QString center = points.count() == 0 ? "121.56358,31.11566" : points.first();
    drawMarkerLine->clear();
    drawMarkerLine->setPara(points);
    drawMarkerLine->init("test", center, 0);
}

void frmMapDrawMarkerLine::runJs(const QString &js)
{
    mapObj->runJs(js);
}

void frmMapDrawMarkerLine::markerClick(const QString &point)
{
    //先恢复上一个图标
    QString p = ui->txtPoint->text();
    if (!p.isEmpty()) {
        int index = AppConfig::MarkerPoints.lastIndexOf(p);
        QString flag = QString("marker%1_%0").arg("test").arg(index);
        QString image = MapHelper::getMarkerIcon("blue", index);
        this->runJs(QString("setMarker('%1', null, null, null, '%2')").arg(flag).arg(image));
    }

    //切换选中图标
    int index = AppConfig::MarkerPoints.lastIndexOf(point);
    QString flag = QString("marker%1_%0").arg("test").arg(index);
    QString image = MapHelper::getMarkerIcon("yellow", index);
    this->runJs(QString("setMarker('%1', null, null, null, '%2')").arg(flag).arg(image));

    ui->txtPoint->setText(point);
    ui->tableWidget->selectRow(index);
}

void frmMapDrawMarkerLine::receivePoints(const QStringList &points)
{
    AppConfig::MarkerPoints = points;
    AppConfig::writeConfig();
    this->loadTable();
}

void frmMapDrawMarkerLine::on_btnDrawMarkerLine_clicked()
{
    //开启标注后禁用双击放大
    if (ui->btnDrawMarkerLine->text() == "开启标注") {
        mapObj->setEnable(EnableType_DoubleClickZoom, false);
        drawMarkerLine->setMapObj(mapObj);
        ui->btnDrawMarkerLine->setText("停止标注");
        this->loadPoint();
    } else {
        mapObj->setEnable(EnableType_DoubleClickZoom, true);
        drawMarkerLine->stop();
        drawMarkerLine->setMapObj(NULL);
        ui->btnDrawMarkerLine->setText("开启标注");
    }
}

void frmMapDrawMarkerLine::on_btnGetMarkerLine_clicked()
{
    drawMarkerLine->getPoints();
    int row = ui->tableWidget->currentRow();
    if (row > 0) {
        ui->txtPoint->setText(ui->tableWidget->item(row, 0)->text());
    }
}

void frmMapDrawMarkerLine::on_btnDeleteMarkerLine_clicked()
{
    if (ui->btnDrawMarkerLine->text() == "开启标注") {
        return;
    }

    //移除单个点并重新加载
    QString point = ui->txtPoint->text().trimmed();
    if (point.contains(",")) {
        AppConfig::MarkerPoints.removeOne(point);
        AppConfig::writeConfig();
        this->loadTable();
        this->loadPoint();
        on_tableWidget_itemSelectionChanged();
    }
}

void frmMapDrawMarkerLine::on_btnClearMarkerLine_clicked()
{
    if (ui->btnDrawMarkerLine->text() == "开启标注") {
        return;
    }

    if (QtHelper::showMessageBoxQuestion("确定要清空吗? 清空后无法恢复!") == QMessageBox::Yes) {
        AppConfig::MarkerPoints.clear();
        AppConfig::writeConfig();
        this->loadTable();
        this->loadPoint();
    }
}

void frmMapDrawMarkerLine::on_tableWidget_itemSelectionChanged()
{
    int row = ui->tableWidget->currentRow();
    if (row >= 0) {
        QString point = ui->tableWidget->item(row, 0)->text();
        this->markerClick(point);
    }
}

void frmMapDrawMarkerLine::on_btnStart_clicked()
{
    //没有一个数据则不用继续
    QStringList datas = AppConfig::MarkerPoints;
    if (datas.count() <= 0) {
        return;
    }

    ui->btnPause->setText("暂停回放");
    if (ui->btnStart->text() == "开始回放") {
        QString image = MapHelper::getFlyIcon(mapObj);
        this->runJs(QString("removeMoveMarker('%1')").arg(flag));
        this->runJs(QString("addMove('%1', '%2', %3, true, true, '%4', 48, 48)").arg(flag).arg(datas.join("|")).arg(500).arg(image));
        this->runJs(QString("moveStart('%1')").arg(flag));
        ui->btnStart->setText("停止回放");
        ui->widgetBtn->setEnabled(false);
    } else {
        this->runJs(QString("moveStop('%1')").arg(flag));
        ui->btnStart->setText("开始回放");
        ui->widgetBtn->setEnabled(true);
    }
}

void frmMapDrawMarkerLine::on_btnPause_clicked()
{
    //启动阶段才能暂停
    if (ui->btnStart->text() == "开始回放") {
        return;
    }

    if (ui->btnPause->text() == "暂停回放") {
        this->runJs(QString("movePause('%1')").arg(flag));
        ui->btnPause->setText("继续回放");
    } else {
        this->runJs(QString("moveNext('%1')").arg(flag));
        ui->btnPause->setText("暂停回放");
    }
}
```

## 四、相关地址
1. 国内站点:[https://gitee.com/feiyangqingyun](https://gitee.com/feiyangqingyun)
2. 国际站点:[https://github.com/feiyangqingyun](https://github.com/feiyangqingyun)
3. 个人作品:[https://blog.csdn.net/feiyangqingyun/article/details/97565652](https://blog.csdn.net/feiyangqingyun/article/details/97565652)
4. 文件地址:[https://pan.baidu.com/s/1ZxG-oyUKe286LPMPxOrO2A](https://pan.baidu.com/s/1ZxG-oyUKe286LPMPxOrO2A) 提取码:o05q  文件名:bin_map.zip

## 五、功能特点
### 5.1 地图功能
1. 支持多种地图内核,默认采用百度地图,可选高德地图、天地图、腾讯地图、谷歌地图等。
2. 同时支持在线地图和离线地图两种模式,离线地图方便在不联网的场景中使用。
3. 支持各种地图控件的启用,比如地图导航、地图类型、缩略图、比例尺、全景导航、实时路况、绘图工具、结果面板等。
4. 支持多种地图功能的动态启用禁用,比如地图拖曳、键盘操作、滚轮缩放、双击放大、连续缩放、地图测距等。
5. 提供众多js函数接口用于交互,参数极其丰富,能够想到的应用场景需求都有。
6. 统一的信号槽机制,地图中的结果统一信号发送出去,收到后根据type类型区分。
7. 支持地图交互,比如鼠标按下获取对应位置的经纬度。单击标注点弹出对应点的信息。
8. 支持添加标注、删除标注、移动标注、清空标注。
9. 标注点可以指定图标图片和尺寸,支持gif动图,支持指定以图片中心对齐还是底部中心对齐。可以设置旋转角度,带富文本提示信息。
10. 标注点事件支持单击发信号通知和自己弹框显示信息。
11. 提供地址转坐标和坐标转地址接口。
12. 支持各种图形绘制,包括折线图、多边形、矩形、圆形、弧线等。
13. 可显示悬浮的绘图工具栏,直接在地图上划线、标注点、矩形、圆形等。
14. 支持各种区域搜索,比如矩形区域、圆形区域,可以按照关键字匹配将搜索结果显示在地图中。
15. 可动态添加离线的行政区边界点数据。可以搜索行政区划并获取该区域的边界点数据。数据可以保存到文件以便离线使用。
16. 支持点聚合功能,多个小标注点合并到一个大标注点,防止点密集导致交互不友好。
17. 可以添加海量点,每个点都可以单击获取对应坐标和信息。
18. 所有的覆盖物信息比如标注点、矩形、多边形、折线图等,都可以主动获取对应的信息比如坐标点和路径等。
19. 支持路径规划,支持公交路线、自驾路线、步行路线、骑行路线,不同查询支持不同策略,可选最少时间、最少换乘、不走高架等。
20. 路径规划结果可以显示在地图中,也可以获取到路径点坐标集合。这个数据可以保存到文件,以便发给机器人或者无人机做导航用来轨迹移动。
21. 可以设置不同的地图视图比如街道图、卫星图、混合图。
22. 可以设置不同的样式,比如午夜蓝、青草绿等样式风格。
23. 可以设置地图的旋转角度和倾斜角度。
24. 提供经纬度坐标纠偏转换功能,比如传入的GPS坐标需要转换到百度地图坐标或者高德地图坐标。各种坐标系转换全部离线函数,支持地球坐标系WGS-84、火星坐标系GCJ-02、百度坐标系BD-09之间的互相转换,涵盖了各种地图的坐标系。
25. 提供动态轨迹点移动功能,按照给定的经纬度坐标集合平滑移动。
26. 同时支持qwidget和qml,支持编译到安卓系统运行。

### 5.2 其他功能
1. 提供离线地图下载模块,可以选择不同的地图内核比如百度地图或者谷歌地图,不同的地图类型比如下载街道图还是卫星图,不同的地图层级,多线程极速下载。
2. 表格行实时显示对应的瓦片下载进度,有下载超时时间,重试次数,每个瓦片下载完成都发送信号通知,参数包括下载用时。
3. 提供省市轮廓图下载模块,自动下载各个地区的轮廓图,保存到脚本文件或者文本文件。
4. 支持手动调整不同区域的轮廓边界,调整后可以主动获取调整后的边界点集合。
5. 提供动态点位示例,手动在地图上选点并添加标注,附带自定义的信息比如速度和时间等。
6. 提供海量点位示例,批量添加标注点、点聚合、海量点。用于测试环境中支持的最大点位性能。
7. 提供动态轨迹示例,在地图上鼠标按下选择起点和终点后,查询路线,获取路径轨迹点,模拟轨迹平滑移动。可以筛选数据将过多的路径点筛选到设定的点数。
8. 提供轨迹回放示例,按照指定的轨迹点列表回放,也可以导入轨迹点数据进行回放。同时支持在街道图、卫星图、混合图中回放轨迹。
9. 提供省市区域地图示例,采用echart组件,同时支持闪烁点图、迁徙图、区域地图、世界地图、仪表盘等。可以设置标题、提示信息、背景颜色、文字颜色、线条颜色、区域颜色等各种颜色。
10. 省市区域地图示例,内置世界地图、全国地图、省份地图、地区地图,可以精确到县,所有地图全部离线使用。可设置城市的名称、值、经纬度集合。
11. 内置通用浏览器组件,同时支持webkit/webengine/miniblink等内核。提供网页控件示例,演示打开网页和本地网页文件。
12. 支持任意Qt版本、任意系统、任意编译器。
欢迎关注微信公众号:Qt实战/Qt入门和进阶(各种开源作品、经验整理、项目实战技巧,专注Qt/C++软件开发,视频监控、物联网、工业控制、嵌入式软件、国产化系统应用软件开发) QQ:517216493  WX:feiyangqingyun  QQ群:751439350
快速回复
限100 字节
 
上一个 下一个