584 lines
26 KiB
C++
584 lines
26 KiB
C++
#include "TileBasemap.hpp"
|
||
|
||
#include <algorithm>
|
||
#include <cmath>
|
||
#include <cstring>
|
||
#include <utility>
|
||
#include <vector>
|
||
|
||
#include <QCryptographicHash>
|
||
#include <QDebug>
|
||
#include <QImage>
|
||
#include <QNetworkReply>
|
||
#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 double kRangeFactor = 10.0;
|
||
constexpr double kRangeFloor = 2000.0; // 至少 2km(小剖面也有足够地理背景)
|
||
constexpr double kRangeCeil = 30000.0; // 最多 30km(防远裁剪面失控)
|
||
constexpr int kMaxConcurrent = 12; // 瓦片请求最大并发(天地图 8 子域+Mapbox,适度提高吞吐)
|
||
constexpr int kMinZoom = 3;
|
||
constexpr int kMaxZoom = 18;
|
||
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 顶点)
|
||
|
||
// 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;
|
||
}
|
||
|
||
// 天地图「此级别下,该区域无影像」固定占位 JPEG:所有无影像瓦片字节完全一致(实测 size=4769、
|
||
// MD5 固定)。按 大小+MD5 精确识别 → 仅命中该占位图,绝不误判真实影像瓦片。
|
||
bool isTiandituNoImagery(const QByteArray& data) {
|
||
if (data.size() != 4769) return false; // 廉价预筛:仅对疑似占位大小算哈希
|
||
static const QByteArray kNoImageMd5 =
|
||
QByteArray::fromHex("c0edbdcb2c8ddd3e6a5cf09348c0fcb4");
|
||
return QCryptographicHash::hash(data, QCryptographicHash::Md5) == kNoImageMd5;
|
||
}
|
||
|
||
// 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,
|
||
double groundZ)
|
||
: QObject(parent), scene_(scene), rw_(rw), frame_(std::move(frame)), groundZ_(groundZ) {}
|
||
|
||
void TileBasemap::requestRender() {
|
||
// 合并渲染:同一事件循环轮次内的多次请求只渲染一帧(标准做法,避免逐瓦片重复 Render 卡顿)。
|
||
if (renderPending_) return;
|
||
renderPending_ = true;
|
||
QMetaObject::invokeMethod(
|
||
this,
|
||
[this]() {
|
||
renderPending_ = false;
|
||
// 渲染前更新裁剪面:把异步刚落地的瓦片纳入近/远裁剪范围,否则它们会被切(屏幕暗带)。
|
||
if (auto* ren = scene_.renderer()) ren->ResetCameraClippingRange();
|
||
if (rw_) rw_->Render();
|
||
},
|
||
Qt::QueuedConnection);
|
||
}
|
||
|
||
TileBasemap::~TileBasemap() {
|
||
// 移除本实例所有已贴瓦片:多实例(每 2D 平面一份)动态建销时,析构须撤回瓦片,否则渲染器仍持引用、
|
||
// 底图不随平面消失。共享 3D 底图存活至退出故旧码无此清理也无碍,但 per-plane 实例必须清。
|
||
if (auto* ren = scene_.renderer())
|
||
for (auto& kv : placed_) ren->RemoveViewProp(kv.second);
|
||
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*) {
|
||
if (auto* self = static_cast<TileBasemap*>(clientData)) self->refresh();
|
||
}
|
||
|
||
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_ 跨隐藏-重选保留 → 重选地图秒出,不重拉。
|
||
terrainProbed_ = false;
|
||
satMaxZoom_ = kMaxZoom; // 新源/新区域:复位卫星层级上限,重新探测该区域影像覆盖深度
|
||
kind_ = kind;
|
||
if (kind == Hidden) {
|
||
requestRender();
|
||
return;
|
||
}
|
||
refresh(); // 四叉树覆盖:近细远粗一次铺满(地形按真实高程,与剖面同系)
|
||
}
|
||
|
||
void TileBasemap::setVerticalExaggeration(double ve) {
|
||
if (ve <= 0.0 || ve == ve_) return;
|
||
ve_ = ve;
|
||
if (kind_ != Hidden) show(kind_); // 重建地形(高程×新VE),与剖面 VE 保持一致
|
||
}
|
||
|
||
void TileBasemap::setOpacity(double o) {
|
||
o = std::clamp(o, 0.0, 1.0);
|
||
if (o == opacity_) return;
|
||
opacity_ = o;
|
||
if (kind_ != Hidden) show(kind_); // 重建套用新透明度
|
||
}
|
||
|
||
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 全在某侧面外侧 → 不在视野内,丢弃(否则屏幕外乱细分耗尽预算)。
|
||
// 只用 4 个侧面(左右上下),不用近/远裁剪面——远裁剪面随已加载几何变化,
|
||
// 首帧底图未齐时远面贴得近会误剔除远处可见瓦片(等多久都不出、微动才出)。
|
||
const double zmin = -1000.0, zmax = 1000.0; // 地形起伏远小于瓦片尺度,给宽松 z 带
|
||
for (int p = 0; p < 4; ++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)); // 瓦片地面尺寸(米)
|
||
|
||
// 距离上限(按剖面范围动态):以覆盖中心(相机焦点 cenX_,cenY_)为心,瓦片离它太远则不加载——
|
||
// 远裁剪面有界(剖面不被近裁剪面切),也避免拉远无限铺。叶块本身可大于此距离(近端仍在范围内即保留)。
|
||
// 心改用焦点而非原点(0,0):否则 frame 锚在别处数据(如深圳)时,看台湾数据全被剔除→底图空。
|
||
const double rx = cx - cenX_, ry = cy - cenY_;
|
||
if (std::sqrt(rx * rx + ry * ry) - g * 0.5 > maxTileDist_) return;
|
||
|
||
// 该瓦片投影到屏幕的近似像素尺寸 > 阈值且未到最大层级 → 细分为 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))
|
||
}
|
||
// 细分条件:屏幕上太大 → 细分(近细远粗);或瓦片本身比允许范围还大 → 也强制细分,
|
||
// 否则拉到最远时一块巨瓦(如 78km)正好盖住数据中心、过不了距离剔除 → 覆盖超大面积。
|
||
// 卫星层用「学习到的」上限 satMaxZoom_(无影像区域已降级),街道层仍到 kMaxZoom。
|
||
const int maxZ = (kind_ == Satellite) ? satMaxZoom_ : kMaxZoom;
|
||
if ((screenPx > kTargetPx || g > maxTileDist_) && z < maxZ) {
|
||
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];
|
||
}
|
||
// 底图覆盖中心 = 相机焦点(用户正看处)的局部 XY,而非世界原点:frame 锚在首个数据集,看远处别处
|
||
// 数据时原点离视野很远会把全部瓦片距离剔除→底图空。焦点为心则底图随视野走(同 frame 仍与数据对齐)。
|
||
cenX_ = fp[0];
|
||
cenY_ = fp[1];
|
||
|
||
// 底图最大距离按当前剖面合并范围动态定(随勾选增删自动伸缩);无数据用下限。
|
||
maxTileDist_ = kRangeFloor;
|
||
if (dataRadiusProvider_) {
|
||
const double r = dataRadiusProvider_();
|
||
if (r > 0.0) maxTileDist_ = std::clamp(r * kRangeFactor, kRangeFloor, kRangeCeil);
|
||
}
|
||
|
||
// 四叉树:从覆盖中心(相机焦点经纬)一圈粗根块出发,按屏幕误差细分 → 近细远粗、铺满视野,无盲区。
|
||
desired_.clear();
|
||
int count = 0;
|
||
const auto c = frame_->toLatLon(cenX_, cenY_); // 覆盖中心 = 相机焦点(非世界原点)
|
||
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);
|
||
|
||
// 拉取缺失瓦片:按离相机距离排序,最近的先拉 → 用户正看的区域最先出现(而非粗/远块先出)。
|
||
std::vector<std::pair<double, long long>> todo;
|
||
for (long long key : desired_) {
|
||
if (placed_.count(key) || inFlight_.count(key)) continue;
|
||
int z, x, y;
|
||
unpackKey(key, z, x, y);
|
||
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);
|
||
const double cx = (sw.x + ne.x) * 0.5 - camX_, cy = (sw.y + ne.y) * 0.5 - camY_;
|
||
todo.push_back({cx * cx + cy * cy, key});
|
||
}
|
||
std::sort(todo.begin(), todo.end());
|
||
for (const auto& t : todo) {
|
||
int z, x, y;
|
||
unpackKey(t.second, z, x, y);
|
||
fetchTile(z, x, y, t.second);
|
||
}
|
||
|
||
purgeStale();
|
||
ren->ResetCameraClippingRange(); // 交互后扩裁剪面以含新载入的底图瓦片(防被"蒙版"切)
|
||
requestRender();
|
||
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) requestRender();
|
||
}
|
||
|
||
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();
|
||
requestRender();
|
||
}
|
||
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 保持到瓦片最终落地(起伏/平面),使旧层在新块就位前不被清理 → 无空白闪烁。
|
||
const bool stale = (gen != generation_) || kind_ == Hidden ||
|
||
desired_.find(key) == desired_.end() || placed_.count(key);
|
||
const QByteArray data =
|
||
(!stale && reply->error() == QNetworkReply::NoError) ? reply->readAll() : QByteArray();
|
||
// 天地图无影像占位图:该区域此层级无卫星影像 → 学习把卫星上限降到 z-1 并重铺(改用父层真实
|
||
// 影像放大覆盖),不缓存/不落地占位图。仅卫星层适用(街道矢量层全球到 z18 无此占位)。
|
||
if (kind_ == Satellite && !data.isEmpty() && isTiandituNoImagery(data)) {
|
||
inFlight_.erase(key);
|
||
if (z - 1 < satMaxZoom_) {
|
||
satMaxZoom_ = z - 1;
|
||
purgeStale();
|
||
refresh(); // 以新上限重铺该区域
|
||
} else {
|
||
purgeStale();
|
||
requestRender();
|
||
}
|
||
return;
|
||
}
|
||
QImage img;
|
||
const bool ok = !data.isEmpty() && img.loadFromData(data);
|
||
if (!ok) {
|
||
inFlight_.erase(key);
|
||
purgeStale();
|
||
requestRender();
|
||
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();
|
||
requestRender();
|
||
}
|
||
});
|
||
}
|
||
|
||
void TileBasemap::placeActor(long long key, vtkSmartPointer<vtkActor> actor) {
|
||
if (!actor) return;
|
||
scene_.addActor(actor);
|
||
placed_[key] = actor;
|
||
}
|
||
|
||
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 = groundZ_ + (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(); // 底图不受场景光照
|
||
actor->GetProperty()->SetOpacity(opacity_); // 半透明:不遮挡地下剖面
|
||
// 注意: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()) {
|
||
placeActor(key, buildWarped(z, x, y, dz, dx, dy, tex, *dem));
|
||
} else {
|
||
placeActor(key, buildFlat(z, x, y, tex)); // DEM 拉不到 → 平面兜底
|
||
}
|
||
inFlight_.erase(key);
|
||
purgeStale();
|
||
requestRender();
|
||
};
|
||
|
||
// 命中缓存:同一祖先 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();
|
||
requestRender();
|
||
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 = groundZ_ + (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 * ve_; // 真实高程×垂直夸张:与剖面(同样真实高程×VE)同系对齐
|
||
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();
|
||
actor->GetProperty()->SetOpacity(opacity_); // 半透明:不遮挡地下剖面
|
||
return actor; // UseBounds 默认 true:参与裁剪面,避免被"蒙版"切掉
|
||
}
|
||
|
||
} // namespace geopro::app
|