
C++QT调用高德地图Api展示实时无人机飞行轨迹
最近公司要做一个关于无人机飞行测量某种物质的项目,但是作者只在大学的时候接触过JS,上班之后致力于C++,已经忘了JS相关内容。不过那段时间正好deepseek横空出世,帮我解决了这个大难题,在这里记录一下学习历程。这里调用的是高德地图的Web端的接口,环境:VS2022+QT5.15.2。
最近公司要做一个关于无人机飞行测量某种物质的项目,但是作者只在大学的时候接触过JS,上班之后致力于C++,已经忘了JS相关内容。不过那段时间正好deepseek横空出世,帮我解决了这个大难题,在这里记录一下学习历程。
这里调用的是高德地图的Web端的接口,环境:VS2022+QT5.15.2
1.JS与QT如何交互
在QT中与JS进行交互,实际上就是Web界面与C++之间的通信,可以在QWebEngineView或Qt WebEngine中嵌入网页。
查了一下相关资料,有两种连接的方式:1.使用QWebChannel直接连接;2.使用QWebSocket连接;两种方式我都试过了,但是完成的太久了,不知道当时为什么不选QWebChannel直接连接,反而最后替换成了QWebSocket连接(所以一定要及时记录,我这个项目才完成一个多月,感觉脑子已经记不住什么东西了)。
这里是deepseek给的他俩的区别和联系。
特性 | QWebChannel | QWebSocket |
---|---|---|
定位 | 本地 C++ 与 Web(JS)的跨语言通信机制 | 基于 WebSocket 协议的跨网络双向通信工具 |
通信范围 | 同一应用内(如 C++ 后端与嵌入的 Web 页面) | 跨应用、跨设备(客户端-服务器、设备间通信) |
依赖协议 | 基于 Qt 对象系统(信号槽),底层可能封装 WebSocket | 直接实现 WebSocket 协议(RFC 6455) |
数据格式 | 自动序列化 Qt 对象(支持信号槽、属性、方法) | 传输原始二进制或文本数据(需手动解析) |
使用场景 | 应用内混合开发(如 Hybrid 应用、数据可视化) | 实时网络通信(如聊天、游戏、IoT 设备控制) |
安全性 | 依赖应用本地环境,无网络暴露风险 | 需处理网络安全(如 SSL/TLS 加密、身份验证) |
可能是因为现在无人机的项目需要实时通信并且是通过MQTT服务器取数后续才换成QWebSocket的吧。
deepseek给的解释:
用户的应用场景是无人机实时数据,数据需要实时传递到前端的高德地图上。这里的关键点在于数据传递的实时性和效率。如果整个应用是在本地运行,比如用Qt的QWebEngineView嵌入高德地图的网页,那么QWebChannel会更适合,因为它可以直接在C++和JS之间传递数据,无需网络开销,延迟更低。
但是,如果无人机数据是通过网络传输,比如无人机作为客户端,通过WiFi或4G发送数据到服务器,然后前端网页从服务器获取数据,这时候就需要QWebSocket了。这种情况下,QWebSocket能处理实时的网络通信,确保数据从服务器推送到前端。
2.开始交互
现在知道了需要用到QT中的哪一个类实现这个操作,下边就开始类的设计了。
我对JS的操作包括:
1.C++向JS传递:实时经纬度、浓度、颜色条范围;
2.JS向C++传递:截取地图的Base64 数据、经纬度信息和化合物切换情况。
主程序调用:
//const QString HttpUserAgent_H5 = QString("Toon-pc/1.0.0 windows,android,iphone,ipad");
void DroneFID::InitGrid()
{
//使用QWebsocketServer实现 启用websocket服务器,监听端口:12345
m_pSocket = new WebSocketServer(12345, nullptr, &m_tMng);
//网格图 载入高德地图
QWebChannel* channel = new QWebChannel(this); //用于C++和JavaScript的通信通道
//ui.tabGrid->page()->profile()->setHttpUserAgent(HttpUserAgent_H5);
ui.tabGrid->load(QUrl("qrc:/DroneFID/html/GAODE.html")); //需要将其中的js文件一并加入到资源文件中
//调用高德地图是异步操作,所以需要等到地图加载完毕才能进行消息传递
connect(ui.tabGrid->page(), &QWebEnginePage::loadFinished, [this](bool ok) {
if (ok) {
// 地图加载完成后再设置活动状态
ui.tabGrid->page()->setLifecycleState(QWebEnginePage::LifecycleState::Active);
}
});//设置不可见的二维图的状态为活跃状态
connect(m_pSocket, &WebSocketServer::SendDataSuccess, this, &DroneFID::Flushed3DPic);
connect(m_pSocket, &WebSocketServer::SendCompoundSwitch, this, &DroneFID::OnSwitchCompound);
connect(m_pSocket, &WebSocketServer::SendCaptureSuccess, this, &DroneFID::SaveToFileNormal);
connect(m_pSocket, &WebSocketServer::SendColorSwitch, this, &DroneFID::ColorRangeChange);
connect(m_pSocket, &WebSocketServer::SendConnectStatus, this, &DroneFID::FlushedGaodeStatus);
//发送一个颜色值
if (m_pSocket) //如果已经连接
{
m_pSocket->SendDataColorRange(m_dColorMinValue, m_dColorMaxValue);
}
}
PS:ui.tabGrid->page()->profile()->setHttpUserAgent(HttpUserAgent_H5);这个地方是设置HTTP用户代理,之前在这里卡住很长时间,不明白明明接口什么的都已经设置了,但是JS端就是接收不到从客户端传递的消息,找了很多资料乱试一通,后来发现需要设置一下用户代理。QWebEngineView 设置用户代理(通过 setHttpUserAgent
方法)的主要目的是控制客户端在发送 HTTP 请求时向服务器标识自己的方式,所以高德地图的服务器端可能对客户端什么进行了严格检测或限制(有懂的大佬可以帮忙解释一下,我这也是通过deepseek或其他平台猜测的想法),但是现在进行测试的时候发现就算不设置这个也可以正常通信,所以留个疑问在这,当时也没有及时记录,中间改了很多,也不知道到底是改了哪里。
客户端实现:
#pragma once
#include <QObject>
#include <QtWebSockets/QWebSocketServer>
#include <QtWebSockets/QWebSocket>
#include <QJsonArray>
#include <QJsonObject>
#include <QJsonDocument>
#include <QList>
#include "DroneFIDMng.h"
class WebSocketServer :public QObject
{
Q_OBJECT
public:
WebSocketServer(quint16 port, QObject* parent = nullptr, DroneFIDMng* pMng = nullptr);
~WebSocketServer();
public:
void SendDataHistory(const QVector<double>& lons, const QVector<double>& lats,
const QVector<QVector<double>>& concentrations);
void SendDataRealTime(const double dLon, const double dLat,
const QVector<double>& arrCon);
void SendDataLocation(const double dLon, const double dLat);
void SendDataColorRange(const double dMin, const double dMax);
void SendDataCapture();
signals:
void SendConnectStatus(const bool flag);
void SendDataSuccess();
void SendCompoundSwitch(int nLoc);
void SendCaptureSuccess();
void SendColorSwitch(double dMin, double dMax); //不同化合物有不同颜色参数
public slots :
void OnNewConnection();
void OnTextMessageReceived(const QString& message);
void OnClientDisconnected();
void SendClearSquaresCommand();
public:
QWebSocketServer* m_pSocketServer;
QList<QWebSocket*> m_arrClients;
DroneFIDMng* m_pMng;
};
#include "WebSocketServer.h"
#include <QMessageBox>
#include <qfile.h>
WebSocketServer::WebSocketServer(quint16 port, QObject* parent /*= nullptr*/, DroneFIDMng* pMng /*= nullptr*/)
:QObject(parent),
m_pSocketServer(new QWebSocketServer("DroneServer", QWebSocketServer::NonSecureMode, this)),
m_pMng(pMng)
{
connect(m_pSocketServer, &QWebSocketServer::newConnection, this, &WebSocketServer::OnNewConnection);
if (m_pSocketServer->listen(QHostAddress::Any, port))
{
emit SendConnectStatus(true);
}
}
WebSocketServer::~WebSocketServer()
{
if (m_pSocketServer)
{
m_pSocketServer->close();
delete m_pSocketServer;
m_pSocketServer = nullptr;
}
for (QWebSocket* client : m_arrClients)
{
if (client) {
client->close(QWebSocketProtocol::CloseCodeNormal, "Server shutdown");
client->deleteLater();
}
}
m_arrClients.clear();
}
void WebSocketServer::SendDataHistory(const QVector<double>& lons, const QVector<double>& lats, const QVector<QVector<double>>& concentrations)
{
//打开历史文件发送的数据
QJsonObject json;
QJsonArray jsonArray;
for (int i = 0; i < lons.size(); ++i)
{
QJsonObject point;
point["lat"] = lats[i];
point["lon"] = lons[i];
QJsonArray concArray;
for (const auto& conc : concentrations[i])
{
concArray.append(conc);
}
point["concentrations"] = concArray;
jsonArray.append(point);
}
json["type"] = "history";
json["arrlist"] = jsonArray;
QJsonDocument doc(json);
QString jsonData = doc.toJson(QJsonDocument::Compact);
// 发送数据给所有连接的客户端
for (const auto& client : qAsConst(m_arrClients))
{
client->sendTextMessage(jsonData);
}
}
void WebSocketServer::SendDataRealTime(const double dLon, const double dLat, const QVector<double>& arrCon)
{
//实时数据
QJsonObject jsonObj;
jsonObj["type"] = "realtime";
jsonObj["lat"] = dLat;
jsonObj["lon"] = dLon;
QJsonArray concArray;
for (const auto& conc : arrCon)
{
concArray.append(conc);
}
jsonObj["concentrations"] = concArray;
QJsonDocument doc(jsonObj);
QString jsonData = doc.toJson(QJsonDocument::Compact);
// 发送数据给所有连接的客户端
for (const auto& client : qAsConst(m_arrClients))
{
client->sendTextMessage(jsonData);
}
}
void WebSocketServer::SendDataLocation(const double dLon, const double dLat)
{
QJsonObject jsonObj;
jsonObj["type"] = "location";
jsonObj["lat"] = dLat;
jsonObj["lon"] = dLon;
QJsonDocument doc(jsonObj);
QString jsonData = doc.toJson(QJsonDocument::Compact);
for (const auto& client : qAsConst(m_arrClients))
{
client->sendTextMessage(jsonData);
}
}
void WebSocketServer::SendDataColorRange(const double dMin, const double dMax)
{
QJsonObject jsonObj;
jsonObj["type"] = "colorRange";
jsonObj["min"] = dMin;
jsonObj["max"] = dMax;
QJsonDocument doc(jsonObj);
QString jsonData = doc.toJson(QJsonDocument::Compact);
for (const auto& client : qAsConst(m_arrClients))
{
client->sendTextMessage(jsonData);
}
}
void WebSocketServer::SendDataCapture()
{
QJsonObject jsonObj;
jsonObj["type"] = "capture";
QJsonDocument doc(jsonObj);
QString jsonData = doc.toJson(QJsonDocument::Compact);
for (const auto& client : qAsConst(m_arrClients))
{
client->sendTextMessage(jsonData);
}
}
void WebSocketServer::OnNewConnection()
{
QWebSocket* pClient = m_pSocketServer->nextPendingConnection();
if (!pClient)
return;
connect(pClient, &QWebSocket::textMessageReceived, this, &WebSocketServer::OnTextMessageReceived);
connect(pClient, &QWebSocket::disconnected, this, &WebSocketServer::OnClientDisconnected);
m_arrClients<<pClient; // 转移所有权到列表
emit SendConnectStatus(true);
}
void WebSocketServer::OnTextMessageReceived(const QString& message)
{
//处理从客户端传来的消息 现在处理图片消息
QWebSocket* socket = qobject_cast<QWebSocket*>(sender());
if (!socket) return;
// 解析 JSON 数据
QJsonDocument doc = QJsonDocument::fromJson(message.toUtf8());
if (doc.isNull())
{
QMessageBox::information(nullptr, QStringLiteral("提示"), QStringLiteral("Invalid JSON data"));
return;
}
QJsonObject obj = doc.object();
if (obj["type"].toString() == "mapImage")
{
// 获取截图 Base64 数据
if (m_pMng->m_pTempMsg == nullptr)
{
return;
}
QString imageData = obj["image"].toString();
if (!imageData.isEmpty())
{
// 解码 Base64 数据并保存为图片文件
QByteArray imageBytes = QByteArray::fromBase64(imageData.toUtf8());
m_pMng->m_pTempMsg->m_PicData = imageBytes;
}
//获取地图四个角的经纬度
QJsonObject corners = obj["corners"].toObject();
QJsonArray topLeft = corners["topLeft"].toArray();
double topLeftLng = topLeft[0].toDouble();
double topLeftLat = topLeft[1].toDouble();
m_pMng->m_pTempMsg->m_dTopLeftLat = topLeftLat;
m_pMng->m_pTempMsg->m_dTopLeftLng = topLeftLng;
QJsonArray bottomRight = corners["bottomRight"].toArray();
double bottomRightLng = bottomRight[0].toDouble();
double bottomRightLat = bottomRight[1].toDouble();
m_pMng->m_pTempMsg->m_dBottomRightLat = bottomRightLat;
m_pMng->m_pTempMsg->m_dBottomRightLng = bottomRightLng;
emit SendDataSuccess();
}
else if (obj["type"].toString() == "compoundChange")
{
//化合物切换 影响的主要是3D点云
int nLoc = obj["compound"].toInt();
double dMinColor = obj["mincolor"].toDouble();
double dMaxColor = obj["maxcolor"].toDouble();
emit SendColorSwitch(dMinColor, dMaxColor);
emit SendCompoundSwitch(nLoc);
}
else if (obj["type"].toString() == "mapAllImage")
{
// 获取截图 Base64 数据
if (m_pMng->m_pTempMsg == nullptr)
{
return;
}
QString imageData = obj["image"].toString();
if (!imageData.isEmpty())
{
// 解码 Base64 数据并保存为图片文件
QByteArray imageBytes = QByteArray::fromBase64(imageData.toUtf8());
m_pMng->m_PicData = imageBytes;
}
//获取地图四个角的经纬度
QJsonObject corners = obj["corners"].toObject();
QJsonArray topLeft = corners["topLeft"].toArray();
double topLeftLng = topLeft[0].toDouble();
double topLeftLat = topLeft[1].toDouble();
m_pMng->m_dTopLeftLat = topLeftLat;
m_pMng->m_dTopLeftLng = topLeftLng;
QJsonArray bottomRight = corners["bottomRight"].toArray();
double bottomRightLng = bottomRight[0].toDouble();
double bottomRightLat = bottomRight[1].toDouble();
m_pMng->m_dBottomRightLat = bottomRightLat;
m_pMng->m_dBottomRightLng = bottomRightLng;
emit SendCaptureSuccess();
}
}
void WebSocketServer::OnClientDisconnected()
{
QWebSocket* pClient = qobject_cast<QWebSocket*>(sender());
if (pClient)
{
m_arrClients.removeAll(pClient);
pClient->deleteLater();
}
}
void WebSocketServer::SendClearSquaresCommand()
{
QJsonObject jsonObj;
jsonObj["type"] = "clearSquares";
QJsonDocument doc(jsonObj);
QString jsonData = doc.toJson(QJsonDocument::Compact);
for (const auto& client : qAsConst(m_arrClients))
{
client->sendTextMessage(jsonData);
}
}
因为使用QWebSocket进行通讯,所以传递的信息使用Json格式。这个代码不算太难,大家可以大概看看是如何实现的,我只是在这里记录一下,就不解释了。
网页端实现:
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="initial-scale=1.0, user-scalable=no, width=device-width">
<title>高德地图</title>
<style>
html,
body,
#mapBase {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 1;
}
#container {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
z-index: 2;
}
#compound-selector {
position: absolute;
top: 10px;
right: 10px;
z-index: 1000;
padding: 5px;
background: white;
border: 1px solid #ccc;
border-radius: 4px;
}
</style>
<script src="https://webapi.amap.com/maps?v=2.0&自己申请的key"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js"></script>
<script type="text/javascript" src="qwebchannel.js"></script>
</head>
<body>
<div id="mapBase" style="width:100%; height:100%;"></div>
<div id="container" style="width:100%; height:100%;"></div>
<div id="compound-selector">
<select id="compound">
<option value="CH4">甲烷</option>
<option value="OTHER">非甲烷</option>
</select>
</div>
<script type="text/javascript">
var mapBase;
var map;
var squares = []; //用于存储方框和浓度值
var currentCompound = 0; //当前选中的化合物
var droneMarker; //无人机位置标记
var ws;
var dMinColor = 0.0; //颜色条范围 甲烷非甲烷
var dMaxColor = 50.0;
var JWColor = {
dMinColor: 0.0,
dMaxColor: 50.0
}
var FJWColor = {
dMinColor: 0.0,
dMaxColor: 50.0
}
//需要有判断当前运行过程中无人机飞过最大面积的经纬度值(用于截图)
var minLoc = {
longitude: 180,
latitude: 90
};
var maxLoc = {
longitude: -180,
latitude: -90
};
var updatePromise = Promise.resolve();
const GRID_SIZE = 100; // 空间索引网格大小(米)
const spatialGrid = new Map(); // 空间索引 {gridKey: [squares]}
// 修改后的方框数据结构
class SquareRecord {
constructor(bounds, rectangle, text, concentrations) {
this.bounds = bounds; // 方框边界
this.rectangle = rectangle; // 高德地图矩形对象
this.text = text; // 浓度文本
this.concentrations = concentrations; // 浓度值
this.lastAccessed = Date.now(); // 最后访问时间
}
}
function initServer() {
ws = new WebSocket("ws://localhost:12345"); //连接到 WebSocket 服务器
ws.onopen = function () {
console.log("WebSocket connection established");
};
ws.onclose = function () {
console.log("WebSocket connection closed");
//尝试重新连接
setTimeout(initServer, 5000); //5秒后重试
};
ws.onerror = function (error) {
console.log("WebSocket error:", error);
};
ws.onmessage = function (event) {
var data = JSON.parse(event.data); //解析收到的数据
if (data.type === "realtime") {
//实时数据:更新无人机位置并添加方块
if (data.lat && data.lon && data.concentrations) {
updatePromise = updatePromise.then(function () {
return new Promise(function (resolve) {
updateDronePosition(data.lon, data.lat, false);
addSquare(data.lon, data.lat, data.concentrations, 0);
setTimeout(() => {
captureAndSendData(data.lon, data.lat);
resolve(); // 确保 Promise 在截图后 resolve
}, 500);
});
});
} else {
console.error("Invalid realtime data:", data);
}
}
else if (data.type == "history") {
//历史数据:遍历并添加所有方块
var num = 0;
if (Array.isArray(data.arrlist)) {
data.arrlist.forEach(function (point) {
if (point.lat && point.lon && point.concentrations) {
if (num == 0) {
map.setCenter([point.lon, point.lat]);
}
addSquare(point.lon, point.lat, point.concentrations, 1);
num++;
} else {
alert("error");
}
});
} else {
console.error("Invalid history data:", data);
}
}
else if (data.type == "location") {
//实时位置
if (data.lat && data.lon) {
updateDronePosition(data.lon, data.lat,true);
} else {
console.error("Invalid location data:", data);
}
}
else if (data.type == "clearSquares") {
clearSquares();
}
else if (data.type == "colorRange") {
setColorRange(data.min, data.max);
updateAllColor();
}
else if (data.type == "capture") {
updatePromise.then(function () {
//alert("截取全屏");
captureAllScreen();
});
}
else {
console.log("Unknown Message");
}
}
ws.onclose = function () {
console.log("WebSocket connection closed");
};
ws.onerror = function (error) {
console.log("WebSocket error:", error);
};
}
//清空地图界面上所有的方块和浓度值
function clearSquares() {
if (!map) {
return;
}
//遍历方块列表,移除所有方块
squares.forEach(function (square) {
if (square && square.rectangle) {
map.remove(square.rectangle); // 移除矩形
square.rectangle = null; // 设置为 null
}
if (square && square.text) {
map.remove(square.text); // 移除浓度值文本
square.text = null; // 设置为 null
}
});
//清空方块列表
squares = [];
spatialGrid.clear();
}
//截取地图图片并发送数据
function captureAndSendData(lng, lat) {
//map.setZoom(map.getZoom());
const container = document.getElementById('mapBase');
const size = 400; //600px*600px 没弄清楚这个是怎么转换的,好像与地图的缩放比例有关
const centerPixel = map.lngLatToContainer([lng, lat]);
const offset = size / 2;
const x = centerPixel.x - offset; //左上角位置
const y = centerPixel.y - offset;
html2canvas(container, {
backgroundColor: null,
border: null,
useCORS: true,//如果截图的内容里有图片,可能会有跨域的情况,加上这个参数,解决文件跨域问题
x: x,
y: y,
width: size,
height: size,
}).then(function (canvas) {
var imageData = canvas.toDataURL('image/png'); // 转换为 Base
//传递的信息:四个角的经纬度
const topLeft = map.containerToLngLat([x, y]);
const bottomRight = map.containerToLngLat([x + size, y + size]);
// 发送数据给主程序
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({
type: "mapImage",
image: imageData.split(',')[1], // 去掉 data:image/png;base64, 前缀
position: {
longitude: lng,
latitude: lat,
height: 0
},
corners: {
topLeft: [topLeft.lng, topLeft.lat],
bottomRight: [bottomRight.lng, bottomRight.lat],
}
}));
} else {
console.error("WebSocket is not open");
}
});
}
//截取当前轨迹所在范围的最大图片
function captureAllScreen() {
const container = document.getElementById('container');
const topLeftPixel = map.lngLatToContainer([minLoc.longitude, maxLoc.latitude]); // 左上角
const bottomRightPixel = map.lngLatToContainer([maxLoc.longitude, minLoc.latitude]); // 右下角
//计算截图的宽度和高度
const width = bottomRightPixel.x - topLeftPixel.x;
const height = bottomRightPixel.y - topLeftPixel.y;
//确定正方形的边长
const expandPercentage = 1;
const squareSize = Math.max(width, height) * (1 + expandPercentage);
//调整截图区域,使其成为正方形
const centerX = (topLeftPixel.x + bottomRightPixel.x) / 2;
const centerY = (topLeftPixel.y + bottomRightPixel.y) / 2;
const squareTopLeftX = centerX - squareSize / 2;
const squareTopLeftY = centerY - squareSize / 2;
// 隐藏不需要的元素
//if (droneMarker) droneMarker.hide(); // 隐藏无人机标记
//squares.forEach(square => {
// square.rectangle.hide(); // 隐藏方框
// square.text.hide(); // 隐藏浓度值文本
//});
//使用 MutationObserver 监听 DOM 变化,确保在 DOM 更新完成后再执行截图。
const observer = new MutationObserver(() => {
// DOM 更新完成后,断开监听并执行截图
observer.disconnect();
//使用 html2canvas 截图 异步执行
html2canvas(container, {
backgroundColor: null,
border: null,
useCORS: true, // 解决跨域问题
x: squareTopLeftX,
y: squareTopLeftY,
width: squareSize,
height: squareSize,
}).then(function (canvas) {
// 将截图转换为 Base64
var imageData = canvas.toDataURL('image/png');
// 计算左上角、右下角的经纬度
const topLeft = map.containerToLngLat([squareTopLeftX, squareTopLeftY]);
const bottomRight = map.containerToLngLat([squareTopLeftX + squareSize, squareTopLeftY + squareSize]);
// 发送数据给主程序
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({
type: "mapAllImage",
image: imageData.split(',')[1], // 去掉 data:image/png;base64, 前缀
corners: {
topLeft: [topLeft.lng, topLeft.lat],
bottomRight: [bottomRight.lng, bottomRight.lat],
}
}));
} else {
console.error("WebSocket is not open");
}
//截图完成后显示隐藏的元素
//if (droneMarker) droneMarker.show(); //显示无人机标记
//squares.forEach(square => {
// square.rectangle.show(); //显示方框
// square.text.show(); //显示浓度值文本
//});
//最大最小经纬度退回
minLoc = {
longitude: 180,
latitude: 90
};
maxLoc = {
longitude: -180,
latitude: -90
};
});
});
observer.observe(container, { attributes: true, childList: true, subtree: true });
}
//更新方块的浓度值和颜色值
function updateConcentrations(newConcentrations) {
//找到所有未测量的方块
var unmeasuredSquares = squares.filter(square => square.concentrations[currentCompound] === -1);
//将累积浓度值分配到未测量的方块
if (unmeasuredSquares.length > 0) {
unmeasuredSquares.forEach(square => {
//更新浓度和颜色信息
square.concentrations = newConcentrations;
updateSquareDisplay(square); //更新显示
});
}
}
//更新方块的显示
function updateSquareDisplay(square) {
var concentration = square.concentrations[currentCompound]; //获取当前化合物的浓度
square.text.setText(concentration.toFixed(2)); //更新文本
square.rectangle.setOptions({
fillColor: getColorFromGradient(concentration),
strokeColor: getColorFromGradient(concentration),
});
}
function updateSquare(square) {
var concentration = square.concentrations[currentCompound]; //获取当前化合物的浓度
square.text.setText(concentration.toFixed(2)); //更新文本
square.rectangle.setOptions({
fillColor: getColorFromGradient(concentration),
strokeColor: getColorFromGradient(concentration),
});
}
// 坐标转网格键
// (116.404, 39.915) → "1298,443"
function lngLatToGridKey(lng, lat) {
const metersPerDegreeLat = 111320;
const metersPerDegreeLng = 111320 * Math.cos(lat * Math.PI / 180);
// 使用全局GRID_SIZE常量(100米)
const x = Math.floor(lng * metersPerDegreeLng / GRID_SIZE);
const y = Math.floor(lat * metersPerDegreeLat / GRID_SIZE);
return `${x},${y}`;
}
// 添加方格到空间索引
function addToSpatialIndex(square) {
const bounds = square.bounds;
// 获取西南角和东北角的网格键
const swKey = lngLatToGridKey(bounds.southWest.lng, bounds.southWest.lat);
const neKey = lngLatToGridKey(bounds.northEast.lng, bounds.northEast.lat);
// 解析网格坐标
const [swX, swY] = swKey.split(',').map(Number);
const [neX, neY] = neKey.split(',').map(Number);
// 遍历覆盖的网格
for (let x = Math.min(swX, neX); x <= Math.max(swX, neX); x++) {
for (let y = Math.min(swY, neY); y <= Math.max(swY, neY); y++) {
const gridKey = `${x},${y}`;
if (!spatialGrid.has(gridKey)) {
spatialGrid.set(gridKey, new Set());
}
spatialGrid.get(gridKey).add(square);
}
}
}
function isInAnySquare(lng, lat) {
const EPSILON = 1e-9;
// 查找当前点所在网格的所有方格
const gridKey = lngLatToGridKey(lng, lat);
// 生成需要检查的相邻网格键(当前+上下左右)
const [x, y] = gridKey.split(',').map(Number);
const neighborKeys = [
// 3x3 网格范围
`${x - 1},${y - 1}`, `${x},${y - 1}`, `${x + 1},${y - 1}`,
`${x - 1},${y}`, `${x},${y}`, `${x + 1},${y}`,
`${x - 1},${y + 1}`, `${x},${y + 1}`, `${x + 1},${y + 1}`
];
// 遍历所有相关网格
return neighborKeys.some(key => {
const squaresInGrid = spatialGrid.get(key);
if (!squaresInGrid) return false;
// 将Set转换为Array后遍历
return Array.from(squaresInGrid).some(square => {
const b = square.bounds;
return (
lng >= b.southWest.lng - EPSILON &&
lng <= b.northEast.lng + EPSILON &&
lat >= b.southWest.lat - EPSILON &&
lat <= b.northEast.lat + EPSILON
);
});
});
}
// 改进方向检测和边界计算
function calculateNewBounds(nearestSquare, direction) {
const baseSW = nearestSquare.bounds.southWest;
const baseNE = nearestSquare.bounds.northEast;
const lngSize = baseNE.lng - baseSW.lng;
const latSize = baseNE.lat - baseSW.lat;
//8方向处理
switch (direction) {
// 主要方向
case 'east':
return new AMap.Bounds(
[baseNE.lng, baseSW.lat],
[baseNE.lng + lngSize, baseNE.lat]
);
case 'west':
return new AMap.Bounds(
[baseSW.lng - lngSize, baseSW.lat],
[baseSW.lng, baseNE.lat]
);
case 'north':
return new AMap.Bounds(
[baseSW.lng, baseNE.lat],
[baseNE.lng, baseNE.lat + latSize]
);
case 'south':
return new AMap.Bounds(
[baseSW.lng, baseSW.lat - latSize],
[baseNE.lng, baseSW.lat]
);
// 新增对角线方向
case 'northeast':
return new AMap.Bounds(
[baseNE.lng, baseNE.lat],
[baseNE.lng + lngSize, baseNE.lat + latSize]
);
case 'northwest':
return new AMap.Bounds(
[baseSW.lng - lngSize, baseNE.lat],
[baseSW.lng, baseNE.lat + latSize]
);
case 'southeast':
return new AMap.Bounds(
[baseNE.lng, baseSW.lat - latSize],
[baseNE.lng + lngSize, baseSW.lat]
);
case 'southwest':
return new AMap.Bounds(
[baseSW.lng - lngSize, baseSW.lat - latSize],
[baseSW.lng, baseSW.lat]
);
}
}
// 查找最近的方框
function findNearestSquare(lng, lat) {
let minDistance = Infinity;
let nearestSquare = null;
let direction = null;
// 查找当前点所在网格的所有方格
const gridKey = lngLatToGridKey(lng, lat);
// 生成需要检查的相邻网格键(当前+上下左右)
const [x, y] = gridKey.split(',').map(Number);
const neighborKeys = [
// 3x3 网格范围
`${x - 1},${y - 1}`, `${x},${y - 1}`, `${x + 1},${y - 1}`,
`${x - 1},${y}`, `${x},${y}`, `${x + 1},${y}`,
`${x - 1},${y + 1}`, `${x},${y + 1}`, `${x + 1},${y + 1}`
];
// 遍历所有相关网格
neighborKeys.forEach(key => {
const squaresInGrid = spatialGrid.get(key);
if (!squaresInGrid) return;
// 将Set转换为Array后遍历
Array.from(squaresInGrid).forEach(square => {
const bounds = square.bounds;
const sw = bounds.southWest; // 方块西南角坐标
const ne = bounds.northEast; // 方块东北角坐标
// 5. 判断当前点与方块的相对位置
const isEast = lng > ne.lng; // 是否在方块东侧
const isWest = lng < sw.lng; // 是否在方块西侧
const isNorth = lat > ne.lat; // 是否在方块北侧
const isSouth = lat < sw.lat; // 是否在方块南侧
// 6. 计算到各边界的距离
const distEast = isEast ? lng - ne.lng : 0; // 东侧距离
const distWest = isWest ? sw.lng - lng : 0; // 西侧距离
const distNorth = isNorth ? lat - ne.lat : 0; // 北侧距离
const distSouth = isSouth ? sw.lat - lat : 0; // 南侧距离
// 7. 确定主要方向(优先级:正交方向 > 对角线)
let currentDir = null;
if (distEast > 0) {
if (distNorth > 0) currentDir = 'northeast';
else if (distSouth > 0) currentDir = 'southeast';
else currentDir = 'east';
} else if (distWest > 0) {
if (distNorth > 0) currentDir = 'northwest';
else if (distSouth > 0) currentDir = 'southwest';
else currentDir = 'west';
} else if (distNorth > 0) {
currentDir = 'north';
} else if (distSouth > 0) {
currentDir = 'south';
} else {
return; // 在方块内部时不处理
}
// 8. 计算曼哈顿距离(提高性能)
const distance = distEast + distWest + distNorth + distSouth;
// 9. 更新最近方块信息
if (distance < minDistance) {
minDistance = distance;
nearestSquare = square;
direction = currentDir;
}
});
});
return { square: nearestSquare, direction };
}
//添加方框
function addSquare(lng, lat, concentrations, RealTime) {
//先判断当前是否有浓度值,有的话需要更新当前方块已经前面没有浓度值的方块
var concentration = concentrations[currentCompound];
if (concentration != -1) {
updateConcentrations(concentrations);
}
if (isInAnySquare(lng, lat)) {
return;
}
// 寻找最近的方框用于连接
const { square: nearestSquare, direction } = findNearestSquare(lng, lat);
let newBounds;
if (nearestSquare) {
newBounds = calculateNewBounds(nearestSquare, direction);
} else {
// 首个方框
const latOffset = 20 / 111320;
const lngOffset = 20 / (111320 * Math.cos((lat * Math.PI) / 180));
newBounds = new AMap.Bounds(
[lng - lngOffset, lat - latOffset],
[lng + lngOffset, lat + latOffset]
);
}
//设置正方形
var rectangle = new AMap.Rectangle({
bounds: newBounds,
strokeColor: getColorFromGradient(concentration),
strokeWeight: 2,
strokeOpacity: 0.5,
fillColor: getColorFromGradient(concentration),
fillOpacity: 0.5,
zIndex: 50,
});
map.add(rectangle);
//添加浓度值文本
const center = newBounds.getCenter();
var text = new AMap.Text({
text: concentration.toFixed(2), // 显示当前化合物的浓度
position: center,
style: {
fontSize: 10,
fillColor: 'black',
strokeColor: 'white',
strokeWidth: 2,
},
zIndex: 100,
});
map.add(text);
addToSpatialIndex(new SquareRecord(newBounds, rectangle, text, concentrations));
// 保存到squares数组
squares.push(new SquareRecord(newBounds, rectangle, text, concentrations));
map.setCenter([lng, lat]);
}
//更新无人机位置
function updateDronePosition(lng, lat, bLoc) {
// 更新最小和最大经纬度
if (lng < minLoc.longitude) {
minLoc.longitude = lng;
}
if (lat < minLoc.latitude) {
minLoc.latitude = lat;
}
if (lng > maxLoc.longitude) {
maxLoc.longitude = lng;
}
if (lat > maxLoc.latitude) {
maxLoc.latitude = lat;
}
if (bLoc) {
map.setCenter([lng, lat]);
}
if (!droneMarker) {
//如果无人机标记不存在,创建一个新的标记
droneMarker = new AMap.Marker({
position: [lng, lat],
map: map,
icon: 'https://webapi.amap.com/theme/v1.3/markers/n/mark_b.png', // 使用高德地图的默认图标
zIndex: 1000 //确保无人机标记在最上层
});
} else {
//如果无人机标记已经存在,更新其位置
droneMarker.setPosition([lng, lat]);
}
}
//设置颜色条范围
function setColorRange(dMin, dMax) {
if (currentCompound == 0) {
JWColor.dMinColor = dMin;
JWColor.dMaxColor = dMax;
} else if (currentCompound == 1) {
FJWColor.dMinColor = dMin;
FJWColor.dMaxColor = dMax;
}
}
//更新所有的颜色(颜色条发生变化)
function updateAllColor() {
for (var i = 0; i < squares.length; i++) {
const square = squares[i];
const concentration = square.concentrations[currentCompound]; // 获取当前化合物的浓度
const color = getColorFromGradient(concentration); // 根据浓度计算颜色
// 更新方块的填充颜色和边框颜色
square.rectangle.setOptions({
fillColor: color,
strokeColor: color,
});
}
}
//读取颜色值
const gradientStops = [
{ position: 0.0, color: { r: 0, g: 255, b: 0 } }, // Green
{ position: 0.5, color: { r: 255, g: 255, b: 0 } }, // Yellow
{ position: 1.0, color: { r: 255, g: 0, b: 0 } } // Red
];
function interpolateColor(color1, color2, t) {
return {
r: Math.round(color1.r + t * (color2.r - color1.r)),
g: Math.round(color1.g + t * (color2.g - color1.g)),
b: Math.round(color1.b + t * (color2.b - color1.b))
};
}
//根据值计算颜色
function getColorFromGradient(value, alpha = 1) {
var dMin, dMax;
if (currentCompound == 0) {
dMin = JWColor.dMinColor;
dMax = JWColor.dMaxColor;
} else if (currentCompound == 1) {
dMin = FJWColor.dMinColor;
dMax = FJWColor.dMaxColor;
}
const position = (value - dMin) / (dMax - dMin);
const normalizedValue = Math.max(0, Math.min(1, position)); //限制值在 0 到 1 之间
for (let i = 1; i < gradientStops.length; i++) {
if (normalizedValue <= gradientStops[i].position) {
const t = (normalizedValue - gradientStops[i - 1].position) / (gradientStops[i].position - gradientStops[i - 1].position);
const color1 = gradientStops[i - 1].color;
const color2 = gradientStops[i].color;
const interpolatedColor = interpolateColor(color1, color2, t);
return `rgba(${interpolatedColor.r}, ${interpolatedColor.g}, ${interpolatedColor.b}, ${alpha})`;
}
}
//如果值超出范围,返回最后一个颜色
const lastColor = gradientStops[gradientStops.length - 1].color;
return `rgba(${lastColor.r}, ${lastColor.g}, ${lastColor.b}, ${alpha})`;
}
//初始化 QWebChannel 和高德地图
document.addEventListener("DOMContentLoaded", function () {
//覆盖物地图
map = new AMap.Map('container', {
layers: [new AMap.TileLayer.Satellite()],
//viewMode: '2D',
center: [117.147547, 35.059065],
zoom: 14,
});
//底图
map.on('complete', () => {
mapBase = new AMap.Map('mapBase', {
layers: [new AMap.TileLayer.Satellite()],
center: map.getCenter(), // 同步初始中心
zoom: map.getZoom(), // 同步初始缩放
WebGLParams: { preserveDrawingBuffer: true },
interactive: false, // 禁用交互
});
// 3. 强制同步一次(避免初始偏差)
mapBase.setCenter(map.getCenter());
mapBase.setZoom(map.getZoom());
// 4. 监听 `map` 的变化,实时同步到 `mapBase`
map.on('moveend', () => {
mapBase.setCenter(map.getCenter());
mapBase.setZoom(map.getZoom());
});
});
//初始化 WebSocket
initServer();
//监听化合物切换
document.getElementById('compound').addEventListener('change', function () {
var compName = this.value;
var dMin, dMax;
if (compName == "CH4") {
currentCompound = 0;
dMin = JWColor.dMinColor;
dMax = JWColor.dMaxColor;
}
else if (compName == "OTHER") {
currentCompound = 1;
dMin = FJWColor.dMinColor;
dMax = FJWColor.dMaxColor;
}
for (var i = 0; i < squares.length; i++) {
updateSquare(squares[i]);
}
//通知服务器化合物切换
ws.send(JSON.stringify({ type: "compoundChange", compound: currentCompound, mincolor: dMin, maxcolor: dMax }));
});
});
</script>
</body>
</html>
网页端这里是设置了两个地图,因为我需要有截图的操作,但是我又需要在地图上实现画方格、记录浓度值以及无人机的实时位置的操作,所以设置两个地图,一个专门用来截图,另一个用来实现其他操作,需要把这两个地图的操作对应一下。但是我看可以设置图层,但是我没搞明白,所以就用这种简单的方式实现。
这里边有个截图需要注意一下,我是用的是html2canvas脚本,因为与高德地图的客服沟通过,他们没有可以用的截图操作(有可能是因为我没开会员)。
这里讲解一下无人机的运行过程,防止我以后再来看看不明白:
1.这里无人机飞行轨迹为什么设置的那么麻烦,是因为想要实现绘制的方格要么是水平方向的要么是对角线方向,这样看着很整齐。
2.接着就是无人机第一次飞行的时候是以当前无人机的位置为中心点,判断无人机飞行的轨迹,超出该方格的区域就先判断是水平还是对角方向,然后继续绘制方格。
3.在这中间又设置了空间索引,设置空间索引的目的是如果无人机飞行半小时及以上,方格的数量过于庞大,这时候如果一个个遍历查找无人机是否经过该方格(如果在之前的方格范围内就不绘制方格,无人机有可能绕圈飞行)会浪费很多时间,导致不必要的操作,基于这个目的设置空间索引,这样只需要根据无人机当前的位置查到以他所在空间为中心的9块空间内的方格是否有重复的就可以了(因为还需要判断如果不在之前的方格内,要找到与之最接近的方格进行继续绘制方格操作),这就是设置空间索引的目的。
4.判断距离那个方格最近使用了曼哈顿距离,有兴趣的可以去看一下,也不是很复杂。
目前实现的话,是无人机可以正常运行,但是鼠标对地图放大缩小或拖动的时候不是很灵敏,有卡顿现象,如果有朋友知道改进方法的话可以与我交流一下,不胜感激!!
至此有关客户端(C++)与界面的交互到这里就告一段落了,其实还有很多东西都不是很理解,只能说deepseek帮助解决了很多麻烦,但是里边的知识点需要自己慢慢消化学习。
更多推荐
所有评论(0)