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:
parent
5e60446210
commit
8e91351dab
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
// 按当前坐标轴设置 + 场景包围盒重建坐标轴 prop(render 末尾调)。
|
||||
void rebuildAxes();
|
||||
void removeProps(std::vector<vtkSmartPointer<vtkProp>>& props); // 从 renderer 移除并清空
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 条 mapLine,worldZ=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);
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue