diff --git a/src/app/TileBasemap.cpp b/src/app/TileBasemap.cpp index a679f9f..7af4d07 100644 --- a/src/app/TileBasemap.cpp +++ b/src/app/TileBasemap.cpp @@ -1,5 +1,7 @@ #include "TileBasemap.hpp" +#include +#include #include #include @@ -9,12 +11,17 @@ #include #include +#include +#include +#include #include +#include #include #include #include #include #include +#include #include #include @@ -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 double kGroundZ = 0.0; // 底图置于 z=0 地面参考(剖面深度向下为负,落其下) +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(key >> 44); + x = static_cast((key >> 22) & 0x3FFFFF); + y = static_cast(key & 0x3FFFFF); +} } // namespace +long long TileBasemap::tileKey(int z, int x, int y) { + return (static_cast(z) << 44) | (static_cast(x) << 22) | + static_cast(y); +} + TileBasemap::TileBasemap(geopro::render::Scene& scene, vtkRenderWindow* rw, std::shared_ptr 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::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(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(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 - 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(QString::fromLatin1(kTk)); - QNetworkReply* reply = nam_.get(QNetworkRequest(QUrl(url))); - connect(reply, &QNetworkReply::finished, this, [this, reply, tx, ty, myGen]() { - reply->deleteLater(); - if (myGen != generation_) return; // 已被新的 show/hide 取代 → 丢弃 - if (reply->error() != QNetworkReply::NoError) return; - QImage img; - if (!img.loadFromData(reply->readAll())) return; - placeTile(kZoom, tx, ty, img); - }); + 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::placeTile(int z, int x, int y, const QImage& img) { +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(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, key, z, x, y, gen]() { + reply->deleteLater(); + 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(key, z, x, y, 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(); } diff --git a/src/app/TileBasemap.hpp b/src/app/TileBasemap.hpp index ab5bdd0..b0cdf83 100644 --- a/src/app/TileBasemap.hpp +++ b/src/app/TileBasemap.hpp @@ -3,42 +3,57 @@ #include #include +#include #include -#include +#include #include 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 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 frame_; QNetworkAccessManager nam_; - std::vector> tiles_; - int generation_ = 0; // 每次 show/hide 自增;迟到的瓦片回包比对丢弃 + Kind kind_ = Hidden; + int generation_ = 0; // show/hide/换源 自增,丢弃过期回包 + std::map> placed_; // 已贴瓦片:key→actor + std::set desired_; // 当前视野应显示的瓦片 key + std::set inFlight_; // 在途请求,避免重复拉 + vtkSmartPointer styleObs_; // 持引用保证回调期有效 + vtkSmartPointer observer_; + bool refreshing_ = false; }; } // namespace geopro::app