diff --git a/src/render/CMakeLists.txt b/src/render/CMakeLists.txt index 330356f..64b8e69 100644 --- a/src/render/CMakeLists.txt +++ b/src/render/CMakeLists.txt @@ -2,7 +2,8 @@ find_package(VTK REQUIRED COMPONENTS CommonCore CommonDataModel FiltersGeometry find_package(GDAL CONFIG REQUIRED) add_library(geopro_render STATIC Scene.cpp ColorLutBuilder.cpp CameraPreset.cpp VoxelFromScatters.cpp ContourBands.cpp actors/GridContourActor.cpp actors/VoxelActor.cpp actors/CurtainActor.cpp actors/MapLineActor.cpp actors/ScatterActor.cpp actors/AnomalyActor.cpp actors/ElectrodeActor.cpp actors/TerrainActor.cpp actors/AxesActor.cpp - interact/SlicePlaneMath.cpp interact/SliceTool.cpp interact/PickInteractorStyle.cpp interact/InteractionManager.cpp) + interact/SlicePlaneMath.cpp interact/SliceTool.cpp interact/PickInteractorStyle.cpp interact/InteractionManager.cpp + ground/TileMath.cpp) target_include_directories(geopro_render PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) target_link_libraries(geopro_render PUBLIC geopro_core ${VTK_LIBRARIES} GDAL::GDAL) target_compile_features(geopro_render PUBLIC cxx_std_17) diff --git a/src/render/ground/TileMath.cpp b/src/render/ground/TileMath.cpp new file mode 100644 index 0000000..3b3b65d --- /dev/null +++ b/src/render/ground/TileMath.cpp @@ -0,0 +1,35 @@ +#include "ground/TileMath.hpp" + +#include + +namespace geopro::render { + +namespace { +constexpr double kPi = 3.14159265358979323846; +int clampInt(int v, int lo, int hi) { return v < lo ? lo : (v > hi ? hi : v); } +} // namespace + +TileXY lonLatToTile(double lonDeg, double latDeg, int z) { + if (z < 0) z = 0; + const double n = std::pow(2.0, z); + const double latR = latDeg * kPi / 180.0; + int x = static_cast(std::floor((lonDeg + 180.0) / 360.0 * n)); + int y = static_cast(std::floor((1.0 - std::asinh(std::tan(latR)) / kPi) / 2.0 * n)); + const int hi = static_cast(n) - 1; + return TileXY{z, clampInt(x, 0, hi), clampInt(y, 0, hi)}; +} + +LonLatBox tileBounds(int z, int x, int y) { + if (z < 0) z = 0; + const double n = std::pow(2.0, z); + const double west = x / n * 360.0 - 180.0; + const double east = (x + 1) / n * 360.0 - 180.0; + auto latAt = [&](double yy) { + return std::atan(std::sinh(kPi * (1.0 - 2.0 * yy / n))) * 180.0 / kPi; + }; + const double north = latAt(static_cast(y)); + const double south = latAt(static_cast(y + 1)); + return LonLatBox{west, south, east, north}; +} + +} // namespace geopro::render diff --git a/src/render/ground/TileMath.hpp b/src/render/ground/TileMath.hpp new file mode 100644 index 0000000..aba1be8 --- /dev/null +++ b/src/render/ground/TileMath.hpp @@ -0,0 +1,22 @@ +#pragma once + +// Web Mercator(EPSG:3857) 瓦片坐标数学:天地图/XYZ 底图瓦片定位用(纯函数,无 VTK/Qt 依赖)。 +// 标准 slippy-map 公式:n=2^z;x=(lon+180)/360*n;y 用墨卡托纬度映射。 +namespace geopro::render { + +struct TileXY { + int z = 0, x = 0, y = 0; +}; + +// 瓦片地理边界(度):west/east 经度,south/north 纬度。 +struct LonLatBox { + double west = 0, south = 0, east = 0, north = 0; +}; + +// 经纬度(度) → 指定 zoom 的瓦片行列(x/y 夹紧到 [0, 2^z-1])。 +TileXY lonLatToTile(double lonDeg, double latDeg, int z); + +// 瓦片 (z,x,y) → 其覆盖的地理边界(度)。 +LonLatBox tileBounds(int z, int x, int y); + +} // namespace geopro::render diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 4bc8839..7c5661d 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -100,6 +100,8 @@ target_sources(geopro_tests PRIVATE render/test_terrain.cpp) target_sources(geopro_tests PRIVATE render/test_camera_preset.cpp) # AxesActor(P2):buildAxes(bounds+unit/mode→vtkCubeAxesActor) 单位换算(英尺/经纬度)/不显示返回空。 target_sources(geopro_tests PRIVATE render/test_axes.cpp) +# TileMath(P5):天地图底图 Web Mercator 瓦片坐标数学(经纬↔z/x/y、瓦片地理边界)——纯函数。 +target_sources(geopro_tests PRIVATE render/test_tile_math.cpp) # SlicePlaneMath(P3):切面法向/滚轮平移+夹限/双击正视相机(含竖直兜底)/滚轮步长/最近切片——纯几何。 target_sources(geopro_tests PRIVATE render/test_slice_plane_math.cpp) target_link_libraries(geopro_tests PRIVATE geopro_render ${VTK_LIBRARIES}) diff --git a/tests/render/test_tile_math.cpp b/tests/render/test_tile_math.cpp new file mode 100644 index 0000000..5575f5e --- /dev/null +++ b/tests/render/test_tile_math.cpp @@ -0,0 +1,48 @@ +#include + +#include + +#include "ground/TileMath.hpp" + +using geopro::render::lonLatToTile; +using geopro::render::tileBounds; + +// z=1 把世界分 2x2:原点(0°,0°)在东/南象限交界 → 标准 slippy 取 (1,1)。 +TEST(TileMath, OriginZoom1) { + auto t = lonLatToTile(0.0, 0.0, 1); + EXPECT_EQ(t.z, 1); + EXPECT_EQ(t.x, 1); + EXPECT_EQ(t.y, 1); +} + +// z=1 西北瓦片 (0,0) 覆盖西半球北部:west=-180, east=0, north≈85.0511(墨卡托上限), south=0。 +TEST(TileMath, BoundsZoom1NW) { + auto b = tileBounds(1, 0, 0); + EXPECT_NEAR(b.west, -180.0, 1e-6); + EXPECT_NEAR(b.east, 0.0, 1e-6); + EXPECT_NEAR(b.north, 85.0511287798, 1e-4); + EXPECT_NEAR(b.south, 0.0, 1e-6); +} + +// 往返一致:任一经纬点所属瓦片的边界必须包含该点(经度严格、纬度含墨卡托方向)。 +TEST(TileMath, RoundTripContains) { + const double lon = 116.391, lat = 39.907; // 北京附近 + const int z = 12; + auto t = lonLatToTile(lon, lat, z); + EXPECT_EQ(t.z, z); + auto b = tileBounds(t.z, t.x, t.y); + EXPECT_GE(lon, b.west); + EXPECT_LE(lon, b.east); + EXPECT_LE(lat, b.north); // north 是瓦片上边界(纬度大) + EXPECT_GE(lat, b.south); +} + +// 夹紧:超界经纬不应产生越界瓦片索引。 +TEST(TileMath, ClampInRange) { + auto t = lonLatToTile(500.0, 95.0, 3); // 非法输入 + const int hi = (1 << 3) - 1; + EXPECT_GE(t.x, 0); + EXPECT_LE(t.x, hi); + EXPECT_GE(t.y, 0); + EXPECT_LE(t.y, hi); +}