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

一、前言说明

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
  2. 国际站点:https://github.com/feiyangqingyun
  3. 个人作品:https://blog.csdn.net/feiyangqingyun/article/details/97565652
  4. 文件地址: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版本、任意系统、任意编译器。
相关推荐
用户805533698034 天前
不止三件套:QObject 属性系统全关键字与运行时反射!
c++·qt
xcyxiner4 天前
DicomViewer (vcpkg Windows和ubuntu编译)7
qt
Quz9 天前
QML Hello World 入门示例
qt
xcyxiner12 天前
DicomViewer (dcmtk读取dcm文件)5
qt
xcyxiner13 天前
DicomViewer (后台线程处理文件)4
qt
xcyxiner13 天前
DicomViewer (添加模型类)3
qt
xcyxiner14 天前
DicomViewer (目录调整) 2
qt
xcyxiner14 天前
dcmtk vtk vtk-dicom(gdcm) 编译(debug) v2
qt
桥田智能16 天前
桥田智能 QT-650S:面向白车身焊装的 800kg 重载快换解决方案
开发语言·qt·系统架构
森G16 天前
75、服务器源码解析---------云视频服务项目
linux·服务器·网络·c++·qt