最近公司要做一个关于无人机飞行测量某种物质的项目,但是作者只在大学的时候接触过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帮助解决了很多麻烦,但是里边的知识点需要自己慢慢消化学习。

 

Logo

欢迎加入DeepSeek 技术社区。在这里,你可以找到志同道合的朋友,共同探索AI技术的奥秘。

更多推荐