#include "TileBasemap.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "Scene.hpp" #include "geo/GeoLocalFrame.hpp" #include "ground/TileMath.hpp" namespace geopro::app { namespace { // 天地图 WMTS 令牌(与轨迹图 trajectory_map.html 同源)。 const char* kTk = "aca91d8c9f59a4f779f39061b8a07737"; constexpr int kRootZoom = 9; // 四叉树根层级(单块~78km,±1 覆盖~234km 到天边) constexpr double kTargetPx = 384.0; // 瓦片屏幕像素阈值:超过则细分(越小越清晰但块更多) constexpr int kMaxLeaves = 200; // 一次覆盖的叶瓦片上限(安全兜底,防细分爆炸) constexpr int kMaxConcurrent = 8; // 瓦片请求最大并发(防暴发饱和单域名连接) constexpr int kMinZoom = 3; constexpr int kMaxZoom = 18; constexpr double kGroundZ = 0.0; // 底图置于 z=0 地面参考(剖面深度向下为负,落其下) constexpr double kZEps = 0.02; // 每层级 Z 微偏移:高层级压上面,避免共面瓦片 z-fighting constexpr int kHardCap = 400; // 瓦片硬上限:超过则即便未落地也强制清理,兜底内存 constexpr double kPi = 3.14159265358979323846; // 地面起伏:Mapbox terrain-RGB DEM 瓦片(原版 web 同款源,全球 CDN,比 AWS Terrarium 快)。 // 公式 elev(米) = -10000 + (R*65536 + G*256 + B)*0.1。数据到 z15,更高层级取祖先块。 // kMapboxToken:原版 commercial-admin 的 Mapbox 公开 token(pk.*,客户端用,同 天地图 tk 性质)。 const char* kMapboxToken = "pk.eyJ1IjoidGJ1c2FuIiwiYSI6ImNtZjY2emZneDBkY24ybXB4cmpvdmwzNWYifQ.h6tcQ380WN5AW6fZr08how"; constexpr int kDemMaxZoom = 15; constexpr int kTerrainGrid = 32; // 每瓦片网格分辨率(33x33 顶点) constexpr double kTerrainExag = 1.0; // 地形垂向夸张(1=真实高程) // key 打包:z<<44 | x<<22 | y(z≤18, x/y<2^18 < 2^22)。 void unpackKey(long long key, int& z, int& x, int& y) { z = static_cast(key >> 44); x = static_cast((key >> 22) & 0x3FFFFF); y = static_cast(key & 0x3FFFFF); } // QImage → vtkTexture:转 RGBA + 垂直翻转,使纹理 v=0 对应瓦片南边(与 PlaneSource tcoord 一致)。 vtkSmartPointer makeTexture(const QImage& img) { const QImage rgba = img.convertToFormat(QImage::Format_RGBA8888); const int w = rgba.width(), h = rgba.height(); if (w <= 0 || h <= 0) return nullptr; vtkNew vimg; vimg->SetDimensions(w, h, 1); vimg->AllocateScalars(VTK_UNSIGNED_CHAR, 4); for (int row = 0; row < h; ++row) { const uchar* src = rgba.scanLine(h - 1 - row); auto* dst = static_cast(vimg->GetScalarPointer(0, row, 0)); std::memcpy(dst, src, static_cast(w) * 4); } auto tex = vtkSmartPointer::New(); tex->SetInputData(vimg); tex->InterpolateOn(); // 双线性 tex->MipmapOn(); // 缩小/斜视不闪烁、不糊 tex->SetMaximumAnisotropicFiltering(16); // 斜视角下纹理保持清晰 tex->EdgeClampOn(); // 边缘夹紧,避免相邻瓦片接缝渗色 return tex; } // Terrarium 像素解码高程:(fx,fy)∈[0,1],fy=0 北/顶行。 double demElev(const QImage& dem, double fx, double fy) { const int w = dem.width(), h = dem.height(); if (w <= 0 || h <= 0) return 0.0; const int px = std::clamp(static_cast(std::lround(fx * (w - 1))), 0, w - 1); const int py = std::clamp(static_cast(std::lround(fy * (h - 1))), 0, h - 1); const QRgb c = dem.pixel(px, py); return -10000.0 + (qRed(c) * 65536.0 + qGreen(c) * 256.0 + qBlue(c)) * 0.1; // Mapbox terrain-RGB } } // namespace long long TileBasemap::tileKey(int z, int x, int y) { return (static_cast(z) << 44) | (static_cast(x) << 22) | static_cast(y); } TileBasemap::TileBasemap(geopro::render::Scene& scene, vtkRenderWindow* rw, std::shared_ptr frame, QObject* parent) : QObject(parent), scene_(scene), rw_(rw), frame_(std::move(frame)) { refreshTimer_ = new QTimer(this); refreshTimer_->setSingleShot(true); connect(refreshTimer_, &QTimer::timeout, this, [this]() { refresh(); }); } TileBasemap::~TileBasemap() { if (styleObs_ && observer_) styleObs_->RemoveObserver(observer_); } void TileBasemap::ensureObserver() { if (styleObs_) return; if (!rw_) return; auto* iren = rw_->GetInteractor(); if (!iren) return; auto* style = iren->GetInteractorStyle(); // EndInteractionEvent 由交互样式发出 if (!style) return; styleObs_ = style; observer_ = vtkSmartPointer::New(); observer_->SetClientData(this); observer_->SetCallback(&TileBasemap::onInteractionEnd); styleObs_->AddObserver(vtkCommand::EndInteractionEvent, observer_); } void TileBasemap::onInteractionEnd(vtkObject*, unsigned long, void* clientData, void*) { // 防抖:交互(滚轮/旋转/平移)停止 ~140ms 后才刷新四叉树,避免每格事件都重算卡顿。 if (auto* self = static_cast(clientData)) self->refreshTimer_->start(140); } void TileBasemap::enqueueGet(const QString& url, std::function onDone) { netQueue_.push_back({url, std::move(onDone)}); pumpNetQueue(); } void TileBasemap::pumpNetQueue() { while (netInFlight_ < kMaxConcurrent && !netQueue_.empty()) { const PendingGet req = std::move(netQueue_.front()); netQueue_.pop_front(); ++netInFlight_; QNetworkReply* reply = nam_.get(QNetworkRequest(QUrl(req.url))); auto cb = req.cb; connect(reply, &QNetworkReply::finished, this, [this, reply, cb]() { cb(reply); // 回调内部 deleteLater + 处理 --netInFlight_; pumpNetQueue(); }); } } void TileBasemap::hide() { show(Hidden); } void TileBasemap::show(Kind kind) { ensureObserver(); ++generation_; // 旧回包(含换源前的层)按 generation 丢弃 for (auto& kv : placed_) scene_.renderer()->RemoveViewProp(kv.second); placed_.clear(); inFlight_.clear(); netQueue_.clear(); // 丢弃换源前排队中的请求(在途的按 gen 自然作废) desired_.clear(); // demCache_/texCache_ 跨隐藏-重选保留 → 重选地图秒出,不重拉。 haveBaseline_ = false; // 数据可能已换位置 → 重算基准高程 terrainProbed_ = false; kind_ = kind; if (kind == Hidden) { if (rw_) rw_->Render(); return; } refresh(); // 四叉树覆盖:近细远粗一次铺满(含远处粗块,无需单独粗底层) } void TileBasemap::refineTile(int z, int x, int y, std::set& out, int& count) { if (count >= kMaxLeaves) { out.insert(tileKey(z, x, y)); return; } // 安全上限:停止细分 const int n = 1 << z; if (x < 0 || y < 0 || x >= n || y >= n) return; const geopro::render::LonLatBox b = geopro::render::tileBounds(z, x, y); const auto sw = frame_->toLocal(b.south, b.west); const auto ne = frame_->toLocal(b.north, b.east); // 视锥剔除:瓦片 AABB 全在某视锥面外侧 → 不在视野内,直接丢弃(否则会在屏幕外乱细分耗尽预算)。 const double zmin = -1000.0, zmax = 1000.0; // 地形起伏远小于瓦片尺度,给宽松 z 带 for (int p = 0; p < 6; ++p) { const double* pl = &frustum_[p * 4]; // 内法向:内侧 a·x+b·y+c·z+d ≥ 0 const double vx = pl[0] >= 0 ? ne.x : sw.x; // 取最朝法向的角点(p-vertex) const double vy = pl[1] >= 0 ? ne.y : sw.y; const double vz = pl[2] >= 0 ? zmax : zmin; if (pl[0] * vx + pl[1] * vy + pl[2] * vz + pl[3] < 0.0) return; // 全在外 → 剔除 } const double cx = (sw.x + ne.x) * 0.5, cy = (sw.y + ne.y) * 0.5; // 瓦片中心(局部米) const double g = std::max(std::abs(ne.x - sw.x), std::abs(ne.y - sw.y)); // 瓦片地面尺寸(米) // 该瓦片投影到屏幕的近似像素尺寸 > 阈值且未到最大层级 → 细分为 4 子块(近处更细)。 double screenPx; if (projParallel_) { screenPx = g * projK_; // 平行投影:projK_ = H/(2·parallelScale) } else { const double dx = cx - camX_, dy = cy - camY_, dz = -camZ_; // 相对相机(瓦片 z≈0) const double dist = std::max(1.0, std::sqrt(dx * dx + dy * dy + dz * dz)); screenPx = g * projK_ / dist; // 透视:projK_ = H/(2·tan(vfov/2)) } if (screenPx > kTargetPx && z < kMaxZoom) { refineTile(z + 1, 2 * x, 2 * y, out, count); refineTile(z + 1, 2 * x + 1, 2 * y, out, count); refineTile(z + 1, 2 * x, 2 * y + 1, out, count); refineTile(z + 1, 2 * x + 1, 2 * y + 1, out, count); } else { out.insert(tileKey(z, x, y)); ++count; } } void TileBasemap::refresh() { if (kind_ == Hidden || refreshing_) return; refreshing_ = true; auto* ren = scene_.renderer(); auto* cam = ren ? ren->GetActiveCamera() : nullptr; if (!cam) { refreshing_ = false; return; } const int* sz = ren->GetSize(); const double H = (sz && sz[1] > 0) ? sz[1] : 800.0; double camPos[3]; cam->GetPosition(camPos); camX_ = camPos[0]; camY_ = camPos[1]; camZ_ = camPos[2]; projParallel_ = cam->GetParallelProjection(); projK_ = projParallel_ ? H / (2.0 * std::max(1.0, cam->GetParallelScale())) : H / (2.0 * std::tan(cam->GetViewAngle() * 0.5 * kPi / 180.0)); const double aspect = (sz && sz[1] > 0) ? double(sz[0]) / double(sz[1]) : 1.0; cam->GetFrustumPlanes(aspect, frustum_); // 6 视锥面(供 refineTile 剔除屏幕外瓦片) // 用焦点(必在视锥内)统一各面方向为"内侧≥0",规避 VTK 法向内/外约定差异(否则可能全剔成黑屏)。 double fp[3]; cam->GetFocalPoint(fp); for (int p = 0; p < 6; ++p) { double* pl = &frustum_[p * 4]; if (pl[0] * fp[0] + pl[1] * fp[1] + pl[2] * fp[2] + pl[3] < 0.0) for (int k = 0; k < 4; ++k) pl[k] = -pl[k]; } // 四叉树:从数据中心一圈粗根块出发,按屏幕误差细分 → 近细远粗、铺满视野,无单层级盲区。 desired_.clear(); int count = 0; const auto c = frame_->toLatLon(0.0, 0.0); // 数据中心 const geopro::render::TileXY root = geopro::render::lonLatToTile(c.lon, c.lat, kRootZoom); for (int dy = -1; dy <= 1; ++dy) for (int dx = -1; dx <= 1; ++dx) refineTile(kRootZoom, root.x + dx, root.y + dy, desired_, count); // 拉取缺失瓦片(旧层暂不删,落地后由 purgeStale 清理)。 for (long long key : desired_) { if (placed_.count(key) || inFlight_.count(key)) continue; int z, x, y; unpackKey(key, z, x, y); fetchTile(z, x, y, key); } purgeStale(); ren->ResetCameraClippingRange(); // 交互后扩裁剪面以含新载入的底图瓦片(防被"蒙版"切) if (rw_) rw_->Render(); refreshing_ = false; } void TileBasemap::purgeStale() { // 仅当本轮所有请求都落地(inFlight 空)后再删旧层;否则缩放/平移期间老瓦片留作回退,避免空白闪烁。 // 超过硬上限则强制清理兜底内存(可能短暂空白,极少触发)。 if (!inFlight_.empty() && placed_.size() <= static_cast(kHardCap)) return; bool removed = false; for (auto it = placed_.begin(); it != placed_.end();) { if (desired_.find(it->first) == desired_.end()) { scene_.renderer()->RemoveViewProp(it->second); it = placed_.erase(it); removed = true; } else { ++it; } } if (removed && rw_) rw_->Render(); } void TileBasemap::fetchTile(int z, int x, int y, long long key) { // 命中影像缓存 → 不走网络,直接落地(DEM 多半也已缓存)。重选地图/缩放回看即秒出。 auto cit = texCache_.find(key); if (cit != texCache_.end()) { inFlight_.insert(key); auto tex = cit->second; if (kind_ == Satellite) { fetchTerrain(z, x, y, key, tex); } else { placeActor(key, buildFlat(z, x, y, tex)); inFlight_.erase(key); purgeStale(); if (rw_) rw_->Render(); } return; } const QString layerDir = (kind_ == Satellite) ? QStringLiteral("img_w") : QStringLiteral("vec_w"); const QString layer = (kind_ == Satellite) ? QStringLiteral("img") : QStringLiteral("vec"); const int sub = (x + y) % 8; // 子域负载分担 t0-t7 const QString url = QStringLiteral("http://t%1.tianditu.gov.cn/%2/wmts?service=wmts&request=GetTile" "&version=1.0.0&LAYER=%3&tileMatrixSet=w&TileMatrix=%4&TileRow=%5" "&TileCol=%6&style=default&format=tiles&tk=%7") .arg(sub) .arg(layerDir, layer) .arg(z) .arg(y) .arg(x) .arg(QString::fromLatin1(kTk)); const int gen = generation_; inFlight_.insert(key); enqueueGet(url, [this, key, z, x, y, gen](QNetworkReply* reply) { reply->deleteLater(); // inFlight 保持到瓦片最终落地(起伏/平面),使旧层在新块就位前不被清理 → 无空白闪烁。 QImage img; const bool stale = (gen != generation_) || kind_ == Hidden || desired_.find(key) == desired_.end() || placed_.count(key); const bool ok = !stale && reply->error() == QNetworkReply::NoError && img.loadFromData(reply->readAll()); if (!ok) { inFlight_.erase(key); purgeStale(); if (rw_) rw_->Render(); return; } auto tex = makeTexture(img); if (texCache_.size() > 1200) texCache_.clear(); // 兜底内存;在用纹理由 actor 自身保活 texCache_[key] = tex; // 缓存供重选/缩放回看复用 if (kind_ == Satellite) { fetchTerrain(z, x, y, key, tex); // 拉 DEM 后直接落地起伏块(inFlight 续到那时) } else { placeActor(key, buildFlat(z, x, y, tex)); // 街道图无地形 → 直接平面 inFlight_.erase(key); purgeStale(); if (rw_) rw_->Render(); } }); } void TileBasemap::placeActor(long long key, vtkSmartPointer actor) { if (!actor) return; scene_.addActor(actor); placed_[key] = actor; } void TileBasemap::ensureBaseline(int z, int x, int y, const QImage& dem) { if (haveBaseline_) return; // 基准必须锚"数据中心"的地面高程(确定性):只有含数据中心的块来定,按中心点采样。 // 否则四叉树里随便哪块先到都定基准 → 地形整体偏移 → 三维剖面相对地面忽上忽下。 const auto c = frame_->toLatLon(0.0, 0.0); // 数据中心经纬 const geopro::render::TileXY ct = geopro::render::lonLatToTile(c.lon, c.lat, z); if (ct.x != x || ct.y != y) return; // 此块不含数据中心 → 不用它定基准 const geopro::render::LonLatBox b = geopro::render::tileBounds(z, x, y); const double fx = (c.lon - b.west) / (b.east - b.west); // 数据中心在块内的列比例 const double fy = (b.north - c.lat) / (b.north - b.south); // 行比例(顶=北) baseline_ = demElev(dem, fx, fy); haveBaseline_ = true; qInfo() << "[basemap] 地形启用 baseline(数据中心)=" << baseline_ << "m z=" << z; } vtkSmartPointer TileBasemap::buildFlat(int z, int x, int y, vtkSmartPointer tex) { const geopro::render::LonLatBox b = geopro::render::tileBounds(z, x, y); const auto sw = frame_->toLocal(b.south, b.west); const auto se = frame_->toLocal(b.south, b.east); const auto nw = frame_->toLocal(b.north, b.west); const double gz = kGroundZ + (z - kMinZoom) * kZEps; // 高层级略抬高,压在旧层之上防共面闪烁 // PlaneSource 自动 tcoord:origin=SW→u 西0东1、v 南0北1(与翻转后纹理对齐)。 vtkNew plane; plane->SetOrigin(sw.x, sw.y, gz); plane->SetPoint1(se.x, se.y, gz); plane->SetPoint2(nw.x, nw.y, gz); vtkNew mapper; mapper->SetInputConnection(plane->GetOutputPort()); auto actor = vtkSmartPointer::New(); actor->SetMapper(mapper); actor->SetTexture(tex); actor->GetProperty()->LightingOff(); // 底图不受场景光照 // 注意:UseBounds 保持默认 true → 参与相机裁剪面计算,否则底图会被裁剪面"蒙版"切掉。 // 坐标轴/取景不被底图撑大,由 VtkSceneView 改用"数据自身包围盒"解决(非靠 UseBounds=false)。 return actor; } void TileBasemap::fetchTerrain(int z, int x, int y, long long key, vtkSmartPointer tex) { // Terrarium 数据约到 z15;更高层级取覆盖本块的祖先 DEM 瓦片,按经纬采样其子区域。 const int dz = std::min(z, kDemMaxZoom); const int shift = z - dz; const int dx = x >> shift, dy = y >> shift; const long long demKey = tileKey(dz, dx, dy); // 落地一块瓦片:DEM 有效→起伏,否则→平面兜底;并推进 inFlight/清理。 auto place = [this, key, z, x, y, dz, dx, dy, tex](const QImage* dem) { if (dem && !dem->isNull()) { ensureBaseline(dz, dx, dy, *dem); // 用含数据中心的 DEM 块定基准(确定性) placeActor(key, buildWarped(z, x, y, dz, dx, dy, tex, *dem)); } else { placeActor(key, buildFlat(z, x, y, tex)); // DEM 拉不到 → 平面兜底 } inFlight_.erase(key); purgeStale(); if (rw_) rw_->Render(); }; // 命中缓存:同一祖先 DEM 块的多个瓦片瞬间起伏,免重复网络。 auto cached = demCache_.find(demKey); if (cached != demCache_.end()) { place(&cached->second); return; } if (!terrainProbed_) { terrainProbed_ = true; qInfo() << "[basemap] 首次拉DEM 卫星z=" << z << " → DEMz=" << dz << "(" << dx << "," << dy << ")"; } // Mapbox terrain-RGB(pngraw 无损,保证高程解码准确);原版同源,全球 CDN。 const QString url = QStringLiteral("https://api.mapbox.com/v4/mapbox.terrain-rgb/%1/%2/%3.pngraw?access_token=%4") .arg(dz) .arg(dx) .arg(dy) .arg(QString::fromLatin1(kMapboxToken)); const int gen = generation_; enqueueGet(url, [this, key, demKey, gen, place](QNetworkReply* reply) { reply->deleteLater(); if (gen != generation_ || kind_ != Satellite || desired_.find(key) == desired_.end() || placed_.count(key)) { inFlight_.erase(key); // 过期/移出视野 → 不落地 purgeStale(); if (rw_) rw_->Render(); return; } QImage dem; if (reply->error() != QNetworkReply::NoError) { qWarning() << "[basemap] DEM 拉取失败(降级平面)" << reply->url().toString() << reply->errorString(); } else if (!dem.loadFromData(reply->readAll())) { qWarning() << "[basemap] DEM 解码失败(降级平面)" << reply->url().toString(); dem = QImage(); } else { demCache_[demKey] = dem; // 缓存供同祖先块复用 } place(dem.isNull() ? nullptr : &dem); }); } vtkSmartPointer TileBasemap::buildWarped(int sz, int sx, int sy, int dz, int dx, int dy, vtkSmartPointer tex, const QImage& dem) { const geopro::render::LonLatBox sb = geopro::render::tileBounds(sz, sx, sy); // 卫星块(几何) const geopro::render::LonLatBox db = geopro::render::tileBounds(dz, dx, dy); // DEM 块(采样) const auto sw = frame_->toLocal(sb.south, sb.west); const auto se = frame_->toLocal(sb.south, sb.east); const auto nw = frame_->toLocal(sb.north, sb.west); const double base = kGroundZ + (sz - kMinZoom) * kZEps; // PlaneSource(等距圆柱下平面插值即正确 x/y) + 自动 tcoord;再按各点真实经纬采 DEM 位移 Z。 vtkNew plane; plane->SetOrigin(sw.x, sw.y, base); plane->SetPoint1(se.x, se.y, base); plane->SetPoint2(nw.x, nw.y, base); plane->SetResolution(kTerrainGrid, kTerrainGrid); plane->Update(); auto warped = vtkSmartPointer::New(); warped->DeepCopy(plane->GetOutput()); vtkDataArray* tc = warped->GetPointData()->GetTCoords(); vtkPoints* pts = warped->GetPoints(); const double sLonSpan = sb.east - sb.west, sLatSpan = sb.north - sb.south; const double dLonSpan = db.east - db.west, dLatSpan = db.north - db.south; const vtkIdType n = pts->GetNumberOfPoints(); for (vtkIdType id = 0; id < n; ++id) { double t[2]; tc->GetTuple(id, t); // u:西0东1, v:南0北1 const double lon = sb.west + t[0] * sLonSpan; const double lat = sb.south + t[1] * sLatSpan; const double fx = (lon - db.west) / dLonSpan; // DEM 块内列比例 const double fy = (db.north - lat) / dLatSpan; // DEM 顶行=北 → fy const double elev = demElev(dem, fx, fy); double p[3]; pts->GetPoint(id, p); p[2] = base + (elev - baseline_) * kTerrainExag; pts->SetPoint(id, p); } pts->Modified(); vtkNew mapper; mapper->SetInputData(warped); auto actor = vtkSmartPointer::New(); actor->SetMapper(mapper); actor->SetTexture(tex); actor->GetProperty()->LightingOff(); return actor; // UseBounds 默认 true:参与裁剪面,避免被"蒙版"切掉 } } // namespace geopro::app