fix(vtk): 二维足迹不可见 + 台湾区天地图底图全占位图

三处缺陷,均由「勾选二维数据集 → VTK 看不到渲染/底图」串起:

1) 摆放默认关闭致足迹静默丢弃
   Column2DDataset「2D视图」下拉可见默认项为 Z=0(setCurrentIndex(1)),
   但该初始信号在 connect 前发射、且组件早于 main.cpp 接线构造 → 永不送达控制器,
   控制器 placement2dMode_ 仍为 0(关闭),勾选被记录却不入场(setChecked2DDatasets
   守卫 placement!=0 不通过)。改:控制器默认 1、main.cpp view2dMode 默认 1,
   与下拉可见默认项对齐,彻底摆脱对信号时序的依赖。

2) 足迹未重锚 frame → 投到数百公里外、移动视角也找不到
   GeoLocalFrame 启动锚在样本 grid1 中位经纬;addCurtain 会重锚到剖面真实经纬,
   但 addMapLine 未重锚 → 台湾足迹(经120.8/纬24.7)按样本锚点投到世界原点数十万米外。
   改:抽出 anchorFrameIfNeeded(剖面/足迹共用),首个带经纬数据(无论帘面或足迹)
   重锚原点;控制器 setChecked2DDatasets 在空场景首批足迹时取景(fitOnArrival)。

3) 台湾区天地图卫星只覆盖到 z16,z17/z18 返回固定「无影像」占位图
   底图四叉树拉近时细分到 kMaxZoom=18 → 台湾中心瓦片全是占位图(实测 z17/z18
   字节恒等 size=4769/MD5 c0edbdcb,z16 为真实影像;内地有 z18 故正常)。
   改:TileBasemap 加自适应 satMaxZoom_,isTiandituNoImagery 按 大小+MD5 精确识别
   占位图 → 学习把卫星上限降到 z-1 并重铺(台湾 18→16 收敛,用 z16 真实影像放大);
   refineTile 卫星层用学习上限,街道矢量仍到 z18;show()/换源复位、refresh 保留。
   内地项目零影响(z18 有影像,永不触发降级)。

测试:253/253 通过;新增 TwoDDefaultPlacementRendersAtZeroOnCheck 回归,
原依赖「默认关闭」的两个用例改为显式 set2DPlacement(0)。
This commit is contained in:
gaozheng 2026-06-22 20:31:25 +08:00
parent 5e60446210
commit 8e91351dab
8 changed files with 84 additions and 24 deletions

View File

@ -6,6 +6,7 @@
#include <utility>
#include <vector>
#include <QCryptographicHash>
#include <QDebug>
#include <QImage>
#include <QNetworkReply>
@ -94,6 +95,15 @@ vtkSmartPointer<vtkTexture> makeTexture(const QImage& img) {
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();
@ -183,6 +193,7 @@ void TileBasemap::show(Kind kind) {
desired_.clear();
// demCache_/texCache_ 跨隐藏-重选保留 → 重选地图秒出,不重拉。
terrainProbed_ = false;
satMaxZoom_ = kMaxZoom; // 新源/新区域:复位卫星层级上限,重新探测该区域影像覆盖深度
kind_ = kind;
if (kind == Hidden) {
requestRender();
@ -236,7 +247,9 @@ void TileBasemap::refineTile(int z, int x, int y, std::set<long long>& out, int&
}
// 细分条件:屏幕上太大 → 细分(近细远粗);或瓦片本身比允许范围还大 → 也强制细分,
// 否则拉到最远时一块巨瓦(如 78km)正好盖住数据中心、过不了距离剔除 → 覆盖超大面积。
if ((screenPx > kTargetPx || g > maxTileDist_) && z < kMaxZoom) {
// 卫星层用「学习到的」上限 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);
@ -368,11 +381,26 @@ void TileBasemap::fetchTile(int z, int x, int y, long long 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());
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();

View File

@ -78,6 +78,10 @@ private:
double ve_ = 1.0; // 地形垂向夸张(与剖面 verticalExaggeration 一致才对齐)
double maxTileDist_ = 2000.0; // 底图最大距离(米),每次刷新按剖面范围动态算
std::function<double()> dataRadiusProvider_; // 返回当前勾选剖面合并范围的半径
// 卫星层「学习到的」最大可用层级:天地图卫星影像各区域覆盖深度不同(内地到z18, 台湾等仅到z16),
// 超出则回固定「此级别下无影像」占位图。检测到占位即把上限降到 z-1 并重铺(改用父层真实影像放大),
// 使该区域不再请求无影像层。show()/换源时复位为 kMaxZoom 以便新区域重新探测。
int satMaxZoom_ = 18;
// 四叉树当前帧相机参数(refresh 写, refineTile 读):相机位置 + 投影系数 + 视锥 6 面。
double camX_ = 0, camY_ = 0, camZ_ = 0;
double projK_ = 1.0;

View File

@ -125,23 +125,26 @@ void VtkSceneView::addSurveyLine(const geopro::core::Grid& grid) {
}
}
void VtkSceneView::addCurtain(const std::string& dsId, const geopro::core::Grid& grid,
const geopro::core::ColorScale& cs) {
// 首个带经纬度的剖面到达 → 把 GeoLocalFrame 原点重锚到该剖面 lat/lon 中心:使局部坐标从 0 附近起
// (轴刻度有意义),同一选择内多条剖面共用此原点 → 相互地理配准。无经纬剖面是平面、不受原点影响。
const int nx = grid.nx();
if (!frameAnchoredToData_ && nx > 0 && static_cast<int>(grid.lat.size()) >= nx &&
static_cast<int>(grid.lon.size()) >= nx) {
double la0 = grid.lat[0], la1 = grid.lat[0], lo0 = grid.lon[0], lo1 = grid.lon[0];
for (int i = 1; i < nx; ++i) {
la0 = std::min(la0, grid.lat[i]); la1 = std::max(la1, grid.lat[i]);
lo0 = std::min(lo0, grid.lon[i]); lo1 = std::max(lo1, grid.lon[i]);
void VtkSceneView::anchorFrameIfNeeded(const std::vector<double>& lat,
const std::vector<double>& lon, int n) {
// 首个带经纬数据到达 → 把 GeoLocalFrame 原点重锚到其 lat/lon 包围盒中心:使局部坐标从 0 附近起
// (轴刻度有意义),同一选择内多条剖面/足迹共用此原点 → 相互地理配准。已锚或无经纬则保持不动。
if (frameAnchoredToData_ || n < 1) return;
if (static_cast<int>(lat.size()) < n || static_cast<int>(lon.size()) < n) return;
double la0 = lat[0], la1 = lat[0], lo0 = lon[0], lo1 = lon[0];
for (int i = 1; i < n; ++i) {
la0 = std::min(la0, lat[i]); la1 = std::max(la1, lat[i]);
lo0 = std::min(lo0, lon[i]); lo1 = std::max(lo1, lon[i]);
}
// 就地重锚共享 frame不换对象→ 同持此 frame 的底图层等随即一致对齐。
frame_->reanchor((la0 + la1) / 2.0, (lo0 + lo1) / 2.0);
frameAnchoredToData_ = true;
if (onFrameReanchored) onFrameReanchored(); // 通知底图刷新到数据位置
}
}
void VtkSceneView::addCurtain(const std::string& dsId, const geopro::core::Grid& grid,
const geopro::core::ColorScale& cs) {
anchorFrameIfNeeded(grid.lat, grid.lon, grid.nx()); // 首个带经纬剖面 → 重锚原点
auto curtain = geopro::render::buildCurtain(grid, cs, *frame_);
if (curtain) {
curtain->SetScale(1.0, 1.0, verticalExaggeration_); // 纵向夸张成墙
@ -175,6 +178,8 @@ void VtkSceneView::addMapLine(const std::string& dsId, const geopro::data::MapLi
double worldZ) {
// 2D 足迹:经共享 frame 投影到世界 XY、Z=worldZ。按 dsId 跟踪(与帘面同 dsProps_ → removeDataset 复用)。
// worldZ 已是最终世界高程(含摆放语义),不再施加 VE足迹是水平线非随深度的竖直图元
// 足迹可能是首个(且唯一)带经纬的数据 → 与帘面同样重锚原点,否则按样本默认原点投到数百公里外不可见。
anchorFrameIfNeeded(line.lat, line.lon, static_cast<int>(line.lat.size()));
auto actor = geopro::render::buildMapLine(line.lat, line.lon, worldZ, *frame_);
if (actor) {
scene_.addActor(actor);

View File

@ -76,6 +76,9 @@ public:
std::function<void()> onCameraChanged;
private:
// 首个带经纬数据(剖面/足迹)到达时把共享 frame 重锚到其 lat/lon 包围盒中心:使数据落在世界原点近旁
// (否则样本默认原点可能离真实数据数百公里→图元在视锥外、移动视角也找不到)。已锚或无经纬则跳过。
void anchorFrameIfNeeded(const std::vector<double>& lat, const std::vector<double>& lon, int n);
// 按当前坐标轴设置 + 场景包围盒重建坐标轴 proprender 末尾调)。
void rebuildAxes();
void removeProps(std::vector<vtkSmartPointer<vtkProp>>& props); // 从 renderer 移除并清空

View File

@ -861,7 +861,9 @@ void buildWorkbench(QMainWindow& window, geopro::data::LocalSampleRepository& re
sceneCtrl, &geopro::controller::VtkSceneController::setChecked2DDatasets);
// 2D视图下拉(0关闭/1 Z=0/2顶部/3底部/4自定义) + 自定义 Z → 控制器重摆已勾选足迹。
auto custom2dZ = std::make_shared<double>(0.0);
auto view2dMode = std::make_shared<int>(0);
// 默认 1(Z=0)与「2D视图」下拉可见默认项(setCurrentIndex(1))及控制器默认摆放一致——
// 组合框初始 view2DModeChanged 因 connect 前发射而丢失,此处不可依赖其同步初值。
auto view2dMode = std::make_shared<int>(1);
QObject::connect(drawer->col2D(), &geopro::app::Column2DDataset::view2DModeChanged, sceneCtrl,
[sceneCtrl, custom2dZ, view2dMode](int mode) {
*view2dMode = mode;

View File

@ -61,12 +61,14 @@ void VtkSceneController::setChecked2DDatasets(const QStringList& dsIds) {
// 二维足迹始终画进 View3D 场景,且按 dsId 跟踪 → 一律增量 diff不全量重建不打断 3D 帘面/体)。
const std::set<std::string> oldSet(checked2dDs_.begin(), checked2dDs_.end());
const std::set<std::string> newSet(newDs.begin(), newDs.end());
// 此前空场景(无 3D 数据且无 2D 足迹) → 首批足迹到场自动取景;否则增量追加保持相机不跳。
const bool wasEmpty = checkedDs_.empty() && checked2dDs_.empty();
for (const auto& id : checked2dDs_)
if (!newSet.count(id)) view_.removeDataset(id); // 取消勾选 → 移除该足迹图元
checked2dDs_ = std::move(newDs);
fitOnArrival_ = false; // 足迹增量追加:保持当前相机不跳
fitOnArrival_ = wasEmpty; // 首批足迹(空场景)取景;否则保持当前相机不跳
// 足迹画进 View3D 场景mode=0 关闭 → 仅记录勾选不渲染(见 set2DPlacement 切回时补画)。
if (placement2dMode_ != 0 && mode_ == ViewMode::View3D) {

View File

@ -81,7 +81,10 @@ private:
// 二维足迹勾选集(与 checkedDs_ 独立;都画进 View3D 场景)。按 dsId 增量加/删。
std::vector<std::string> checked2dDs_;
// 二维足迹摆放mode 0关闭/1 Z=0/2顶部/3底部/4自定义customZ2d_ 仅 mode=4 用。
int placement2dMode_ = 0;
// 默认 Z=0(1) 与 Column2DDataset「2D视图」下拉可见默认项一致——避免「下拉显示 Z=0 但
// 控制器实为关闭」的初始信号丢失desync(组合框 setCurrentIndex 在 connect 前发射、且
// 组件早于 main.cpp 接线构造,初始 view2DModeChanged 永不送达),致勾选足迹静默不渲染。
int placement2dMode_ = 1;
double customZ2d_ = 0.0;
ViewMode mode_ = ViewMode::Map2D;
bool showCurtain_ = true;

View File

@ -406,15 +406,27 @@ TEST(VtkSceneController, ZoomAndFitForwarded) {
// ── 二维数据集视图:足迹平铺进 View3D ──
// 默认摆放模式=关闭(0) → 勾选 2D 足迹不渲染(仅记录勾选)。
// 显式关闭摆放(0) → 勾选 2D 足迹不渲染(仅记录勾选)。
TEST(VtkSceneController, TwoDPlacementOffDoesNotRender) {
FakeDsRepo ds; FakeSceneRepo sc; FakeView view;
VtkSceneController c(ds, sc, view);
c.setViewMode(ViewMode::View3D);
c.setChecked2DDatasets({"traj1"}); // 摆放默认关闭
c.set2DPlacement(0, 0.0); // 显式关闭
c.setChecked2DDatasets({"traj1"});
EXPECT_EQ(view.mapLines, 0);
}
// 回归(足迹默认不渲染 bug):默认摆放=Z=0(1),与 2D视图下拉可见默认项一致 →
// 仅勾选 2D 足迹(不手动调 set2DPlacement即应在 View3D 渲染worldZ=0。
TEST(VtkSceneController, TwoDDefaultPlacementRendersAtZeroOnCheck) {
FakeDsRepo ds; FakeSceneRepo sc; FakeView view;
VtkSceneController c(ds, sc, view);
c.setViewMode(ViewMode::View3D);
c.setChecked2DDatasets({"traj1"}); // 不调 set2DPlacement依赖默认摆放
EXPECT_EQ(view.mapLines, 1);
EXPECT_DOUBLE_EQ(view.lastMapLineZ, 0.0);
}
// 摆放 Z=0(1) + 勾选足迹 → 1 条 mapLineworldZ=0不影响帘面/体素计数。
TEST(VtkSceneController, TwoDPlacementZeroAddsMapLine) {
FakeDsRepo ds; FakeSceneRepo sc; FakeView view;
@ -494,6 +506,7 @@ TEST(VtkSceneController, TwoDPlacementOffToOnDrawsCheckedFootprint) {
FakeDsRepo ds; FakeSceneRepo sc; FakeView view;
VtkSceneController c(ds, sc, view);
c.setViewMode(ViewMode::View3D);
c.set2DPlacement(0, 0.0); // 显式关闭(默认已是 Z=0
c.setChecked2DDatasets({"traj1"}); // 关闭态:仅记录
ASSERT_EQ(view.mapLines, 0);