feat(vtk): 底图瓦片坐标数学 TileMath(EPSG:3857 经纬↔z/x/y+瓦片边界)+单测(P5基石)

This commit is contained in:
gaozheng 2026-06-16 21:29:59 +08:00
parent f407c0adbc
commit 8684e52939
5 changed files with 109 additions and 1 deletions

View File

@ -2,7 +2,8 @@ find_package(VTK REQUIRED COMPONENTS CommonCore CommonDataModel FiltersGeometry
find_package(GDAL CONFIG REQUIRED) find_package(GDAL CONFIG REQUIRED)
add_library(geopro_render STATIC 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 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_include_directories(geopro_render PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(geopro_render PUBLIC geopro_core ${VTK_LIBRARIES} GDAL::GDAL) target_link_libraries(geopro_render PUBLIC geopro_core ${VTK_LIBRARIES} GDAL::GDAL)
target_compile_features(geopro_render PUBLIC cxx_std_17) target_compile_features(geopro_render PUBLIC cxx_std_17)

View File

@ -0,0 +1,35 @@
#include "ground/TileMath.hpp"
#include <cmath>
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<int>(std::floor((lonDeg + 180.0) / 360.0 * n));
int y = static_cast<int>(std::floor((1.0 - std::asinh(std::tan(latR)) / kPi) / 2.0 * n));
const int hi = static_cast<int>(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<double>(y));
const double south = latAt(static_cast<double>(y + 1));
return LonLatBox{west, south, east, north};
}
} // namespace geopro::render

View File

@ -0,0 +1,22 @@
#pragma once
// Web Mercator(EPSG:3857) 瓦片坐标数学:天地图/XYZ 底图瓦片定位用(纯函数,无 VTK/Qt 依赖)。
// 标准 slippy-map 公式n=2^zx=(lon+180)/360*ny 用墨卡托纬度映射。
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

View File

@ -100,6 +100,8 @@ target_sources(geopro_tests PRIVATE render/test_terrain.cpp)
target_sources(geopro_tests PRIVATE render/test_camera_preset.cpp) target_sources(geopro_tests PRIVATE render/test_camera_preset.cpp)
# AxesActor(P2)buildAxes(bounds+unit/mode→vtkCubeAxesActor) 单位换算(英尺/经纬度)/ # AxesActor(P2)buildAxes(bounds+unit/mode→vtkCubeAxesActor) 单位换算(英尺/经纬度)/
target_sources(geopro_tests PRIVATE render/test_axes.cpp) 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)/+/双击正视相机(含竖直兜底)// # SlicePlaneMath(P3)/+/双击正视相机(含竖直兜底)//
target_sources(geopro_tests PRIVATE render/test_slice_plane_math.cpp) target_sources(geopro_tests PRIVATE render/test_slice_plane_math.cpp)
target_link_libraries(geopro_tests PRIVATE geopro_render ${VTK_LIBRARIES}) target_link_libraries(geopro_tests PRIVATE geopro_render ${VTK_LIBRARIES})

View File

@ -0,0 +1,48 @@
#include <gtest/gtest.h>
#include <cmath>
#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);
}