feat(render): GeoLocalFrame(经纬→局部米) + CurtainActor(测线竖直帘面)

This commit is contained in:
gaozheng 2026-06-07 22:47:41 +08:00
parent f4e61cb947
commit c9d0d90433
9 changed files with 233 additions and 3 deletions

View File

@ -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

View File

@ -0,0 +1,24 @@
#include "geo/GeoLocalFrame.hpp"
#include <cmath>
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

View File

@ -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

View File

@ -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)

View File

@ -0,0 +1,93 @@
#include "actors/CurtainActor.hpp"
#include <vtkDataSetMapper.h>
#include <vtkDoubleArray.h>
#include <vtkNew.h>
#include <vtkPointData.h>
#include <vtkPoints.h>
#include <vtkStructuredGrid.h>
#include <cstddef>
#include "ColorLutBuilder.hpp"
namespace geopro::render {
namespace {
// LUT 级数。
constexpr int kLutLevels = 256;
} // namespace
vtkSmartPointer<vtkActor> 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调用方仍可安全 addActormapper 无输入则不绘制)。
if (nx < 1 || ny < 1 || g.y.size() < static_cast<size_t>(ny)) {
return vtkSmartPointer<vtkActor>::New();
}
const bool hasLatLon =
g.lat.size() >= static_cast<size_t>(nx) && g.lon.size() >= static_cast<size_t>(nx);
// 结构化网格:维度 (nx, ny, 1),点序 i 最快、j 次之id = j*nx + i
vtkNew<vtkStructuredGrid> sgrid;
sgrid->SetDimensions(nx, ny, 1);
vtkNew<vtkPoints> points;
points->SetNumberOfPoints(static_cast<vtkIdType>(nx) * ny);
vtkNew<vtkDoubleArray> sc;
sc->SetName("v");
sc->SetNumberOfTuples(static_cast<vtkIdType>(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<size_t>(i)) ? g.x[i] : static_cast<double>(i);
py = 0.0;
}
const vtkIdType id = static_cast<vtkIdType>(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<vtkDataSetMapper> mapper;
mapper->SetInputData(sgrid);
mapper->SetScalarModeToUsePointData();
mapper->SetLookupTable(lut);
mapper->SetScalarRange(vmin, vmax);
auto actor = vtkSmartPointer<vtkActor>::New();
actor->SetMapper(mapper);
return actor;
}
} // namespace geopro::render

View File

@ -0,0 +1,14 @@
#pragma once
#include <vtkSmartPointer.h>
#include <vtkActor.h>
#include "model/Field.hpp"
#include "model/ColorScale.hpp"
#include "geo/GeoLocalFrame.hpp"
namespace geopro::render {
// 测线竖直帘面:沿 lat/lon 迹线立在世界(x,y),纵向为深度 g.y面按 colorBar 着色。
vtkSmartPointer<vtkActor> buildCurtain(const geopro::core::Grid& g,
const geopro::core::ColorScale& cs,
const geopro::core::GeoLocalFrame& frame);
} // namespace geopro::render

View File

@ -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 ColorLutBuildercore ColorScale -> vtkLookupTable
# vtkLookupTableVTK::CommonCoregeopro_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_voxelbuildVoxel(ScalarVolume->vtkImageData->GPU 体绘制) + dims
target_sources(geopro_tests PRIVATE render/test_voxel_build.cpp)
# CurtainbuildCurtain(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})

View File

@ -0,0 +1,39 @@
#include <gtest/gtest.h>
#include <cmath>
#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°)≈1028m5% 容差)。
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≈1105m5% 容差)。
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);
}

View File

@ -0,0 +1,40 @@
#include <gtest/gtest.h>
#include <vtkDataSet.h>
#include <vtkDataSetMapper.h>
#include <vtkMapper.h>
#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 -> 非空 actormapper 输入点数=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);
}