拓冰建站拓冰建站
首页 / 资讯中心 / 正文

Qt雷达图叠加卫星图:极坐标与Web Mercator坐标转换实战

简介面向Qt 5.9开发者的雷达图卫星图与余晖扫描功能实现源码包涵盖数据准备、坐标系设定、QPainter/QCustomPlot绘图、卫星增删、连线设置及定时器模拟扫描等关键环节适合需要复现多轴雷达可视化或学习Qt图形渲染的C开发者。压缩包大小2.03MB共53个文件其中包含11个cpp、9个h源文件3个pro工程文件及2个ui界面文件便于直接打开编译另附2个exe可执行文件用于快速预览效果以及docx说明文档、readme和Makefile等辅助材料。资源目录按debug/release、构建配置和源码分层结构清晰上手门槛较低。包内还保留测试工程和资源文件可对比学习雷达图坐标计算、余晖扫描动画及事件交互的具体写法从而快速迁移到自己的项目中已有35人学习下载。1. Qt 雷达图与卫星图先想清楚两套坐标系的换算做 Qt 雷达图显示多数项目最后都会遇到叠加卫星图底图的需求目标散点、航迹和扫描范围不再只画在黑底圆环上而是要落到山川河流、街道楼宇的卫星影像上才能判断目标到底落在哪里。于是「Qt 雷达图 卫星图」成了很实际的组合。难的不在画图而在坐标系雷达给的是极坐标方位角、距离卫星图是 Web Mercator 投影下的瓦片像素坐标中间隔着雷达站经纬度、缩放级别和视口偏移任何一环对不上目标点就会偏出去几百米。下面按「自绘雷达图 → 瓦片底图加载 → 三级坐标变换叠加 → 缓存与排错」的顺序给出方案代码对 Qt 5.15 与 Qt 6 均适用适合正在做态势显示、雷达终端或监控标绘的开发者。如果只是要一个静态的多维指标雷达图看第 2 章的前半部分就够了。2. 雷达图绘制QPolarChart 与 QPainter 的取舍2.1 QPolarChart 能出雷达图但叠不了卫星图底图Qt Charts 模块里的 QPolarChart 是官方极坐标图网上搜「Qt 雷达图」出来最多的是它。画一个方位增益图代码很短#include QPolarChart #include QLineSeries QPolarChart *chart new QPolarChart(); QLineSeries *series new QLineSeries(); *series QPointF(0, 1.0) QPointF(45, 0.75) QPointF(90, 1.2) QPointF(135, 0.9) QPointF(180, 0.95); chart-addSeries(series); chart-setTitle(方位增益); QChartView *view new QChartView(chart); view-resize(480, 360); view-show();这里 QPointF 的 x 是角度y 是幅值QPolarChart 自动把角度轴设成 0~360 度幅值轴按 y 范围伸缩画出来就是标准的蛛网雷达图。Qt Charts 在 Qt 5.7 以后内置Qt 6 里继续维护做静态展示完全没有问题。真正卡住这个需求的是后面几步。第一卫星图要作为底图放在极坐标图背后QChart 没有提供这种 layer有人把瓦片拼成 QGraphicsPixmapItem 挂到 QChartView 的场景里但图表内部坐标轴一缩放底图就会被甩开或者拉伸变形联调成本高。第二雷达画面每秒要刷新几十次series 更新会触发整个图表的坐标轴重算目标一多就掉帧。第三像素级命中检测在 QChart 里要从场景坐标反推轴坐标绕的弯比 QPainter 直接算屏幕点大得多。所以选型结论很直接纯指标展示、不需要底图和高速刷新用 QPolarChart 省事要叠加卫星图或者做像素级交互就直接 QWidget 重绘。后面内容全部围绕自绘展开。2.2 Qt 绘图里 QPainter 手写雷达图极坐标映射与网格绘制Qt 绘图里自绘控件绕不开 QPainter。第一步先写一个极坐标到屏幕直角坐标的映射函数后续画什么都可以复用QPointF polarToCartesian(qreal distance, qreal azimuthDeg, qreal radius, const QPointF center) { // 减 90 度让 0 度指向正北方位角沿屏幕顺时针增长 double rad qDegreesToRadians(azimuthDeg - 90.0); return QPointF(center.x() distance * radius * qCos(rad), center.y() distance * radius * qSin(rad)); }distance 是 0~1 的归一化距离由调用方把目标实际距离除以当前量程得到。这样量程环、方位线、目标点全部复用同一个函数。要注意 Qt 的屏幕 y 轴向下所以方位角顺时针增加和真实雷达屏幕的方向习惯一致0 度在正上方90 度在正右方。最小雷达控件的 paintEvent 可以这样组织void RadarWidget::paintEvent(QPaintEvent *) { QPainter p(this); p.setRenderHint(QPainter::Antialiasing); QPointF center rect().center(); qreal radius qMin(width(), height()) / 2.0 - 40.0; // 量程环从 1/4 半径一直画到整半径 p.setPen(QPen(QColor(0, 255, 0, 160), 1)); for (int i 1; i m_ringCount; i) { qreal r radius * i / m_ringCount; p.drawEllipse(center, r, r); } // 方位线每 30 度一条从中心连到边缘 for (int deg 0; deg 360; deg 30) { p.drawLine(center, polarToCartesian(1.0, deg, radius, center)); } // 目标点归一化距离落到网格内 p.setBrush(Qt::red); for (const Target t : m_targets) { QPointF pt polarToCartesian(t.range / m_maxRange, t.azimuth, radius, center); p.drawEllipse(pt, 4, 4); } }这里的绘制顺序是环、线、点点永远在最上层。量程环数和方位线步进都做成成员变量UI 上留两个 spinbox 就能实时调。此时 center 用的是控件中心叠加卫星图后它会变成「雷达站经纬度在当前视口下的屏幕点」映射函数的 center 参数就是为那一步留的接口。另外QPainter 默认逻辑坐标系等于设备坐标系这里没有手动做 scale/translate真正加缩放时建议用 p.translate 和 p.scale 设置视图变换而不是在映射函数里乘系数网格和字体会更统一。2.3 雷达图控件的参数表与调参要点自绘雷达图需要调整的参数没几个但每一个对最终显示影响都很大。参数含义常用范围调错的典型表现m_maxRange雷达最大量程与探测距离一致量程过大近处目标挤在圆心m_ringCount量程环个数48环太多压住底图radius显示半径min(w,h)/2 - 40过大被窗口裁切方位线步进相邻方位线夹角10° 或 30°太密视觉杂乱目标绘制半径目标点外接圆半径310 px太小像噪点叠加卫星图之后量程环颜色建议改成半透明亮绿色RGBA 写成 (0, 255, 0, 140)不要用纯红纯白否则底图上道路和建筑细节全被盖住。目标标签如果要画在 drawText 之前先把目标点附近一小块矩形填成半透明白再画文字对比度才够。3. 卫星图加载瓦片规范与坐标配准3.1 Web Mercator 瓦片的 z/x/y 规则与最小加载实现卫星图的通用分发单位是 256x256 的瓦片。第 0 级是一张全球图第 1 级切成 2x2第 z 级一共有 2^z 行、2^z 列每张瓦片的位置由三个整数 z/x/y 唯一确定。大多数瓦片服务地址是这样https://example.com/tiles/{z}/{x}/{y}.jpg。Qt 里加载一张瓦片用 QNetworkAccessManager 异步下载。一个只负责取图、不负责拼接的最小类长这样class TileLoader : public QObject { Q_OBJECT public: using Callback std::functionvoid(int z, int x, int y, const QByteArray data); void load(int z, int x, int y, Callback cb) { QNetworkRequest req(QUrl(QString(baseUrl).arg(z).arg(x).arg(y))); QNetworkReply *reply m_manager.get(req); Meta meta {z, x, y, cb}; m_pending.insert(reply, meta); connect(reply, QNetworkReply::finished, this, [this, reply]() { Meta meta m_pending.take(reply); if (meta.cb) { meta.cb(meta.z, meta.x, meta.y, reply-readAll()); } reply-deleteLater(); }); } private: struct Meta { int z, x, y; Callback cb; }; QNetworkAccessManager m_manager; QHashQNetworkReply*, Meta m_pending; };这里有两个工程点。一是把 z/x/y 连同回调一起放进 QHash异步返回时通过 reply 索引找回原坐标就不用担心回调顺序错乱二是必须调用 reply-deleteLater()QNetworkReply 在 finished 后不会自动释放漏掉会持续涨内存程序跑久了越来越卡甚至崩溃。3.2 经纬度与瓦片坐标互转以及缩放级别对应的分辨率经纬度转瓦片坐标是 Web Mercator 的标准反算。经度方向是线性映射纬度方向带投影变换double lonToTileX(double lon, int z) { return (lon 180.0) / 360.0 * (1 z); } double latToTileY(double lat, int z) { double latRad qDegreesToRadians(lat); double n (1.0 - qAsinh(qTan(latRad)) / M_PI) / 2.0; return n * (1 z); }lonToTileX 把 [-180, 180] 线性映射到 [0, 2^z]latToTileY 用 qAsinh(qTan(lat)) 代替了老式写法里的 log(tan 1/cos)结果一样但数值稳定性更好。注意 MSVC 下 M_PI 需要先定义 _USE_MATH_DEFINES或者直接写 qDegreesToRadians(180.0)。有了瓦片坐标还需要把它变成视口像素坐标。下面这个 geoToPixel 是后续所有叠加层的基础它把某个经纬度换算成以视图中心为原点的屏幕坐标QPointF geoToPixel(const QPointF geo, int z, const QSize viewport, const QPointF centerGeo) { double px lonToTileX(geo.x(), z) * 256.0; double py latToTileY(geo.y(), z) * 256.0; double cx lonToTileX(centerGeo.x(), z) * 256.0; double cy latToTileY(centerGeo.y(), z) * 256.0; return QPointF(px - cx viewport.width() / 2.0, py - cy viewport.height() / 2.0); }需要注意lonToTileX 返回的是「瓦片坐标浮点数」乘以 256 之后才是整张世界地图的像素坐标视图中心对应的像素和当前点的像素相减再加上视口一半得到的就是相对屏幕的偏移。这个函数在拖拽时会被反复调用建议只传 int z 和 QPointF不要每次都去查对象成员。反算时用 pixelToGeo逻辑正好相反QPointF pixelToGeo(const QPointF pt, int z, const QSize viewport, const QPointF centerGeo) { double cx lonToTileX(centerGeo.x(), z) * 256.0; double cy latToTileY(centerGeo.y(), z) * 256.0; double tx (pt.x() - viewport.width() / 2.0 cx) / 256.0; double ty (pt.y() - viewport.height() / 2.0 cy) / 256.0; double lon tx / (1 z) * 360.0 - 180.0; double n M_PI * (1.0 - 2.0 * ty / (double)(1 z)); double lat qRadiansToDegrees(qAtan(qSinh(n))); return QPointF(lon, lat); }lon 直接线性反解lat 用 sinh 和 atan 做墨卡托反变换。这两个正反函数建议放在同一个头文件里配合使用后面鼠标拾取、坐标调试都要用。不同缩放级别下赤道附近的地面分辨率大致如下缩放级别 z地面分辨率(米/像素)单张瓦片覆盖范围10152.8739.1 km × 39.1 km1238.229.8 km × 9.8 km149.552.4 km × 2.4 km162.39611 m × 611 m高纬度地区经线收敛真实覆盖范围比表中数值小。给雷达选初始缩放级别时50 公里量程通常 z10~12 就能看全要看 2 公里内的城区细节才需要 z15 以上。3.3 加载后先用一个地标自检再接线底图不配准就直接接雷达数据是联调里最常见的浪费时间方式。建议先不画任何雷达信息在鼠标移动事件里实时显示鼠标经纬度然后把鼠标移到你已知坐标的地标上看显示值和真实值差多少。常见偏差两种。一种是瓦片拼接时视口偏移没算对目标整体往一个方向偏一整屏另一种是经度方向差一个固定百分比多半是某个中间量没有过 lonToTileX 就直接当像素用了。自检通过的标志是鼠标位置和地标互差不超过一个像素。这一步做完后面第 4 章的三级坐标变换才有一个可信的地基。注意瓦片服务通常有并发上限批量请求时建议控制并发数为 4~6避免被服务端限流。4. 雷达数据叠加卫星图三级坐标变换与重绘顺序4.1 从极坐标到屏幕像素的三级坐标变换雷达目标数据通常是「方位角 斜距」。要把一个目标放到卫星图上需要经过三级变换极坐标转东北偏移东北偏移转经纬度差经纬度转屏幕像素。代码如下QPointF targetToScreen(const QPointF radarGeo, qreal azimuth, qreal rangeMeters, int zoom, const QSize viewport, const QPointF viewCenterGeo) { // 第一级极坐标转以雷达站为原点的东、北分量 double azRad qDegreesToRadians(azimuth); double east rangeMeters * qSin(azRad); double north rangeMeters * qCos(azRad); // 第二级东北偏移转经纬度差 double dLat north / 111320.0; double dLon east / (111320.0 * qCos(qDegreesToRadians(radarGeo.y()))); QPointF targetGeo(radarGeo.x() dLon, radarGeo.y() dLat); // 第三级目标经纬度转当前视口屏幕位置 return geoToPixel(targetGeo, zoom, viewport, viewCenterGeo); }三个层级的单位分别是米、度、像素。第二级里 111320 是一纬度对应的米数经度方向要除以 cos(lat) 做纬度的长度修正。这套球面近似在几十公里量程内误差很小如果探测半径超过 300 公里建议把第二级和第三级整体替换成 proj 库的精确投影换算工程上把这三段拆成独立类后面替换就不动 UI。三级变换的接口关系可以这样理解变换层级输入输出关键函数或常量极坐标→东北偏移azimuth, rangeMeterseast, northsin/cos东北偏移→经纬度差east, northdLon, dLat111320, cos(lat)经纬度→屏幕像素targetGeoscreenPosgeoToPixel提示当雷达站所在地方用 GCJ-02 加密影像时radarGeo 也要先转成同一套坐标再参与距离换算否则会有几百米的系统性偏移。4.2 图层绘制顺序底图 → 量程环 → 目标叠加显示的两个高频错误是绘制顺序反了、雷达中心取错了。绘制顺序决定遮挡关系paintEvent 里按下面三段组织void MapRadarView::paintEvent(QPaintEvent *) { QPainter p(this); p.setRenderHint(QPainter::SmoothPixmapTransform); drawTiles(p); // 1. 卫星图瓦片 drawRadarGrid(p); // 2. 量程环与方位线 drawTargets(p); // 3. 目标图标和标签 }drawRadarGrid 里最容易错的是中心点它不是控件中心而是雷达站经纬度在当前视口下的屏幕位置。每次重绘都要重新算一次void MapRadarView::drawRadarGrid(QPainter p) { QPointF center geoToPixel(m_radarGeo, m_zoom, size(), m_viewCenterGeo); qreal rMax geoDistanceToPixels(m_radarRange, m_radarGeo); p.setPen(QPen(QColor(0, 255, 0, 140), 1)); for (int i 1; i m_ringCount; i) { qreal r rMax * i / m_ringCount; p.drawEllipse(center, r, r); } // 方位线、目标点绘制略画法同第 2 章只是 center 换成上面这个 }geoDistanceToPixels 用来解决「米和像素之间没有固定比例」的问题。它的实现思路是把雷达站的纬度加 0.001 度用 geoToPixel 算出另一个屏幕点两个屏幕点的距离再乘以 1000就是每度对应的像素。这个办法不用做任何投影换算任何缩放级别下都跟着真实投影走。目标点绘制在量程环之上。标签要加半透明白底否则深色卫星图上白字看不清楚。整个雷达图层拆成三个小函数改颜色、线宽、图标互不影响。4.3 鼠标反算用屏幕上的一点验证整套变换联调阶段最需要的是一个反算函数鼠标指到屏幕上任意位置实时显示这个点的经纬度、到雷达站的距离和方位。它能直接验证 4.1 的变换链没有闭合。void MapRadarView::mouseMoveEvent(QMouseEvent *e) { QPointF geo pixelToGeo(e-pos(), m_zoom, size(), m_viewCenterGeo); double dLat geo.y() - m_radarGeo.y(); double dLon geo.x() - m_radarGeo.x(); double east dLon * 111320.0 * qCos(qDegreesToRadians(m_radarGeo.y())); double north dLat * 111320.0; double dist qSqrt(east * east north * north); double mathAz qRadiansToDegrees(qAtan2(north, east)); // atan2 返回的是数学坐标角0 度指向东逆时针为正 // 转成雷达方位0 度指向北顺时针为正 double displayAz fmod(90.0 - mathAz 360.0, 360.0); emit statusMessage(QString(方位 %1° 距离 %2 km) .arg(displayAz, 0, f, 1) .arg(dist / 1000.0, 0, f, 2)); }这段代码里最容易绕晕的是方位角方向。数学角 0 度指向正东逆时针增大雷达方位角 0 度指向正北顺时针增大。正东方向经反算后应显示 90 度正北显示 0 度如果鼠标往右拖显示 0 度就是最后这步转反了。验证方式是鼠标水平向右移动方位应从 90 度向 180 度走垂直向上为 0 度。这里用到的 pixelToGeo 就是 3.2 节里的反函数正反两个函数建议放在同一个文件里维护。5. 联调收尾三个能省半天排错的实用技巧5.1 底图与雷达层拆成两张 QPixmap 分开重绘拖拽视角时每次都重新拼瓦片卡顿感会非常明显。常见做法是维护两张离屏 QPixmap一张保存当前视野的瓦片拼接结果只在拖拽停止的瞬间重建另一张保存量程环和目标鼠标移动时只重画这一张。最终绘制时先把底图 drawPixmap 到窗口再把雷达层叠上去。重绘成本从几十张瓦片合成降为两次位图拷贝帧率提升明显。5.2 瓦片缓存设上限视野外的请求跳过缓存无节制地增长程序跑一两个小时内存就开始膨胀。给内存缓存设一个上限比如 128 张超出就把最久没用的清掉磁盘缓存放在 QStandardPaths::writableLocation(QStandardPaths::CacheLocation) 下key 用瓦片 URL 的哈希值。绘制瓦片前先用瓦片矩形和当前视口求交不相交就直接跳过避免拖动时把屏外瓦片也拉进来。5.3 坐标漂移按顺序查投影 → 方位 → 偏移目标和卫星图地物对不齐时按下面顺序排查关掉雷达图层只用底图和鼠标经纬度做地标校验确认瓦片本身没偏。确认雷达站经纬度坐标和底图一致WGS84 与 GCJ-02 混用会有几百米平移。检查方位角是磁北还是真北磁偏角要在极坐标转东北偏移之前修正。前三步都查完仍有残余偏差给雷达图层加一个可调的亚像素偏移存入 QSettings 作为应急对齐手段。第 3 条最常被忽略雷达输出若是罗盘方位就是磁北而卫星图坐标基于真北二者差一个磁偏角距离越远偏得越厉害。在目标转换入口统一加一个 angleOffset 参数这个值按当地磁偏角填目标对不齐的现象会少一大半。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门