diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index f1fb57d..6312581 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -3,6 +3,7 @@ find_package(PROJ CONFIG REQUIRED) add_library(geopro_core STATIC geo/LocalFrame.cpp + geo/GeoLocalFrame.cpp geo/CrsTransform.cpp model/ColorScale.cpp algo/IdwInterpolator.cpp diff --git a/src/core/geo/GeoLocalFrame.cpp b/src/core/geo/GeoLocalFrame.cpp new file mode 100644 index 0000000..18d3ec8 --- /dev/null +++ b/src/core/geo/GeoLocalFrame.cpp @@ -0,0 +1,24 @@ +#include "geo/GeoLocalFrame.hpp" + +#include + +namespace geopro::core { + +namespace { +constexpr double kPi = 3.14159265358979323846; +// 纬度每度约 110540 m(近似常数);经度每度随纬度收缩 111320*cos(lat)。 +constexpr double kMetersPerDegLat = 110540.0; +constexpr double kMetersPerDegLonEquator = 111320.0; +} // namespace + +GeoLocalFrame::GeoLocalFrame(double lat0, double lon0) + : lat0_(lat0), + lon0_(lon0), + mPerDegLon_(kMetersPerDegLonEquator * std::cos(lat0 * kPi / 180.0)), + mPerDegLat_(kMetersPerDegLat) {} + +LocalXY GeoLocalFrame::toLocal(double lat, double lon) const { + return LocalXY{(lon - lon0_) * mPerDegLon_, (lat - lat0_) * mPerDegLat_}; +} + +} // namespace geopro::core diff --git a/src/core/geo/GeoLocalFrame.hpp b/src/core/geo/GeoLocalFrame.hpp new file mode 100644 index 0000000..e401352 --- /dev/null +++ b/src/core/geo/GeoLocalFrame.hpp @@ -0,0 +1,16 @@ +#pragma once +namespace geopro::core { + +struct LocalXY { double x, y; }; + +// 等距圆柱(equirectangular)近似:把经纬度投到以(lat0,lon0)为原点的局部米平面。 +// 小范围测区足够;x=East、y=North(米)。 +class GeoLocalFrame { +public: + GeoLocalFrame(double lat0, double lon0); + LocalXY toLocal(double lat, double lon) const; // -> (x East m, y North m) +private: + double lat0_, lon0_, mPerDegLon_, mPerDegLat_; +}; + +} // namespace geopro::core diff --git a/src/render/CMakeLists.txt b/src/render/CMakeLists.txt index 4662654..d6e9813 100644 --- a/src/render/CMakeLists.txt +++ b/src/render/CMakeLists.txt @@ -1,6 +1,6 @@ -find_package(VTK REQUIRED COMPONENTS CommonCore CommonDataModel FiltersGeometry FiltersModeling RenderingOpenGL2 RenderingVolumeOpenGL2 InteractionStyle InteractionWidgets) +find_package(VTK REQUIRED COMPONENTS CommonCore CommonDataModel FiltersGeometry FiltersModeling RenderingCore RenderingOpenGL2 RenderingVolumeOpenGL2 InteractionStyle InteractionWidgets) add_library(geopro_render STATIC - Scene.cpp ColorLutBuilder.cpp CameraPreset.cpp actors/GridContourActor.cpp actors/VoxelActor.cpp) + Scene.cpp ColorLutBuilder.cpp CameraPreset.cpp actors/GridContourActor.cpp actors/VoxelActor.cpp actors/CurtainActor.cpp) target_include_directories(geopro_render PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) target_link_libraries(geopro_render PUBLIC geopro_core ${VTK_LIBRARIES}) target_compile_features(geopro_render PUBLIC cxx_std_17) diff --git a/src/render/actors/CurtainActor.cpp b/src/render/actors/CurtainActor.cpp new file mode 100644 index 0000000..7d818ac --- /dev/null +++ b/src/render/actors/CurtainActor.cpp @@ -0,0 +1,93 @@ +#include "actors/CurtainActor.hpp" + +#include +#include +#include +#include +#include +#include + +#include + +#include "ColorLutBuilder.hpp" + +namespace geopro::render { + +namespace { +// LUT 级数。 +constexpr int kLutLevels = 256; +} // namespace + +vtkSmartPointer buildCurtain(const geopro::core::Grid& g, + const geopro::core::ColorScale& cs, + const geopro::core::GeoLocalFrame& frame) +{ + const int nx = g.nx(), ny = g.ny(); + + // 退化网格:返回空 actor(调用方仍可安全 addActor,mapper 无输入则不绘制)。 + if (nx < 1 || ny < 1 || g.y.size() < static_cast(ny)) { + return vtkSmartPointer::New(); + } + + const bool hasLatLon = + g.lat.size() >= static_cast(nx) && g.lon.size() >= static_cast(nx); + + // 结构化网格:维度 (nx, ny, 1),点序 i 最快、j 次之(id = j*nx + i)。 + vtkNew sgrid; + sgrid->SetDimensions(nx, ny, 1); + + vtkNew points; + points->SetNumberOfPoints(static_cast(nx) * ny); + + vtkNew sc; + sc->SetName("v"); + sc->SetNumberOfTuples(static_cast(nx) * ny); + + for (int j = 0; j < ny; ++j) { + for (int i = 0; i < nx; ++i) { + double px, py; + if (hasLatLon) { + auto p = frame.toLocal(g.lat[i], g.lon[i]); + px = p.x; + py = p.y; + } else { + // 退化:用 g.x[i] 作 x、0 作 y。 + px = (g.x.size() > static_cast(i)) ? g.x[i] : static_cast(i); + py = 0.0; + } + const vtkIdType id = static_cast(j) * nx + i; + points->SetPoint(id, px, py, g.y[j]); // z=深度/高程,VTK Z 向上 + sc->SetValue(id, g.valueAt(i, j)); + } + } + + sgrid->SetPoints(points); + sgrid->GetPointData()->SetScalars(sc); + + // vmin/vmax 来自 Grid;若退化(==)则扫数据兜底。 + double vmin = g.vmin, vmax = g.vmax; + if (vmin >= vmax) { + const auto& vals = g.values(); + vmin = vals.empty() ? 0.0 : vals.front(); + vmax = vmin; + for (double v : vals) { + if (v < vmin) vmin = v; + if (v > vmax) vmax = v; + } + if (vmin >= vmax) vmax = vmin + 1.0; + } + + auto lut = buildLut(cs, vmin, vmax, kLutLevels); + + vtkNew mapper; + mapper->SetInputData(sgrid); + mapper->SetScalarModeToUsePointData(); + mapper->SetLookupTable(lut); + mapper->SetScalarRange(vmin, vmax); + + auto actor = vtkSmartPointer::New(); + actor->SetMapper(mapper); + return actor; +} + +} // namespace geopro::render diff --git a/src/render/actors/CurtainActor.hpp b/src/render/actors/CurtainActor.hpp new file mode 100644 index 0000000..3404245 --- /dev/null +++ b/src/render/actors/CurtainActor.hpp @@ -0,0 +1,14 @@ +#pragma once +#include +#include +#include "model/Field.hpp" +#include "model/ColorScale.hpp" +#include "geo/GeoLocalFrame.hpp" +namespace geopro::render { + +// 测线竖直帘面:沿 lat/lon 迹线立在世界(x,y),纵向为深度 g.y,面按 colorBar 着色。 +vtkSmartPointer buildCurtain(const geopro::core::Grid& g, + const geopro::core::ColorScale& cs, + const geopro::core::GeoLocalFrame& frame); + +} // namespace geopro::render diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index bd16b57..300b12c 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -31,6 +31,7 @@ target_sources(geopro_tests PRIVATE core/test_color_scale.cpp) target_sources(geopro_tests PRIVATE core/test_idw.cpp) target_sources(geopro_tests PRIVATE core/test_crs_transform.cpp) target_sources(geopro_tests PRIVATE core/test_model_data.cpp) +target_sources(geopro_tests PRIVATE core/test_geo_frame.cpp) target_link_libraries(geopro_tests PRIVATE geopro_core) target_sources(geopro_tests PRIVATE data/test_parsers.cpp) @@ -57,10 +58,12 @@ endif() # render 层:ColorLutBuilder(core ColorScale -> vtkLookupTable)。 # 需 vtkLookupTable(VTK::CommonCore);geopro_render 已 PUBLIC 传递其余 VTK 组件。 -find_package(VTK REQUIRED COMPONENTS CommonCore CommonDataModel) +find_package(VTK REQUIRED COMPONENTS CommonCore CommonDataModel RenderingCore) target_sources(geopro_tests PRIVATE render/test_color_lut.cpp) # dd_voxel:buildVoxel(ScalarVolume->vtkImageData->GPU 体绘制) 构建不崩 + dims 正确。 target_sources(geopro_tests PRIVATE render/test_voxel_build.cpp) +# Curtain:buildCurtain(Grid+GeoLocalFrame->vtkStructuredGrid 帘面) 非空 actor + 点数=nx*ny。 +target_sources(geopro_tests PRIVATE render/test_curtain.cpp) target_link_libraries(geopro_tests PRIVATE geopro_render ${VTK_LIBRARIES}) vtk_module_autoinit(TARGETS geopro_tests MODULES ${VTK_LIBRARIES}) diff --git a/tests/core/test_geo_frame.cpp b/tests/core/test_geo_frame.cpp new file mode 100644 index 0000000..75a263a --- /dev/null +++ b/tests/core/test_geo_frame.cpp @@ -0,0 +1,39 @@ +#include + +#include + +#include "geo/GeoLocalFrame.hpp" + +using geopro::core::GeoLocalFrame; + +namespace { +constexpr double kPi = 3.14159265358979323846; +} + +// 原点投影到 (0,0)。 +TEST(GeoFrame, OriginMapsToZero) { + GeoLocalFrame f(22.5, 114.16); + auto p = f.toLocal(22.5, 114.16); + EXPECT_NEAR(p.x, 0.0, 1e-9); + EXPECT_NEAR(p.y, 0.0, 1e-9); +} + +// 东向 0.01 度经度:x>0,约 0.01*111320*cos(22.5°)≈1028m(5% 容差)。 +TEST(GeoFrame, EastwardLongitudeGivesPositiveX) { + GeoLocalFrame f(22.5, 114.16); + auto p = f.toLocal(22.5, 114.17); + const double expected = 0.01 * 111320.0 * std::cos(22.5 * kPi / 180.0); + EXPECT_GT(p.x, 0.0); + EXPECT_NEAR(p.x, expected, expected * 0.05); + EXPECT_NEAR(p.y, 0.0, 1e-9); +} + +// 北向 0.01 度纬度:y≈0.01*110540≈1105m(5% 容差)。 +TEST(GeoFrame, NorthwardLatitudeGivesPositiveY) { + GeoLocalFrame f(22.5, 114.16); + auto p = f.toLocal(22.51, 114.16); + const double expected = 0.01 * 110540.0; + EXPECT_GT(p.y, 0.0); + EXPECT_NEAR(p.y, expected, expected * 0.05); + EXPECT_NEAR(p.x, 0.0, 1e-9); +} diff --git a/tests/render/test_curtain.cpp b/tests/render/test_curtain.cpp new file mode 100644 index 0000000..ddb3a18 --- /dev/null +++ b/tests/render/test_curtain.cpp @@ -0,0 +1,40 @@ +#include + +#include +#include +#include + +#include "actors/CurtainActor.hpp" +#include "geo/GeoLocalFrame.hpp" +#include "model/ColorScale.hpp" +#include "model/Field.hpp" + +using namespace geopro::core; + +// buildCurtain: 小 Grid(3,2) + lat/lon/y/values -> 非空 actor,mapper 输入点数=6。 +TEST(Curtain, BuildsStructuredGridFromSmallGrid) { + Grid g(3, 2); // nx=3 (lat/lon 列), ny=2 (深度行) + g.lat = {22.50, 22.50, 22.50}; + g.lon = {114.16, 114.17, 114.18}; + g.y = {0.0, -10.0}; // 两层深度 + double n = 0.0; + for (int j = 0; j < 2; ++j) + for (int i = 0; i < 3; ++i) g.valueAt(i, j) = n++; + g.vmin = 0.0; + g.vmax = 5.0; + + ColorScale cs; + cs.addStop(0.0, Rgba{0, 0, 255, 255}); + cs.addStop(5.0, Rgba{255, 0, 0, 255}); + + GeoLocalFrame frame(22.50, 114.16); + + auto actor = geopro::render::buildCurtain(g, cs, frame); + ASSERT_NE(actor.GetPointer(), nullptr); + + auto* mapper = vtkDataSetMapper::SafeDownCast(actor->GetMapper()); + ASSERT_NE(mapper, nullptr); + auto* input = mapper->GetInput(); + ASSERT_NE(input, nullptr); + EXPECT_EQ(input->GetNumberOfPoints(), 6); +}