feat/vtk-3d-view #7

Merged
gaozheng merged 301 commits from feat/vtk-3d-view into main 2026-06-27 18:43:52 +08:00
2 changed files with 180 additions and 50 deletions
Showing only changes of commit aaf150ca2e - Show all commits

View File

@ -1,5 +1,7 @@
#include "TileBasemap.hpp" #include "TileBasemap.hpp"
#include <algorithm>
#include <cmath>
#include <cstring> #include <cstring>
#include <QImage> #include <QImage>
@ -9,12 +11,17 @@
#include <QUrl> #include <QUrl>
#include <vtkActor.h> #include <vtkActor.h>
#include <vtkCallbackCommand.h>
#include <vtkCamera.h>
#include <vtkCommand.h>
#include <vtkImageData.h> #include <vtkImageData.h>
#include <vtkInteractorObserver.h>
#include <vtkNew.h> #include <vtkNew.h>
#include <vtkPlaneSource.h> #include <vtkPlaneSource.h>
#include <vtkPolyDataMapper.h> #include <vtkPolyDataMapper.h>
#include <vtkProperty.h> #include <vtkProperty.h>
#include <vtkRenderWindow.h> #include <vtkRenderWindow.h>
#include <vtkRenderWindowInteractor.h>
#include <vtkRenderer.h> #include <vtkRenderer.h>
#include <vtkTexture.h> #include <vtkTexture.h>
@ -27,68 +34,176 @@ namespace geopro::app {
namespace { namespace {
// 天地图 WMTS 令牌(与轨迹图 trajectory_map.html 同源)。 // 天地图 WMTS 令牌(与轨迹图 trajectory_map.html 同源)。
const char* kTk = "aca91d8c9f59a4f779f39061b8a07737"; const char* kTk = "aca91d8c9f59a4f779f39061b8a07737";
constexpr int kZoom = 16; // 固定层级(MVP):单瓦片 ~数百米,半径覆盖典型测线 constexpr int kRadius = 3; // 中心瓦片 ±3 → 最多 7x7=49 块,留平移余量
constexpr int kRadius = 3; // 中心瓦片 ±3 → 7x7=49 块,约数公里 constexpr int kMinZoom = 3;
constexpr int kMaxZoom = 18;
constexpr double kGroundZ = 0.0; // 底图置于 z=0 地面参考(剖面深度向下为负,落其下) constexpr double kGroundZ = 0.0; // 底图置于 z=0 地面参考(剖面深度向下为负,落其下)
constexpr double kPi = 3.14159265358979323846;
constexpr double kEarthCirc = 40075016.686; // 赤道周长(米) = z0 单瓦片地面尺寸
constexpr double kTilesAcross = 4.0; // 视野跨度目标覆盖瓦片数(决定层级)
// key 打包z<<44 | x<<22 | yz≤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);
}
} // namespace } // 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, TileBasemap::TileBasemap(geopro::render::Scene& scene, vtkRenderWindow* rw,
std::shared_ptr<geopro::core::GeoLocalFrame> frame, QObject* parent) std::shared_ptr<geopro::core::GeoLocalFrame> frame, QObject* parent)
: QObject(parent), scene_(scene), rw_(rw), frame_(std::move(frame)) {} : QObject(parent), scene_(scene), rw_(rw), frame_(std::move(frame)) {}
void TileBasemap::clearTiles() { TileBasemap::~TileBasemap() {
for (auto& a : tiles_) scene_.renderer()->RemoveViewProp(a); if (styleObs_ && observer_) styleObs_->RemoveObserver(observer_);
tiles_.clear();
} }
void TileBasemap::hide() { void TileBasemap::ensureObserver() {
++generation_; if (styleObs_) return;
clearTiles(); if (!rw_) return;
if (rw_) rw_->Render(); 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::hide() { show(Hidden); }
void TileBasemap::show(Kind kind) { void TileBasemap::show(Kind kind) {
clearTiles(); ensureObserver();
++generation_; // 旧回包(含换源前的层)按 generation 丢弃
for (auto& kv : placed_) scene_.renderer()->RemoveViewProp(kv.second);
placed_.clear();
inFlight_.clear();
desired_.clear();
kind_ = kind;
if (kind == Hidden) { if (kind == Hidden) {
++generation_;
if (rw_) rw_->Render(); if (rw_) rw_->Render();
return; return;
} }
const int myGen = ++generation_; refresh();
const auto c = frame_->toLatLon(0.0, 0.0); // 数据原点经纬(已重锚到真实剖面中心) }
const geopro::render::TileXY center = geopro::render::lonLatToTile(c.lon, c.lat, kZoom);
const QString layerDir = (kind == Satellite) ? QStringLiteral("img_w") : QStringLiteral("vec_w"); bool TileBasemap::computeView(double& centerLat, double& centerLon, int& zoom) const {
const QString layer = (kind == Satellite) ? QStringLiteral("img") : QStringLiteral("vec"); auto* ren = scene_.renderer();
const int n = 1 << kZoom; if (!ren) return false;
auto* cam = ren->GetActiveCamera();
if (!cam) return false;
double fp[3];
cam->GetFocalPoint(fp);
const auto c = frame_->toLatLon(fp[0], fp[1]); // 焦点局部米(x East,y North) → 经纬
centerLat = c.lat;
centerLon = c.lon;
// 视野在地面的近似跨度(米):透视用 2·dist·tan(半 FOV),平行投影用 2·parallelScale。
double span;
if (cam->GetParallelProjection()) {
span = 2.0 * cam->GetParallelScale();
} else {
const double halfAngle = cam->GetViewAngle() * 0.5 * kPi / 180.0;
span = 2.0 * cam->GetDistance() * std::tan(halfAngle);
}
span = std::clamp(span, 1.0, 4.0e7);
const double cosLat = std::max(0.01, std::cos(centerLat * kPi / 180.0));
const double tileMeters = span / kTilesAcross;
int z = static_cast<int>(std::lround(std::log2(kEarthCirc * cosLat / tileMeters)));
zoom = std::clamp(z, kMinZoom, kMaxZoom);
return true;
}
void TileBasemap::refresh() {
if (kind_ == Hidden || refreshing_) return;
refreshing_ = true;
double clat = 0, clon = 0;
int zoom = 0;
if (!computeView(clat, clon, zoom)) {
refreshing_ = false;
return;
}
const geopro::render::TileXY center = geopro::render::lonLatToTile(clon, clat, zoom);
const int n = 1 << zoom;
desired_.clear();
for (int dy = -kRadius; dy <= kRadius; ++dy) { for (int dy = -kRadius; dy <= kRadius; ++dy) {
for (int dx = -kRadius; dx <= kRadius; ++dx) { for (int dx = -kRadius; dx <= kRadius; ++dx) {
const int tx = center.x + dx, ty = center.y + dy; const int tx = center.x + dx, ty = center.y + dy;
if (tx < 0 || ty < 0 || tx >= n || ty >= n) continue; if (tx < 0 || ty < 0 || tx >= n || ty >= n) continue;
const int sub = (tx + ty) % 8; // 子域负载分担 t0-t7 desired_.insert(tileKey(zoom, tx, ty));
}
}
// 移除离开视野/换层级的瓦片。
for (auto it = placed_.begin(); it != placed_.end();) {
if (desired_.find(it->first) == desired_.end()) {
scene_.renderer()->RemoveViewProp(it->second);
it = placed_.erase(it);
} else {
++it;
}
}
// 拉取缺失瓦片。
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);
}
if (rw_) rw_->Render();
refreshing_ = false;
}
void TileBasemap::fetchTile(int z, int x, int y, long long key) {
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 = const QString url =
QStringLiteral("http://t%1.tianditu.gov.cn/%2/wmts?service=wmts&request=GetTile" 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" "&version=1.0.0&LAYER=%3&tileMatrixSet=w&TileMatrix=%4&TileRow=%5"
"&TileCol=%6&style=default&format=tiles&tk=%7") "&TileCol=%6&style=default&format=tiles&tk=%7")
.arg(sub) .arg(sub)
.arg(layerDir, layer) .arg(layerDir, layer)
.arg(kZoom) .arg(z)
.arg(ty) .arg(y)
.arg(tx) .arg(x)
.arg(QString::fromLatin1(kTk)); .arg(QString::fromLatin1(kTk));
const int gen = generation_;
inFlight_.insert(key);
QNetworkReply* reply = nam_.get(QNetworkRequest(QUrl(url))); QNetworkReply* reply = nam_.get(QNetworkRequest(QUrl(url)));
connect(reply, &QNetworkReply::finished, this, [this, reply, tx, ty, myGen]() { connect(reply, &QNetworkReply::finished, this, [this, reply, key, z, x, y, gen]() {
reply->deleteLater(); reply->deleteLater();
if (myGen != generation_) return; // 已被新的 show/hide 取代 → 丢弃 inFlight_.erase(key);
if (gen != generation_) return; // 换源/隐藏后丢弃
if (kind_ == Hidden) return;
if (desired_.find(key) == desired_.end()) return; // 已移出视野
if (placed_.count(key)) return;
if (reply->error() != QNetworkReply::NoError) return; if (reply->error() != QNetworkReply::NoError) return;
QImage img; QImage img;
if (!img.loadFromData(reply->readAll())) return; if (!img.loadFromData(reply->readAll())) return;
placeTile(kZoom, tx, ty, img); placeTile(key, z, x, y, img);
}); });
} }
}
}
void TileBasemap::placeTile(int z, int x, int y, const QImage& img) { void TileBasemap::placeTile(long long key, int z, int x, int y, const QImage& img) {
const geopro::render::LonLatBox b = geopro::render::tileBounds(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 sw = frame_->toLocal(b.south, b.west);
const auto se = frame_->toLocal(b.south, b.east); const auto se = frame_->toLocal(b.south, b.east);
@ -125,7 +240,7 @@ void TileBasemap::placeTile(int z, int x, int y, const QImage& img) {
actor->GetProperty()->LightingOff(); // 底图不受场景光照 actor->GetProperty()->LightingOff(); // 底图不受场景光照
scene_.addActor(actor); scene_.addActor(actor);
tiles_.push_back(actor); placed_[key] = actor;
if (rw_) rw_->Render(); if (rw_) rw_->Render();
} }

View File

@ -3,42 +3,57 @@
#include <QNetworkAccessManager> #include <QNetworkAccessManager>
#include <QObject> #include <QObject>
#include <map>
#include <memory> #include <memory>
#include <vector> #include <set>
#include <vtkSmartPointer.h> #include <vtkSmartPointer.h>
class vtkActor; class vtkActor;
class vtkObject;
class vtkRenderWindow; class vtkRenderWindow;
class vtkInteractorObserver;
class vtkCallbackCommand;
class QImage; class QImage;
namespace geopro::render { class Scene; } namespace geopro::render { class Scene; }
namespace geopro::core { class GeoLocalFrame; } namespace geopro::core { class GeoLocalFrame; }
namespace geopro::app { namespace geopro::app {
// 天地图 WMTS 底图层:以共享 GeoLocalFrame 原点为中心,异步拉取覆盖瓦片贴成地面纹理面(z=0)。 // 天地图 WMTS 底图层局部平面B 方案)+ LOD按相机视距自动选瓦片层级、覆盖可视范围
// 复用轨迹图同款 token/WMTS。瓦片经同一 frame 配准 → 自动与帘面/轨迹对齐。 // 缩放/平移结束后增量增删瓦片。复用轨迹图同款 token瓦片经同一 GeoLocalFrame 配准。
// 注:本期固定 zoom + 固定覆盖半径(MVP),相机驱动 LOD/数据范围自适应后续再加。
class TileBasemap : public QObject { class TileBasemap : public QObject {
Q_OBJECT Q_OBJECT
public: public:
enum Kind { Street = 0, Satellite = 1, Hidden = 2 }; enum Kind { Street = 0, Satellite = 1, Hidden = 2 };
TileBasemap(geopro::render::Scene& scene, vtkRenderWindow* rw, TileBasemap(geopro::render::Scene& scene, vtkRenderWindow* rw,
std::shared_ptr<geopro::core::GeoLocalFrame> frame, QObject* parent = nullptr); std::shared_ptr<geopro::core::GeoLocalFrame> frame, QObject* parent = nullptr);
~TileBasemap() override;
void show(Kind kind); // 拉取并显示Hidden 等同 hide void show(Kind kind); // 显示某底图Hidden 等同 hide记住类型供 LOD 刷新复用
void hide(); // 移除全部瓦片面 void hide(); // 移除全部瓦片
void refresh(); // 按当前相机重算层级+覆盖,增量更新瓦片(交互结束回调)
private: private:
void clearTiles(); static long long tileKey(int z, int x, int y);
void placeTile(int z, int x, int y, const QImage& img); void ensureObserver(); // 首次显示时挂到交互样式的 EndInteractionEvent
bool computeView(double& centerLat, double& centerLon, int& zoom) const;
void fetchTile(int z, int x, int y, long long key);
void placeTile(long long key, int z, int x, int y, const QImage& img);
static void onInteractionEnd(vtkObject*, unsigned long, void* clientData, void*);
geopro::render::Scene& scene_; geopro::render::Scene& scene_;
vtkRenderWindow* rw_; vtkRenderWindow* rw_;
std::shared_ptr<geopro::core::GeoLocalFrame> frame_; std::shared_ptr<geopro::core::GeoLocalFrame> frame_;
QNetworkAccessManager nam_; QNetworkAccessManager nam_;
std::vector<vtkSmartPointer<vtkActor>> tiles_; Kind kind_ = Hidden;
int generation_ = 0; // 每次 show/hide 自增;迟到的瓦片回包比对丢弃 int generation_ = 0; // show/hide/换源 自增,丢弃过期回包
std::map<long long, vtkSmartPointer<vtkActor>> placed_; // 已贴瓦片key→actor
std::set<long long> desired_; // 当前视野应显示的瓦片 key
std::set<long long> inFlight_; // 在途请求,避免重复拉
vtkSmartPointer<vtkInteractorObserver> styleObs_; // 持引用保证回调期有效
vtkSmartPointer<vtkCallbackCommand> observer_;
bool refreshing_ = false;
}; };
} // namespace geopro::app } // namespace geopro::app