509 lines
22 KiB
C++
509 lines
22 KiB
C++
#include "TileBasemap.hpp"
|
||
|
||
#include <algorithm>
|
||
#include <cmath>
|
||
#include <cstring>
|
||
#include <utility>
|
||
|
||
#include <QDebug>
|
||
#include <QImage>
|
||
#include <QNetworkReply>
|
||
#include <QTimer>
|
||
#include <QNetworkRequest>
|
||
#include <QString>
|
||
#include <QUrl>
|
||
|
||
#include <vtkActor.h>
|
||
#include <vtkCallbackCommand.h>
|
||
#include <vtkCamera.h>
|
||
#include <vtkCommand.h>
|
||
#include <vtkDataArray.h>
|
||
#include <vtkImageData.h>
|
||
#include <vtkInteractorObserver.h>
|
||
#include <vtkNew.h>
|
||
#include <vtkPlaneSource.h>
|
||
#include <vtkPointData.h>
|
||
#include <vtkPolyData.h>
|
||
#include <vtkPolyDataMapper.h>
|
||
#include <vtkPoints.h>
|
||
#include <vtkProperty.h>
|
||
#include <vtkRenderWindow.h>
|
||
#include <vtkRenderWindowInteractor.h>
|
||
#include <vtkRenderer.h>
|
||
#include <vtkTexture.h>
|
||
|
||
#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<int>(key >> 44);
|
||
x = static_cast<int>((key >> 22) & 0x3FFFFF);
|
||
y = static_cast<int>(key & 0x3FFFFF);
|
||
}
|
||
|
||
// QImage → vtkTexture:转 RGBA + 垂直翻转,使纹理 v=0 对应瓦片南边(与 PlaneSource tcoord 一致)。
|
||
vtkSmartPointer<vtkTexture> 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<vtkImageData> 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<uchar*>(vimg->GetScalarPointer(0, row, 0));
|
||
std::memcpy(dst, src, static_cast<size_t>(w) * 4);
|
||
}
|
||
auto tex = vtkSmartPointer<vtkTexture>::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<int>(std::lround(fx * (w - 1))), 0, w - 1);
|
||
const int py = std::clamp(static_cast<int>(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<long long>(z) << 44) | (static_cast<long long>(x) << 22) |
|
||
static_cast<long long>(y);
|
||
}
|
||
|
||
TileBasemap::TileBasemap(geopro::render::Scene& scene, vtkRenderWindow* rw,
|
||
std::shared_ptr<geopro::core::GeoLocalFrame> 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<vtkCallbackCommand>::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<TileBasemap*>(clientData)) self->refreshTimer_->start(140);
|
||
}
|
||
|
||
void TileBasemap::enqueueGet(const QString& url, std::function<void(QNetworkReply*)> 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<long long>& 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<size_t>(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<vtkActor> 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<vtkActor> TileBasemap::buildFlat(int z, int x, int y,
|
||
vtkSmartPointer<vtkTexture> 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<vtkPlaneSource> plane;
|
||
plane->SetOrigin(sw.x, sw.y, gz);
|
||
plane->SetPoint1(se.x, se.y, gz);
|
||
plane->SetPoint2(nw.x, nw.y, gz);
|
||
vtkNew<vtkPolyDataMapper> mapper;
|
||
mapper->SetInputConnection(plane->GetOutputPort());
|
||
auto actor = vtkSmartPointer<vtkActor>::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<vtkTexture> 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<vtkActor> TileBasemap::buildWarped(int sz, int sx, int sy, int dz, int dx, int dy,
|
||
vtkSmartPointer<vtkTexture> 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<vtkPlaneSource> 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<vtkPolyData>::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<vtkPolyDataMapper> mapper;
|
||
mapper->SetInputData(warped);
|
||
auto actor = vtkSmartPointer<vtkActor>::New();
|
||
actor->SetMapper(mapper);
|
||
actor->SetTexture(tex);
|
||
actor->GetProperty()->LightingOff();
|
||
return actor; // UseBounds 默认 true:参与裁剪面,避免被"蒙版"切掉
|
||
}
|
||
|
||
} // namespace geopro::app
|