feat(vtk): 底图LOD-按相机视距自动选瓦片层级+覆盖可视范围+增量增删(交互结束触发)
This commit is contained in:
parent
11349e533c
commit
aaf150ca2e
|
|
@ -1,5 +1,7 @@
|
|||
#include "TileBasemap.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
|
||||
#include <QImage>
|
||||
|
|
@ -9,12 +11,17 @@
|
|||
#include <QUrl>
|
||||
|
||||
#include <vtkActor.h>
|
||||
#include <vtkCallbackCommand.h>
|
||||
#include <vtkCamera.h>
|
||||
#include <vtkCommand.h>
|
||||
#include <vtkImageData.h>
|
||||
#include <vtkInteractorObserver.h>
|
||||
#include <vtkNew.h>
|
||||
#include <vtkPlaneSource.h>
|
||||
#include <vtkPolyDataMapper.h>
|
||||
#include <vtkProperty.h>
|
||||
#include <vtkRenderWindow.h>
|
||||
#include <vtkRenderWindowInteractor.h>
|
||||
#include <vtkRenderer.h>
|
||||
#include <vtkTexture.h>
|
||||
|
||||
|
|
@ -27,68 +34,176 @@ namespace geopro::app {
|
|||
namespace {
|
||||
// 天地图 WMTS 令牌(与轨迹图 trajectory_map.html 同源)。
|
||||
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 kPi = 3.14159265358979323846;
|
||||
constexpr double kEarthCirc = 40075016.686; // 赤道周长(米) = z0 单瓦片地面尺寸
|
||||
constexpr double kTilesAcross = 4.0; // 视野跨度目标覆盖瓦片数(决定层级)
|
||||
|
||||
// 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);
|
||||
}
|
||||
} // 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)) {}
|
||||
|
||||
void TileBasemap::clearTiles() {
|
||||
for (auto& a : tiles_) scene_.renderer()->RemoveViewProp(a);
|
||||
tiles_.clear();
|
||||
TileBasemap::~TileBasemap() {
|
||||
if (styleObs_ && observer_) styleObs_->RemoveObserver(observer_);
|
||||
}
|
||||
|
||||
void TileBasemap::hide() {
|
||||
++generation_;
|
||||
clearTiles();
|
||||
if (rw_) rw_->Render();
|
||||
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::hide() { show(Hidden); }
|
||||
|
||||
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) {
|
||||
++generation_;
|
||||
if (rw_) rw_->Render();
|
||||
return;
|
||||
}
|
||||
const int myGen = ++generation_;
|
||||
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");
|
||||
const QString layer = (kind == Satellite) ? QStringLiteral("img") : QStringLiteral("vec");
|
||||
const int n = 1 << kZoom;
|
||||
refresh();
|
||||
}
|
||||
|
||||
bool TileBasemap::computeView(double& centerLat, double& centerLon, int& zoom) const {
|
||||
auto* ren = scene_.renderer();
|
||||
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 dx = -kRadius; dx <= kRadius; ++dx) {
|
||||
const int tx = center.x + dx, ty = center.y + dy;
|
||||
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 =
|
||||
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(kZoom)
|
||||
.arg(ty)
|
||||
.arg(tx)
|
||||
.arg(z)
|
||||
.arg(y)
|
||||
.arg(x)
|
||||
.arg(QString::fromLatin1(kTk));
|
||||
|
||||
const int gen = generation_;
|
||||
inFlight_.insert(key);
|
||||
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();
|
||||
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;
|
||||
QImage img;
|
||||
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 auto sw = frame_->toLocal(b.south, b.west);
|
||||
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(); // 底图不受场景光照
|
||||
|
||||
scene_.addActor(actor);
|
||||
tiles_.push_back(actor);
|
||||
placed_[key] = actor;
|
||||
if (rw_) rw_->Render();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,42 +3,57 @@
|
|||
#include <QNetworkAccessManager>
|
||||
#include <QObject>
|
||||
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
#include <set>
|
||||
|
||||
#include <vtkSmartPointer.h>
|
||||
|
||||
class vtkActor;
|
||||
class vtkObject;
|
||||
class vtkRenderWindow;
|
||||
class vtkInteractorObserver;
|
||||
class vtkCallbackCommand;
|
||||
class QImage;
|
||||
namespace geopro::render { class Scene; }
|
||||
namespace geopro::core { class GeoLocalFrame; }
|
||||
|
||||
namespace geopro::app {
|
||||
|
||||
// 天地图 WMTS 底图层:以共享 GeoLocalFrame 原点为中心,异步拉取覆盖瓦片贴成地面纹理面(z=0)。
|
||||
// 复用轨迹图同款 token/WMTS。瓦片经同一 frame 配准 → 自动与帘面/轨迹对齐。
|
||||
// 注:本期固定 zoom + 固定覆盖半径(MVP),相机驱动 LOD/数据范围自适应后续再加。
|
||||
// 天地图 WMTS 底图层(局部平面,B 方案)+ LOD:按相机视距自动选瓦片层级、覆盖可视范围,
|
||||
// 缩放/平移结束后增量增删瓦片。复用轨迹图同款 token;瓦片经同一 GeoLocalFrame 配准。
|
||||
class TileBasemap : public QObject {
|
||||
Q_OBJECT
|
||||
public:
|
||||
enum Kind { Street = 0, Satellite = 1, Hidden = 2 };
|
||||
TileBasemap(geopro::render::Scene& scene, vtkRenderWindow* rw,
|
||||
std::shared_ptr<geopro::core::GeoLocalFrame> frame, QObject* parent = nullptr);
|
||||
~TileBasemap() override;
|
||||
|
||||
void show(Kind kind); // 拉取并显示(Hidden 等同 hide)
|
||||
void hide(); // 移除全部瓦片面
|
||||
void show(Kind kind); // 显示某底图(Hidden 等同 hide);记住类型供 LOD 刷新复用
|
||||
void hide(); // 移除全部瓦片
|
||||
void refresh(); // 按当前相机重算层级+覆盖,增量更新瓦片(交互结束回调)
|
||||
|
||||
private:
|
||||
void clearTiles();
|
||||
void placeTile(int z, int x, int y, const QImage& img);
|
||||
static long long tileKey(int z, int x, int y);
|
||||
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_;
|
||||
vtkRenderWindow* rw_;
|
||||
std::shared_ptr<geopro::core::GeoLocalFrame> frame_;
|
||||
QNetworkAccessManager nam_;
|
||||
std::vector<vtkSmartPointer<vtkActor>> tiles_;
|
||||
int generation_ = 0; // 每次 show/hide 自增;迟到的瓦片回包比对丢弃
|
||||
Kind kind_ = Hidden;
|
||||
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
|
||||
|
|
|
|||
Loading…
Reference in New Issue