diff --git a/src/app/TileBasemap.cpp b/src/app/TileBasemap.cpp index 0218950..534e9e5 100644 --- a/src/app/TileBasemap.cpp +++ b/src/app/TileBasemap.cpp @@ -14,11 +14,15 @@ #include #include #include +#include #include #include #include #include +#include +#include #include +#include #include #include #include @@ -44,12 +48,28 @@ constexpr double kPi = 3.14159265358979323846; constexpr double kEarthCirc = 40075016.686; // 赤道周长(米) = z0 单瓦片地面尺寸 constexpr double kTilesAcross = 4.0; // 视野跨度目标覆盖瓦片数(决定层级) +// 地面起伏:AWS Terrarium DEM 瓦片(免费/无 token,XYZ Web-Mercator 与卫星瓦片对齐)。 +// elev(米) = R*256 + G + B/256 - 32768。数据到约 z15,更高层级无 DEM → 保持平面。 +constexpr int kDemMaxZoom = 15; +constexpr int kTerrainGrid = 32; // 每瓦片网格分辨率(33x33 顶点) +constexpr double kTerrainExag = 1.0; // 地形垂向夸张(1=真实高程) + // 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); } + +// Terrarium 像素解码高程:(fx,fy)∈[0,1],fy=0 北/顶行。 +double demElev(const QImage& dem, double fx, double fy) { + const int w = dem.width(), h = dem.height(); + if (w <= 0 || h <= 0) return 0.0; + const int px = std::clamp(static_cast(std::lround(fx * (w - 1))), 0, w - 1); + const int py = std::clamp(static_cast(std::lround(fy * (h - 1))), 0, h - 1); + const QRgb c = dem.pixel(px, py); + return qRed(c) * 256.0 + qGreen(c) + qBlue(c) / 256.0 - 32768.0; +} } // namespace long long TileBasemap::tileKey(int z, int x, int y) { @@ -91,6 +111,7 @@ void TileBasemap::show(Kind kind) { for (auto& kv : placed_) scene_.renderer()->RemoveViewProp(kv.second); placed_.clear(); inFlight_.clear(); + terrainInFlight_.clear(); desired_.clear(); kind_ = kind; if (kind == Hidden) { @@ -209,6 +230,7 @@ void TileBasemap::fetchTile(int z, int x, int y, long long key) { if (!placed_.count(key) && reply->error() == QNetworkReply::NoError && img.loadFromData(reply->readAll())) { placeTile(key, z, x, y, img); // 仅新瓦片落地;其余(失败/已存在)只推动清理 + if (kind_ == Satellite) fetchTerrain(z, x, y, key); // 平面就位后拉 DEM 换起伏 } purgeStale(); // 本块已解决 → 若整轮落地则清理被顶替的旧层 }); @@ -256,4 +278,86 @@ void TileBasemap::placeTile(long long key, int z, int x, int y, const QImage& im if (rw_) rw_->Render(); } +void TileBasemap::fetchTerrain(int z, int x, int y, long long key) { + if (z > kDemMaxZoom) return; // 高层级无 DEM → 维持平面 + if (terrainInFlight_.count(key)) return; + const QString url = + QStringLiteral("http://s3.amazonaws.com/elevation-tiles-prod/terrarium/%1/%2/%3.png") + .arg(z) + .arg(x) + .arg(y); + const int gen = generation_; + terrainInFlight_.insert(key); + QNetworkReply* reply = nam_.get(QNetworkRequest(QUrl(url))); + connect(reply, &QNetworkReply::finished, this, [this, reply, key, z, x, y, gen]() { + reply->deleteLater(); + terrainInFlight_.erase(key); + if (gen != generation_ || kind_ != Satellite) return; + if (placed_.find(key) == placed_.end()) return; // 平面块已被清理 + if (reply->error() != QNetworkReply::NoError) return; + QImage dem; + if (!dem.loadFromData(reply->readAll())) return; + applyTerrain(key, z, x, y, dem); + }); +} + +void TileBasemap::applyTerrain(long long key, int z, int x, int y, const QImage& dem) { + auto it = placed_.find(key); + if (it == placed_.end()) return; + vtkSmartPointer tex = it->second->GetTexture(); // 复用已贴卫星纹理 + if (!tex) return; + if (!haveBaseline_) { // 基准高程取首块中心 → 地形整体绕 z=0 起伏 + baseline_ = demElev(dem, 0.5, 0.5); + haveBaseline_ = true; + } + auto warped = buildWarped(z, x, y, tex, dem); + scene_.renderer()->RemoveViewProp(it->second); + scene_.addActor(warped); + it->second = warped; + if (rw_) rw_->Render(); +} + +vtkSmartPointer TileBasemap::buildWarped(int z, int x, int y, + vtkSmartPointer tex, + const QImage& dem) { + 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); + const auto nw = frame_->toLocal(b.north, b.west); + const double base = kGroundZ + (z - kMinZoom) * kZEps; + + // PlaneSource(等距圆柱下平面插值即正确 x/y) + 自动 tcoord;再按高程位移每点 Z。 + vtkNew plane; + plane->SetOrigin(sw.x, sw.y, base); + plane->SetPoint1(se.x, se.y, base); + plane->SetPoint2(nw.x, nw.y, base); + plane->SetResolution(kTerrainGrid, kTerrainGrid); + plane->Update(); + + auto warped = vtkSmartPointer::New(); + warped->DeepCopy(plane->GetOutput()); + vtkDataArray* tc = warped->GetPointData()->GetTCoords(); + vtkPoints* pts = warped->GetPoints(); + const vtkIdType n = pts->GetNumberOfPoints(); + for (vtkIdType id = 0; id < n; ++id) { + double t[2]; + tc->GetTuple(id, t); + const double fx = t[0], fy = 1.0 - t[1]; // tcoord v: 南0北1 → fy: 北0南1(对齐 DEM 顶行=北) + const double elev = demElev(dem, fx, fy); + double p[3]; + pts->GetPoint(id, p); + p[2] = base + (elev - baseline_) * kTerrainExag; + pts->SetPoint(id, p); + } + pts->Modified(); + + vtkNew mapper; + mapper->SetInputData(warped); + auto actor = vtkSmartPointer::New(); + actor->SetMapper(mapper); + actor->SetTexture(tex); + actor->GetProperty()->LightingOff(); + return actor; +} + } // namespace geopro::app diff --git a/src/app/TileBasemap.hpp b/src/app/TileBasemap.hpp index b4f23b1..d5873e1 100644 --- a/src/app/TileBasemap.hpp +++ b/src/app/TileBasemap.hpp @@ -11,6 +11,7 @@ class vtkActor; class vtkObject; +class vtkTexture; class vtkRenderWindow; class vtkInteractorObserver; class vtkCallbackCommand; @@ -41,6 +42,10 @@ private: 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); + void fetchTerrain(int z, int x, int y, long long key); // 拉对应 DEM 瓦片(卫星模式) + void applyTerrain(long long key, int z, int x, int y, const QImage& dem); // 平面块换起伏块 + vtkSmartPointer buildWarped(int z, int x, int y, vtkSmartPointer tex, + const QImage& dem); // DEM 位移网格 + 卫星贴图 static void onInteractionEnd(vtkObject*, unsigned long, void* clientData, void*); geopro::render::Scene& scene_; @@ -51,7 +56,10 @@ private: int generation_ = 0; // show/hide/换源 自增,丢弃过期回包 std::map> placed_; // 已贴瓦片:key→actor std::set desired_; // 当前视野应显示的瓦片 key - std::set inFlight_; // 在途请求,避免重复拉 + std::set inFlight_; // 卫星瓦片在途请求 + std::set terrainInFlight_; // DEM 瓦片在途请求 + double baseline_ = 0.0; // 基准高程(首块中心),地形绕 z=0 起伏 + bool haveBaseline_ = false; vtkSmartPointer styleObs_; // 持引用保证回调期有效 vtkSmartPointer observer_; bool refreshing_ = false;