feat/dataset-detail-chart #5

Merged
gaozheng merged 74 commits from feat/dataset-detail-chart into main 2026-06-13 17:30:37 +08:00
25 changed files with 1552 additions and 2 deletions
Showing only changes of commit b033dc2a2c - Show all commits

View File

@ -12,6 +12,10 @@ find_package(VTK REQUIRED COMPONENTS
)
find_package(nlohmann_json CONFIG REQUIRED)
find_package(Qt6 REQUIRED COMPONENTS Svg)
# trajectory QWebEngineView + Leaflet + TrajectoryMapView.{hpp,cpp} +
# resources/map/WebEngine Qt6WebChannel / Qt6Positioning find_package/link/
# deploy + main.cpp DetailViewFactory WebMap TrajectoryMapView
find_package(Qt6 REQUIRED COMPONENTS WebEngineWidgets WebEngineQuick)
add_executable(geopro_desktop WIN32
main.cpp
@ -31,7 +35,10 @@ add_executable(geopro_desktop WIN32
panels/chart/GridDataChartView.cpp
panels/chart/DataTableView.cpp
panels/chart/BarChartView.cpp
panels/chart/LineChartView.cpp
panels/chart/TrajectoryMapView.cpp
panels/chart/DetailViewFactory.cpp
resources/map/map.qrc
panels/chart/ChartTheme.cpp
panels/chart/ColorMapService.cpp
panels/chart/ColorBarWidget.cpp
@ -54,6 +61,7 @@ target_include_directories(geopro_desktop PRIVATE ${qtkeychain_SOURCE_DIR} ${qtk
target_link_libraries(geopro_desktop PRIVATE
Qt6::Core Qt6::Gui Qt6::Widgets Qt6::Svg
Qt6::WebEngineWidgets Qt6::WebEngineQuick
${VTK_LIBRARIES}
ads::qt6advanceddocking
qt6keychain
@ -94,4 +102,19 @@ if(WIN32)
"$<TARGET_FILE_DIR:Qt6::Core>/../plugins/${_pl}"
"$<TARGET_FILE_DIR:geopro_desktop>/${_pl}")
endforeach()
# Qt WebEngine
# WebEngine QtWebEngineProcess.exeChromium 宿 exe PATH
# resources/*.pak + icudtl.dat + v8 qtwebengine_locales/*.pakdev-build
# windeployqtWebEngine DLLQt6WebEngineCore/Widgets/QuickWebChannelPositioning
# QmlQuickQuickWidgets TARGET_RUNTIME_DLLS
set(_qt_bin "$<TARGET_FILE_DIR:Qt6::Core>")
add_custom_command(TARGET geopro_desktop POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different
"${_qt_bin}/QtWebEngineProcess.exe" "$<TARGET_FILE_DIR:geopro_desktop>"
COMMAND ${CMAKE_COMMAND} -E copy_directory
"${_qt_bin}/../resources" "$<TARGET_FILE_DIR:geopro_desktop>/resources"
COMMAND ${CMAKE_COMMAND} -E copy_directory
"${_qt_bin}/../translations/qtwebengine_locales"
"$<TARGET_FILE_DIR:geopro_desktop>/qtwebengine_locales")
endif()

View File

@ -61,6 +61,8 @@
#include <QVBoxLayout>
#include <QWidget>
#include <QtWebEngineQuick/QtWebEngineQuick>
#include <DockAreaTitleBar.h>
#include <DockAreaWidget.h>
#include <DockManager.h>
@ -87,6 +89,7 @@
#include "panels/chart/ErtInversionStrategy.hpp"
#include "panels/chart/MeasurementStrategy.hpp"
#include "panels/chart/GrMeasurementStrategy.hpp"
#include "panels/chart/TrajectoryStrategy.hpp"
#include "api/ApiProjectRepository.hpp"
#include "api/ApiDatasetRepository.hpp"
#include "panels/ObjectTreePanel.hpp"
@ -836,6 +839,12 @@ public:
int main(int argc, char* argv[])
{
// Qt WebEngine地图页签的 QWebEngineView必须在 QApplication 构造前初始化,
// 且需启用跨上下文共享 OpenGLQtWebEngine 与 QVTK 同进程共用 GL context避免黑屏/崩溃)。
// AA_ShareOpenGLContexts 须在 QApplication 之前设置QtWebEngineQuick::initialize() 同样须前置。
QCoreApplication::setAttribute(Qt::AA_ShareOpenGLContexts);
QtWebEngineQuick::initialize();
// 高 DPI 缩放采用直通策略:在 125%/150% 等分数缩放下字体/图标按真实比例渲染,更清晰。
// 必须在 QApplication 构造前设置。
QApplication::setHighDpiScaleFactorRoundingPolicy(
@ -914,6 +923,7 @@ int main(int argc, char* argv[])
chartRegistry.add(std::make_unique<geopro::app::ErtInversionStrategy>());
chartRegistry.add(std::make_unique<geopro::app::MeasurementStrategy>());
chartRegistry.add(std::make_unique<geopro::app::GrMeasurementStrategy>());
chartRegistry.add(std::make_unique<geopro::app::TrajectoryStrategy>());
geopro::controller::DatasetDetailController detailCtrl(datasetRepo, chartRegistry);
// ── 外壳:标准 QMainWindow原生标题栏。buildWorkbench 直接用其

View File

@ -5,7 +5,9 @@
#include "panels/chart/BarChartView.hpp"
#include "panels/chart/DataTableView.hpp"
#include "panels/chart/GridDataChartView.hpp"
#include "panels/chart/LineChartView.hpp"
#include "panels/chart/RawDataChartView.hpp"
#include "panels/chart/TrajectoryMapView.hpp"
namespace geopro::app {
@ -20,8 +22,12 @@ std::unique_ptr<IDetailView> makeDetailView(controller::ViewKind kind, QWidget*
case controller::ViewKind::Bar:
return std::unique_ptr<IDetailView>(new BarChartView(parent));
case controller::ViewKind::LineProfile:
return std::unique_ptr<IDetailView>(new LineChartView(parent));
case controller::ViewKind::WebMap:
// trajectory 地图QWebEngineView + Leaflet + 天地图瓦片,电极经纬点画橙色空心圈并取景。
return std::unique_ptr<IDetailView>(new TrajectoryMapView(parent));
case controller::ViewKind::PolylineMap:
// 后续阶段补LineProfile,PolylineMap(trajectory)。
// 后续阶段补:PolylineMap。
throw std::runtime_error("makeDetailView: ViewKind not yet implemented");
}
throw std::runtime_error("makeDetailView: unknown ViewKind");

View File

@ -0,0 +1,380 @@
#include "panels/chart/LineChartView.hpp"
#include <QBrush>
#include <QEvent>
#include <QGuiApplication>
#include <QHBoxLayout>
#include <QLabel>
#include <QMouseEvent>
#include <QPainter>
#include <QPalette>
#include <QPen>
#include <QScreen>
#include <QVBoxLayout>
#include <qwt_plot.h>
#include <qwt_plot_canvas.h>
#include <qwt_plot_curve.h>
#include <qwt_plot_grid.h>
#include <qwt_scale_draw.h>
#include <qwt_scale_map.h>
#include <qwt_spline_local.h>
#include <qwt_spline_parametrization.h>
#include <qwt_text.h>
#include <qwt_widget_overlay.h>
#include <algorithm>
#include <cmath>
#include <limits>
#include "Theme.hpp"
#include "panels/chart/ChartTheme.hpp"
namespace geopro::app {
namespace {
// 类目轴刻度把整数刻度位0,1,2,…)映射为类目标签 "#1","#2",…(来自 categories
// 非整数/越界刻度返回空标签(避免次刻度污染)。与 BarChartView::CategoryScaleDraw 同款。
class CategoryScaleDraw : public QwtScaleDraw {
public:
explicit CategoryScaleDraw(std::vector<QString> labels) : labels_(std::move(labels)) {
enableComponent(QwtScaleDraw::Backbone, true);
enableComponent(QwtScaleDraw::Ticks, true);
}
QwtText label(double v) const override {
const double r = std::round(v);
if (std::abs(v - r) > 1e-6) return QwtText(); // 仅整数刻度出标签
const int i = static_cast<int>(r);
if (i < 0 || i >= static_cast<int>(labels_.size())) return QwtText();
return labels_[static_cast<size_t>(i)];
}
private:
std::vector<QString> labels_;
};
QColor lineColor(const QString& hex) {
QColor c(hex);
return c.isValid() ? c : QColor(0x54, 0x70, 0xc6); // 回退 ECharts 蓝
}
} // namespace
LineChartView::LineChartView(QWidget* parent) : QWidget(parent) {
auto* lay = new QVBoxLayout(this);
lay->setContentsMargins(0, 0, 0, 0);
lay->setSpacing(0);
// 左上水平 y 轴标题ECharts 风格,对齐 BarChartView
yTitle_ = new QLabel(this);
auto* titleRow = new QWidget(this);
auto* titleLay = new QHBoxLayout(titleRow);
titleLay->setContentsMargins(48, 6, 8, 0); // 左缩进对齐 y 轴上方
titleLay->setSpacing(0);
titleLay->addWidget(yTitle_);
titleLay->addStretch();
lay->addWidget(titleRow);
plot_ = new QwtPlot(this);
plot_->setObjectName(QStringLiteral("trajLinePlotArea"));
plot_->enableAxis(QwtPlot::xBottom, true);
plot_->enableAxis(QwtPlot::yLeft, true);
// x 轴标题「电极号」底部居中setData 设文本)。
plot_->setAxisTitle(QwtPlot::xBottom, QwtText());
// 仅横向y网格弱化与原版 ECharts 一致:仅水平刻度线)。
auto* grid = new QwtPlotGrid();
grid->enableX(false);
grid->enableY(true);
grid->enableXMin(false);
grid->enableYMin(false);
grid->setMajorPen(QColor(225, 225, 225), 1.0, Qt::SolidLine);
grid->attach(plot_);
plot_->setMinimumSize(0, 0);
lay->addWidget(plot_, 1);
// 鼠标 hover 追踪:虚线参考线 + 实心点 + 浮动框data_ 地址稳定setData 只改其内容)。
hover_ = new LineHoverTip(plot_, &data_, this);
// 主题:底色/轴字/网格按当前主题套一次 + 热切换。
applyChartPlotTheme(plot_);
QObject::connect(&ThemeManager::instance(), &ThemeManager::changed, plot_, [this]() {
applyChartPlotTheme(plot_);
QPalette pal = yTitle_->palette();
pal.setColor(QPalette::WindowText,
isDarkTheme() ? tokenColor("text/secondary") : QColor(90, 90, 90));
yTitle_->setPalette(pal);
});
}
LineChartView::~LineChartView() {
// 卸载并删除已挂折线,先于 QwtPlot autoDelete 触发(与 BarChartView 析构对称,避免双删)。
clearCurve();
}
void LineChartView::clearCurve() {
if (curve_) {
curve_->detach();
delete curve_;
curve_ = nullptr;
}
}
void LineChartView::setPayload(const QVariant& payload) {
if (!payload.canConvert<geopro::core::LinePayload>()) return; // 坏/空 → 空态
setData(payload.value<geopro::core::LinePayload>());
}
void LineChartView::setData(const geopro::core::LinePayload& p) {
data_ = p;
clearCurve();
const int n = static_cast<int>(p.categories.size());
// y 轴标题(左上水平 QLabel
yTitle_->setText(p.yTitle);
QPalette tpal = yTitle_->palette();
tpal.setColor(QPalette::WindowText,
isDarkTheme() ? tokenColor("text/secondary") : QColor(90, 90, 90));
yTitle_->setPalette(tpal);
// 锁定两轴为固定刻度(关闭自动缩放),使 hover 时挂/卸标记的 replot 不会重新拟合 →
// 曲线不再随鼠标进入而上下漂移FIX 3
plot_->setAxisAutoScale(QwtPlot::xBottom, false);
plot_->setAxisAutoScale(QwtPlot::yLeft, false);
// x 轴标题「电极号」+ 类目刻度 "#1".."#40"。
plot_->setAxisTitle(QwtPlot::xBottom, p.xTitle);
plot_->setAxisScaleDraw(QwtPlot::xBottom, new CategoryScaleDraw(p.categories));
plot_->setAxisScale(QwtPlot::xBottom, 0.0, n > 0 ? n - 1 : 0.0, 1.0); // 每类目一刻度
plot_->setAxisMaxMinor(QwtPlot::xBottom, 0);
// y 范围固定 0..max*1.1(从数据取上界,留 10% 余量;对齐原版 030 观感)。
// 固定刻度后 hover 标记不再触发 y 轴重算。
double yMax = 0.0;
for (double v : p.y) yMax = std::max(yMax, v);
plot_->setAxisScale(QwtPlot::yLeft, 0.0, yMax > 0 ? yMax * 1.1 : 1.0);
if (p.y.empty()) {
plot_->replot();
return;
}
// 单条折线:电极号(0..n-1) → 高程。
//
// 平滑必须在【数据坐标】里一次性算好,绝不能用 QwtPlotCurve::Fitted。原因本 BUG 根因):
// QwtPlotCurve 的 Fitted 拟合器在【绘制时】对【已映射到画布像素】的点跑样条
// (见 qwt_plot_curve.cpp 注释 “The curve fitter operates on the translated points
// ( = widget coordinates)”)。于是拟合结果取决于当时画布的像素几何;当首次 hover 触发
// replot 时画布几何与首帧哪怕有一丁点差异,样条就在不同像素网格上重采样 → 曲线竖直方向
// 细微漂移。锁轴并不能消除这一点(漂移源是像素级重拟合,不是自动缩放)。
// 解法QWT 官方建议:把拟合结果缓存进 series在数据坐标里把样条展开成稠密折线
// 作为静态 samples 交给普通 Lines 曲线。此后每次 replot 都映射同一条固定折线 → 首帧与
// hover 帧像素完全一致零漂移。Cardinal + ParameterUniform 与原 QwtSplineCurveFitter
// 内部一致,平滑观感不变。
curve_ = new QwtPlotCurve(p.seriesName);
QVector<QPointF> nodes;
nodes.reserve(n);
for (int i = 0; i < n && i < static_cast<int>(p.y.size()); ++i)
nodes.append(QPointF(i, p.y[static_cast<size_t>(i)]));
QVector<QPointF> samples = nodes;
if (p.smooth && nodes.size() > 2) {
QwtSplineLocal spline(QwtSplineLocal::Cardinal);
spline.setParametrization(QwtSplineParametrization::ParameterUniform);
// 展平容差取数据范围的极小比例 → 折线足够稠密、肉眼平滑;数据坐标空间,缩放无关。
const double xSpan = nodes.isEmpty() ? 1.0 : (nodes.last().x() - nodes.first().x());
const double tolerance = std::max(std::abs(xSpan), 1.0) / 2000.0;
const QPolygonF smooth = spline.polygon(QPolygonF(nodes), tolerance);
if (smooth.size() > 1) samples = QVector<QPointF>(smooth.begin(), smooth.end());
}
curve_->setSamples(samples);
curve_->setStyle(QwtPlotCurve::Lines);
curve_->setPen(QPen(lineColor(p.color), 2));
curve_->setRenderHint(QwtPlotItem::RenderAntialiased, true);
curve_->attach(plot_);
plot_->replot();
}
// ── LineHoverOverlay / LineHoverTip折线 hover 虚线参考线 + 实心点 + 浮动框 ──────
namespace {
// 从类目标签 "#N" 解析电极号;无前缀/非数字 → 回退 1-based 索引。
int electrodeNoFromCategory(const std::vector<QString>& cats, int i) {
if (i >= 0 && i < static_cast<int>(cats.size())) {
QString s = cats[static_cast<size_t>(i)];
if (s.startsWith(QLatin1Char('#'))) s = s.mid(1);
bool ok = false;
const int no = s.toInt(&ok);
if (ok) return no;
}
return i + 1;
}
} // namespace
// hover 叠层:画在 plot canvas 之上的 QwtWidgetOverlay。激活时用 plot 的 canvasMap
// 把数据坐标 (idx, yVal) 变换为像素,画一条垂直虚线参考线(满画布高)+ 曲线上实心点。
// updateOverlay() 只重绘本叠层、【不】触碰 plot/曲线 → 曲线零漂移。颜色每帧按主题重算(廉价)。
class LineHoverOverlay : public QwtWidgetOverlay {
public:
explicit LineHoverOverlay(QwtPlot* plot)
: QwtWidgetOverlay(plot->canvas()), plot_(plot) {
// 不用 mask默认 MaskHint 在未重写 maskHint 时会把可见区域裁空 → 叠层不显示)。
setMaskMode(QwtWidgetOverlay::NoMask);
}
// 设置 hover 状态并重绘叠层(仅叠层,不 replot plot
void setHover(bool active, int idx, double yVal, const QColor& dotColor) {
active_ = active;
idx_ = idx;
yVal_ = yVal;
dotColor_ = dotColor;
updateOverlay();
}
void clearHover() {
if (!active_) return;
active_ = false;
updateOverlay();
}
protected:
void drawOverlay(QPainter* painter) const override {
if (!active_ || !plot_) return;
const QwtScaleMap xMap = plot_->canvasMap(QwtPlot::xBottom);
const QwtScaleMap yMap = plot_->canvasMap(QwtPlot::yLeft);
const double xPix = xMap.transform(idx_);
const double yPix = yMap.transform(yVal_);
// 垂直虚线参考线:满画布高(主题感知颜色)。
const QColor lineCol =
isDarkTheme() ? tokenColor("border/strong") : QColor(150, 170, 210);
painter->setRenderHint(QPainter::Antialiasing, true);
painter->setPen(QPen(lineCol, 1.0, Qt::DashLine));
painter->drawLine(QPointF(xPix, 0.0), QPointF(xPix, static_cast<double>(height())));
// 曲线上实心圆点(系列色填充 + 同色描边,直径 ~8px
QColor dot = dotColor_.isValid() ? dotColor_ : QColor(0x54, 0x70, 0xc6);
painter->setPen(QPen(dot, 1.0));
painter->setBrush(QBrush(dot));
painter->drawEllipse(QPointF(xPix, yPix), 4.0, 4.0);
}
private:
QwtPlot* plot_;
bool active_ = false;
int idx_ = 0;
double yVal_ = 0.0;
QColor dotColor_;
};
LineHoverTip::LineHoverTip(QwtPlot* plot, const geopro::core::LinePayload* data, QObject* parent)
: QObject(parent), plot_(plot), data_(data) {
if (plot_ && plot_->canvas()) {
// hover无按键需开启鼠标跟踪否则默认仅按键按下时才收到 MouseMove。
plot_->canvas()->setMouseTracking(true);
plot_->canvas()->installEventFilter(this);
// 叠层 parent 为 canvas → 随 canvas 尺寸/位置自动跟随QwtWidgetOverlay 处理 resize
overlay_ = new LineHoverOverlay(plot_);
overlay_->show();
}
}
LineHoverTip::~LineHoverTip() {
// tip_ 是无父顶层 widget须显式删除。overlay_ parent 为 canvas随其析构无需手删
delete tip_;
tip_ = nullptr;
}
void LineHoverTip::hideHover() {
// 叠层置非激活并重绘(仅叠层,不 replot plot隐藏浮动框。
if (overlay_) overlay_->clearHover();
if (tip_) tip_->hide();
}
void LineHoverTip::showTip(const QString& html, const QPoint& globalPos) {
// 自定义浮动框Qt::ToolTip 顶层 QLabel富文本。每次 MouseMove 必更新文本与位置,
// 不经 QToolTip 去重逻辑 → 提示稳定显示FIX 4。主题感知配色白框/暗框)。
if (!tip_) {
tip_ = new QLabel(nullptr, Qt::ToolTip | Qt::FramelessWindowHint);
tip_->setObjectName(QStringLiteral("lineHoverTip"));
tip_->setTextFormat(Qt::RichText);
tip_->setMargin(8);
tip_->setAttribute(Qt::WA_ShowWithoutActivating, true);
}
const bool dark = isDarkTheme();
const QColor bg = dark ? tokenColor("bg/panel") : QColor(0xFF, 0xFF, 0xFF);
const QColor border = dark ? tokenColor("border/strong") : QColor(0xE3, 0xE6, 0xEB);
const QColor fg = dark ? tokenColor("text/primary") : QColor(0x27, 0x2C, 0x35);
tip_->setStyleSheet(QStringLiteral(
"QLabel#lineHoverTip{background:%1;color:%2;border:1px solid %3;"
"border-radius:4px;}")
.arg(bg.name(), fg.name(), border.name()));
tip_->setText(html);
tip_->adjustSize();
// 在光标右下偏移摆放;越右/下边界则翻向左/上,避免被屏幕裁切。
QPoint pos = globalPos + QPoint(14, 16);
if (QScreen* scr = QGuiApplication::screenAt(globalPos)) {
const QRect g = scr->availableGeometry();
if (pos.x() + tip_->width() > g.right()) pos.setX(globalPos.x() - tip_->width() - 14);
if (pos.y() + tip_->height() > g.bottom()) pos.setY(globalPos.y() - tip_->height() - 16);
}
tip_->move(pos);
if (!tip_->isVisible()) tip_->show();
}
bool LineHoverTip::eventFilter(QObject* obj, QEvent* ev) {
if (!plot_ || obj != plot_->canvas()) return QObject::eventFilter(obj, ev);
if (ev->type() == QEvent::Leave) {
hideHover();
return false;
}
if (ev->type() != QEvent::MouseMove) return QObject::eventFilter(obj, ev);
auto* me = static_cast<QMouseEvent*>(ev);
// 拖动中(有按键)或无数据 → 不弹提示。
if (me->buttons() != Qt::NoButton || !data_ || data_->y.empty()) {
hideHover();
return false;
}
const auto& ys = data_->y;
const int n = static_cast<int>(ys.size());
// 按 x 吸附到最近电极索引(类目位于整数 x = 0..n-1
const QwtScaleMap xMap = plot_->canvasMap(QwtPlot::xBottom);
const double xVal = xMap.invTransform(me->position().x());
int idx = static_cast<int>(std::lround(xVal));
idx = std::clamp(idx, 0, n - 1);
const double yVal = ys[static_cast<size_t>(idx)];
const int electrodeNo = electrodeNoFromCategory(data_->categories, idx);
// 实心点系列色(虚线参考线颜色在 overlay 内按主题重算)。
QColor dotCol(data_->color);
if (!dotCol.isValid()) dotCol = QColor(0x54, 0x70, 0xc6);
// 关键:把虚线 + 实心点交给叠层重绘仅叠层重绘【绝不】replot plot → 曲线零漂移)。
if (overlay_) overlay_->setHover(true, idx, yVal, dotCol);
// 浮动框:#<电极号> 表头 + 系列点·标签·值(高程保留 3 位小数)。系列点用系列色圆点。
const QString tip = QStringLiteral("#%1<br><span style='color:%2'>&#9679;</span> 高程(m)"
"&nbsp;&nbsp;%3")
.arg(electrodeNo)
.arg(dotCol.name())
.arg(yVal, 0, 'f', 3);
showTip(tip, me->globalPosition().toPoint());
return false; // 不消费,保留其它过滤器链路
}
} // namespace geopro::app

View File

@ -0,0 +1,78 @@
#pragma once
#include <QObject>
#include <QWidget>
#include "model/detail/DetailPayloads.hpp"
#include "panels/chart/IDetailView.hpp"
class QEvent;
class QLabel;
class QPoint;
class QwtPlot;
class QwtPlotCurve;
namespace geopro::app {
class LineHoverTip;
class LineHoverOverlay;
// 折线图视图dd_trajectory_data 高程页签):
// QwtPlot + QwtPlotCurve单条平滑折线Fitted + QwtSplineCurveFitter
// x 轴类目标签 "#1".."#40"(自定义 QwtScaleDraw复用 BarChartView 同款思路);
// x 轴标题「电极号」底部居中y 轴标题「高程」用左上水平 QLabelECharts 风格);
// 线色 #5470c6ECharts 默认蓝,数据色,两主题一致);无图例(单系列)。
// 背景/轴字/网格随主题ChartTheme / ThemeManager
class LineChartView : public QWidget, public IDetailView {
Q_OBJECT
public:
explicit LineChartView(QWidget* parent = nullptr);
~LineChartView() override;
void setData(const geopro::core::LinePayload& p);
QWidget* widget() override { return this; }
void setPayload(const QVariant& payload) override; // 坏/空 variant → 空态不崩
private:
void clearCurve(); // 卸载并删除已挂折线(避免 QwtPlot autoDelete 双删)
geopro::core::LinePayload data_;
QwtPlot* plot_;
QLabel* yTitle_; // 左上水平 y 轴标题ECharts 风格)
QwtPlotCurve* curve_ = nullptr; // 当前挂载的折线(已 attach卸载时 detach+delete
LineHoverTip* hover_ = nullptr; // 鼠标 hover 追踪器(虚线参考线 + 实心点 + 浮动框)
};
// 折线 hover 追踪器(对齐原版 ECharts 高程折线 hover监听画布鼠标移动无按键时
// 按 x 吸附到最近电极索引,绘制:垂直虚线参考线 + 曲线上该点实心圆点 +
// 自定义浮动框(#<电极号> / ● 高程(m): <值,3 位>)。
//
// 关键DEFINITIVE FIX虚线参考线 + 实心点用 QwtWidgetOverlay 画在 canvas 之上,
// hover 路径【绝不】调用 plot_->replot()。此前用 QwtPlotMarker + 每次 MouseMove replot
// 那次 replot 重跑 plot 布局/updateAxes使曲线随 hover 标记的挂/卸而上下漂移(锁轴 + 数据
// 坐标样条都无法阻止——漂移源是 replot/marker-attach 本身)。改为 overlay 后曲线在 setData
// 里只渲染一次、hover 全程不再 replot → 曲线不可能移动。
// 浮动框仍用独立的 Qt::ToolTip 顶层 QLabel富文本QToolTip 会去重抑制相同/相近位置的重复
// showTextFIX 4自定义 widget 每次 MouseMove 必更新位置/内容。
// 鼠标离开/越界时隐藏。不消费事件(镜像 ScatterHoverTip
class LineHoverTip : public QObject {
Q_OBJECT
public:
LineHoverTip(QwtPlot* plot, const geopro::core::LinePayload* data, QObject* parent = nullptr);
~LineHoverTip() override;
protected:
bool eventFilter(QObject* obj, QEvent* ev) override;
private:
void hideHover(); // overlay 置非激活 + 隐藏浮动框(无 replot
// 在 globalPos 旁显示富文本浮动框(按需创建、主题感知样式;每次必更新)。
void showTip(const QString& html, const QPoint& globalPos);
QwtPlot* plot_;
const geopro::core::LinePayload* data_; // 由 LineChartView 持有data_ 成员,地址稳定);只读不拥有
LineHoverOverlay* overlay_ = nullptr; // 画布之上的 hover 叠层(虚线 + 实心点;不触发 plot replot
QLabel* tip_ = nullptr; // 自定义浮动框Qt::ToolTip 顶层 QLabel富文本替代 QToolTip
};
} // namespace geopro::app

View File

@ -0,0 +1,58 @@
#include "panels/chart/TrajectoryMapView.hpp"
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QUrl>
#include <QVBoxLayout>
#include <QWebEngineView>
namespace geopro::app {
namespace {
// MapPayload → 紧凑 JSON 数组串 "[{\"no\":1,\"lon\":..,\"lat\":..}, ...]"(供页面 setPoints
QString pointsToJson(const geopro::core::MapPayload& m) {
QJsonArray arr;
for (const auto& p : m.points) {
QJsonObject o;
o.insert(QStringLiteral("no"), p.electrodeNo);
o.insert(QStringLiteral("lon"), p.lon);
o.insert(QStringLiteral("lat"), p.lat);
arr.append(o);
}
return QString::fromUtf8(QJsonDocument(arr).toJson(QJsonDocument::Compact));
}
} // namespace
TrajectoryMapView::TrajectoryMapView(QWidget* parent) : QWidget(parent) {
auto* lay = new QVBoxLayout(this);
lay->setContentsMargins(0, 0, 0, 0);
lay->setSpacing(0);
view_ = new QWebEngineView(this);
lay->addWidget(view_, 1);
// 页面加载完成 → 标记就绪并注入待推送的点(载荷可能先到)。
QObject::connect(view_, &QWebEngineView::loadFinished, this, [this](bool ok) {
pageLoaded_ = ok;
if (ok) injectPoints();
});
view_->load(QUrl(QStringLiteral("qrc:/map/trajectory_map.html")));
}
void TrajectoryMapView::setPayload(const QVariant& payload) {
if (!payload.canConvert<geopro::core::MapPayload>()) return; // 坏/空 → 忽略
pendingJson_ = pointsToJson(payload.value<geopro::core::MapPayload>());
if (pageLoaded_) injectPoints(); // 页面已就绪则立即注入,否则待 loadFinished
}
void TrajectoryMapView::injectPoints() {
if (!pageLoaded_ || pendingJson_.isEmpty()) return;
// 无 QWebChannel单向经 runJavaScript 调页面侧 setPoints(arr)。
view_->page()->runJavaScript(QStringLiteral("setPoints(%1)").arg(pendingJson_));
}
} // namespace geopro::app

View File

@ -0,0 +1,35 @@
#pragma once
#include <QString>
#include <QWidget>
#include "model/detail/DetailPayloads.hpp"
#include "panels/chart/IDetailView.hpp"
class QWebEngineView;
namespace geopro::app {
// 轨迹「地图」页签视图dd_trajectory_data 地图页签):
// QWebEngineView 加载本地 MapLibre GL 地图页qrc:/map/trajectory_map.htmlWebGL 渲染,
// 底图用天地图 WMTS 栅格瓦片 + 注记瓦片,含街道/卫星/地形三套底图切换(仅瓦片联网,
// MapLibre 库本地内置于 qrc
// setPayload 解包 MapPayload电极经纬点构造 JSON 数组后经 page()->runJavaScript()
// 调页面侧 setPoints(arr) 画实心橙点 + 连线 + 起点标记 + 取景;无 QWebChannel未安装单向推数据。
// 载荷可能早于页面 loadFinished 到达 → 暂存,待 loadFinished 后再注入(幂等)。
class TrajectoryMapView : public QWidget, public IDetailView {
Q_OBJECT
public:
explicit TrajectoryMapView(QWidget* parent = nullptr);
QWidget* widget() override { return this; }
void setPayload(const QVariant& payload) override; // 坏/空 variant → 忽略
private:
void injectPoints(); // 把 pendingJson_ 推入页面(仅页面就绪时)
QWebEngineView* view_ = nullptr;
QString pendingJson_; // 最近一次载荷的 JS 数组串("[]" 表示无点)
bool pageLoaded_ = false;
};
} // namespace geopro::app

View File

@ -0,0 +1,25 @@
#pragma once
#include <vector>
#include "IDatasetChartStrategy.hpp" // geopro::controller
namespace geopro::app {
// ERT 电极坐标轨迹dd_trajectory_data策略地图 / 列表 / 高程 三页签(页签序对齐原版)。
// 地图(默认/首位)= WebMapQWebEngineView + Leaflet + 天地图瓦片traj.map 产 MapPayload
// 端点 trajectory/lineelectrodeCoordinate → 经纬点)。
// 列表 = Table懒加载gridHeaderDisplay+rowList → TablePayload
// 高程 = LineProfile平滑折线electrodeNo→elevation
// 列表与高程同端点 trajectory/rowsloaderKey 不同traj.rows 产 TablePayload / traj.elev 产 LinePayload
struct TrajectoryStrategy : controller::IDatasetChartStrategy {
std::string ddCode() const override { return "dd_trajectory_data"; }
std::vector<controller::TabSpec> tabs() const override {
return {
{QStringLiteral("地图"), controller::ViewKind::WebMap,
QStringLiteral("traj.map"), /*lazy*/ false, /*paginated*/ false},
{QStringLiteral("列表"), controller::ViewKind::Table,
QStringLiteral("traj.rows"), /*lazy*/ true, /*paginated*/ false},
{QStringLiteral("高程"), controller::ViewKind::LineProfile,
QStringLiteral("traj.elev"), /*lazy*/ false, /*paginated*/ false},
};
}
};
} // namespace geopro::app

View File

@ -0,0 +1,8 @@
<!DOCTYPE RCC>
<RCC version="1.0">
<qresource prefix="/map">
<file>trajectory_map.html</file>
<file>maplibre-gl.js</file>
<file>maplibre-gl.css</file>
</qresource>
</RCC>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,363 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<title>电极轨迹地图</title>
<link rel="stylesheet" href="maplibre-gl.css" />
<style>
html, body { margin: 0; padding: 0; height: 100%; }
#map { position: absolute; top: 0; left: 0; right: 0; bottom: 0; background: #f0eee0; }
/* 底图切换控件:右上角小圆角按钮组(街道 / 卫星)。主题中性(纯 Web 控件)。 */
.basemap-switcher {
display: flex;
background: #ffffff;
border-radius: 6px;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.3);
overflow: hidden;
font: 13px/1 "Microsoft YaHei", Arial, sans-serif;
}
.basemap-switcher button {
border: 0;
background: transparent;
color: #333;
padding: 6px 12px;
cursor: pointer;
outline: none;
border-right: 1px solid #e0e0e0;
transition: background 120ms ease;
}
.basemap-switcher button:last-child { border-right: 0; }
.basemap-switcher button:hover { background: #f2f2f2; }
.basemap-switcher button.active {
background: #ff8c00;
color: #ffffff;
}
/* 定位/回到测线控件:白色小圆角方块 + 十字准星图标(对齐主流地图「回到当前位置」按钮)。 */
.locate-ctrl {
background: #ffffff;
border-radius: 4px;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.3);
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: background 120ms ease;
}
.locate-ctrl:hover { background: #f2f2f2; }
.locate-ctrl svg { display: block; }
</style>
</head>
<body>
<div id="map"></div>
<script src="maplibre-gl.js"></script>
<script>
// 天地图 WMTS 令牌(实证:与原版一致)。
var TK = 'aca91d8c9f59a4f779f39061b8a07737';
// 子域 t0-t7MapLibre raster source 用 tiles 数组列全所有子域主机实现负载分担)。
var SUBS = ['0', '1', '2', '3', '4', '5', '6', '7'];
// 电极轨迹橙(线 + 点 + 起点标记共用一个常量;对齐先前修复后的实心橙观感)。
var TRAJ_ORANGE = '#ff8c00';
// 天地图各图层的原生最细瓦片层vec/cva街道、img/cia卫星原生到 z18。
// 把每个 source 的 maxzoom 设为其真实原生上限后MapLibre 在该层级之上【对已有最细瓦片
// 超采样放大over-zoom upscale】而不会去请求不存在的更细瓦片否则 404 → 无数据 →
// 整图退化成纯蓝灰)。地图 maxZoom=22 仍是人工超放大上限。
// 注地形ter/cta底图已移除——其原生只到 z14缩放太浅不可用。
var TILE_MAXZOOM = 18; // 街道/卫星原生最细
var MAP_MAXZOOM = 22;
var MAP_MINZOOM = 1;
// 构造某一天地图图层的 WMTS XYZ 瓦片 URL 模板数组(列全 t0-t7 子域主机)。
// layerDir 形如 'vec_w'URL 路径段LAYER 形如 'vec'KVP 参数)。
function tdtTiles(layerDir, LAYER) {
return SUBS.map(function (s) {
return 'http://t' + s + '.tianditu.gov.cn/' + layerDir + '/wmts' +
'?service=wmts&request=GetTile&version=1.0.0' +
'&LAYER=' + LAYER +
'&tileMatrixSet=w&TileMatrix={z}&TileRow={y}&TileCol={x}' +
'&style=default&format=tiles&tk=' + TK;
});
}
// 两套底图:每套 = 底图层 + 注记叠加层(标准配对 vec/cva、img/cia
// maxzoom 按图层原生上限给:街道/卫星均 18base 与其注记同上限)。
var BASEMAPS = {
street: { base: ['vec_w', 'vec'], anno: ['cva_w', 'cva'], maxzoom: TILE_MAXZOOM }, // 街道(默认)
satellite: { base: ['img_w', 'img'], anno: ['cia_w', 'cia'], maxzoom: TILE_MAXZOOM }, // 卫星
};
var DEFAULT_BASEMAP = 'street';
// 为某套底图注册 sourcebase 或 anno。maxzoom 取该套底图的原生上限:
// MapLibre 据此在上限以上对最细瓦片超采样放大,而不去请求不存在的更细瓦片。
function rasterSource(layer, maxzoom) {
return {
type: 'raster',
tiles: tdtTiles(layer[0], layer[1]),
tileSize: 256,
minzoom: MAP_MINZOOM,
maxzoom: maxzoom,
};
}
// 空样式(无外部 sprite/glyph 依赖 → 完全离线于库)。底图层在运行期按需添加。
var map = new maplibregl.Map({
container: 'map',
style: { version: 8, sources: {}, layers: [] },
center: [114.1637, 22.5458], // 深圳一带([lon, lat]setPoints 到达后 fitBounds 覆盖)
zoom: 16,
minZoom: MAP_MINZOOM,
maxZoom: MAP_MAXZOOM,
attributionControl: false,
// 超放大时 WebGL 连续光栅化,关闭瓦片淡入抖动让超放大平移更顺滑。
fadeDuration: 0,
});
map.addControl(new maplibregl.NavigationControl({ showCompass: false }), 'top-left');
// 当前激活底图 key。切换时移除旧 base+anno 层与 source添加新的电极层始终置顶。
var activeBasemap = null;
// 电极数据层/source 的固定 id确保切底图后仍在最上层
var SRC_POINTS = 'traj-points';
var SRC_LINE = 'traj-line';
var SRC_START = 'traj-start';
var LYR_LINE = 'traj-line-lyr';
var LYR_POINTS = 'traj-points-lyr';
var LYR_START = 'traj-start-lyr';
// 移除某 id 的 layer + source存在才移
function removeLayerSource(id) {
if (map.getLayer(id)) map.removeLayer(id);
if (map.getSource(id)) map.removeSource(id);
}
// 切换底图先卸载旧底图层再装新底图层base 先、anno 后,确保注记压在底图上)。
// 电极层在 setBasemap 之后重新置顶(见 reorderTrajectoryLayers
function setBasemap(key) {
if (!BASEMAPS[key]) return;
// 卸载旧底图。
if (activeBasemap) {
removeLayerSource('bm-base');
removeLayerSource('bm-anno');
}
var bm = BASEMAPS[key];
map.addSource('bm-base', rasterSource(bm.base, bm.maxzoom));
map.addLayer({ id: 'bm-base', type: 'raster', source: 'bm-base' });
map.addSource('bm-anno', rasterSource(bm.anno, bm.maxzoom));
map.addLayer({ id: 'bm-anno', type: 'raster', source: 'bm-anno' });
activeBasemap = key;
reorderTrajectoryLayers();
updateSwitcherUI(key);
}
// 把电极轨迹层移到最顶(底图切换后底图层是新加的,会盖住电极层 → 重新置顶)。
function reorderTrajectoryLayers() {
[LYR_LINE, LYR_POINTS, LYR_START].forEach(function (id) {
if (map.getLayer(id)) map.moveLayer(id); // 无 beforeId → 移到最顶
});
}
// ---- 底图切换控件(右上角自定义 control----
function BasemapSwitcher() {}
BasemapSwitcher.prototype.onAdd = function () {
var wrap = document.createElement('div');
wrap.className = 'maplibregl-ctrl basemap-switcher';
var items = [
{ key: 'street', label: '街道' },
{ key: 'satellite', label: '卫星' },
];
this._buttons = {};
var self = this;
items.forEach(function (it) {
var btn = document.createElement('button');
btn.type = 'button';
btn.textContent = it.label;
btn.dataset.key = it.key;
btn.addEventListener('click', function () { setBasemap(it.key); });
wrap.appendChild(btn);
self._buttons[it.key] = btn;
});
this._container = wrap;
switcherButtons = this._buttons;
return wrap;
};
BasemapSwitcher.prototype.onRemove = function () {
if (this._container && this._container.parentNode) {
this._container.parentNode.removeChild(this._container);
}
};
var switcherButtons = null;
function updateSwitcherUI(key) {
if (!switcherButtons) return;
Object.keys(switcherButtons).forEach(function (k) {
switcherButtons[k].classList.toggle('active', k === key);
});
}
map.addControl(new BasemapSwitcher(), 'top-right');
// ---- 定位/回到测线控件(左下角,导航控件附近)----
// 点击 → 用最近一次 setPoints 的 bounds 重新 fitBounds 取景(回到/重新框住测线)。
// 十字准星图标(圆 + 中心点 + 四向短刻度),对齐主流地图「回到当前位置」按钮观感。
var currentBounds = null; // 最近一次 setPoints 计算的 LngLatBoundslocate 用)
function LocateControl() {}
LocateControl.prototype.onAdd = function () {
var btn = document.createElement('div');
btn.className = 'maplibregl-ctrl locate-ctrl';
btn.title = '回到测线';
btn.innerHTML =
'<svg width="18" height="18" viewBox="0 0 24 24" fill="none" ' +
'stroke="#333" stroke-width="2" stroke-linecap="round">' +
'<circle cx="12" cy="12" r="6"></circle>' +
'<circle cx="12" cy="12" r="1.6" fill="#333" stroke="none"></circle>' +
'<line x1="12" y1="1.5" x2="12" y2="5"></line>' +
'<line x1="12" y1="19" x2="12" y2="22.5"></line>' +
'<line x1="1.5" y1="12" x2="5" y2="12"></line>' +
'<line x1="19" y1="12" x2="22.5" y2="12"></line>' +
'</svg>';
btn.addEventListener('click', function () {
if (currentBounds && !currentBounds.isEmpty()) {
map.fitBounds(currentBounds, { padding: 40, maxZoom: 18 });
}
});
this._container = btn;
return btn;
};
LocateControl.prototype.onRemove = function () {
if (this._container && this._container.parentNode) {
this._container.parentNode.removeChild(this._container);
}
};
map.addControl(new LocateControl(), 'bottom-left');
// ---- 电极点/线/起点渲染 ----
// 待注入载荷队列:若 setPoints 在 map 'load' 之前被调用C++ 侧 loadFinished 与
// 页面 load 时序不保证),先暂存,待 load 后应用。
var mapLoaded = false;
var pendingPoints = null;
// 由宿主推送arr = [{no, lon, lat}, ...]。构造 GeoJSON → 画线 + 实心橙点 + 起点标记fitBounds 取景。
window.setPoints = function (arr) {
if (!mapLoaded) { pendingPoints = arr; return; }
renderPoints(arr);
};
function renderPoints(arr) {
// 清旧电极层。
removeLayerSource(SRC_LINE);
removeLayerSource(SRC_POINTS);
removeLayerSource(SRC_START);
if (!arr || !arr.length) return;
var features = [];
var lineCoords = [];
var bounds = new maplibregl.LngLatBounds();
for (var i = 0; i < arr.length; i++) {
var p = arr[i];
if (typeof p.lat !== 'number' || typeof p.lon !== 'number') continue;
var coord = [p.lon, p.lat];
lineCoords.push(coord);
bounds.extend(coord);
features.push({
type: 'Feature',
geometry: { type: 'Point', coordinates: coord },
properties: { no: p.no },
});
}
if (!features.length) return;
// 线(细橙),置于点之下。
if (lineCoords.length > 1) {
map.addSource(SRC_LINE, {
type: 'geojson',
data: { type: 'Feature', geometry: { type: 'LineString', coordinates: lineCoords } },
});
map.addLayer({
id: LYR_LINE, type: 'line', source: SRC_LINE,
paint: { 'line-color': TRAJ_ORANGE, 'line-width': 1, 'line-opacity': 0.5 },
});
}
// 圆点:实心橙填充 + 白描边(实心橙点配白环,对齐修复后观感)。
map.addSource(SRC_POINTS, {
type: 'geojson',
data: { type: 'FeatureCollection', features: features },
});
map.addLayer({
id: LYR_POINTS, type: 'circle', source: SRC_POINTS,
paint: {
'circle-radius': 5,
'circle-color': TRAJ_ORANGE,
'circle-opacity': 1,
'circle-stroke-color': '#ffffff',
'circle-stroke-width': 1.5,
},
});
// 起点标记首点处一个略大的橙色方块distinct marker对齐原版起点小图标
var first = features[0].geometry.coordinates;
map.addSource(SRC_START, {
type: 'geojson',
data: { type: 'Feature', geometry: { type: 'Point', coordinates: first }, properties: {} },
});
map.addLayer({
id: LYR_START, type: 'circle', source: SRC_START,
paint: {
'circle-radius': 6,
'circle-color': TRAJ_ORANGE,
'circle-stroke-color': '#b35f00',
'circle-stroke-width': 1.5,
},
});
// 取景覆盖所有点maxZoom 18 避免过度放大。bounds 存为 currentBounds 供 locate 按钮复用。
currentBounds = bounds;
if (!bounds.isEmpty()) {
map.fitBounds(bounds, { padding: 40, maxZoom: 18, duration: 0 });
}
// 悬停提示 #<no>nice-to-have
attachHoverTooltip();
}
// 圆点悬停时弹出 #<no> 提示(低优先级)。
var hoverPopup = null;
function attachHoverTooltip() {
if (hoverPopup) return; // 仅绑定一次
hoverPopup = new maplibregl.Popup({ closeButton: false, closeOnClick: false, offset: 8 });
map.on('mouseenter', LYR_POINTS, function (e) {
map.getCanvas().style.cursor = 'pointer';
var f = e.features && e.features[0];
if (!f) return;
hoverPopup
.setLngLat(f.geometry.coordinates)
.setHTML('#' + f.properties.no)
.addTo(map);
});
map.on('mousemove', LYR_POINTS, function (e) {
var f = e.features && e.features[0];
if (f) hoverPopup.setLngLat(f.geometry.coordinates).setHTML('#' + f.properties.no);
});
map.on('mouseleave', LYR_POINTS, function () {
map.getCanvas().style.cursor = '';
hoverPopup.remove();
});
}
// 页面就绪:装默认底图,应用排队的载荷(若 C++ 已先调 setPoints
map.on('load', function () {
setBasemap(DEFAULT_BASEMAP);
mapLoaded = true;
if (pendingPoints) {
var pts = pendingPoints;
pendingPoints = null;
renderPoints(pts);
}
});
</script>
</body>
</html>

View File

@ -6,7 +6,8 @@
namespace geopro::controller {
// 详情页签的渲染 kind 全集Image/GISMap 待 GPR/radar 有活样本再加YAGNI
enum class ViewKind { Scatter, FilledContour, Bar, LineProfile, PolylineMap, Table };
// WebMap = trajectory 地图页签占位(真实 GIS 地图待 Qt WebEngine 模块安装后接 QWebEngineView 替换)。
enum class ViewKind { Scatter, FilledContour, Bar, LineProfile, PolylineMap, Table, WebMap };
// 页签描述符:策略声明每个 dd 类型的页签集(标题/kind/加载键/惰性/分页)。
struct TabSpec {

View File

@ -83,9 +83,37 @@ struct BarPayload {
QString yTitle;
};
// 折线图载荷dd_trajectory_data 高程页签):单条平滑折线。
// 类目x 轴标签,如 "#1".."#40",来自 electrodeNo+ 对齐的 y 值(高程)+ 系列名 + 线色hex
// 如 #5470c6ECharts 默认蓝,两主题一致)+ 轴标题x「电极号」/ y「高程」+ 是否平滑。
struct LinePayload {
std::vector<QString> categories;
std::vector<double> y;
QString seriesName;
QString color;
QString xTitle;
QString yTitle;
bool smooth = true;
};
// 轨迹地图点dd_trajectory_data 地图页签):电极号 + WGS84 经纬度。
// 服务端 dd/ert/trajectory/line 直接返回 EPSG:4326 经纬x=经度 lon、y=纬度 lat
struct MapPoint {
int electrodeNo = 0;
double lon = 0;
double lat = 0;
};
// 轨迹地图载荷:一组电极经纬点(按电极号顺序)。视图据此在 Leaflet 上画橙色空心圈标记。
struct MapPayload {
std::vector<MapPoint> points;
};
} // namespace geopro::core
Q_DECLARE_METATYPE(geopro::core::ScatterPayload)
Q_DECLARE_METATYPE(geopro::core::ContourPayload)
Q_DECLARE_METATYPE(geopro::core::TablePayload)
Q_DECLARE_METATYPE(geopro::core::BarPayload)
Q_DECLARE_METATYPE(geopro::core::LinePayload)
Q_DECLARE_METATYPE(geopro::core::MapPayload)

View File

@ -7,6 +7,7 @@ add_library(geopro_data STATIC
dto/DatasetChartDto.cpp
dto/MeasurementDto.cpp
dto/GrMeasurementDto.cpp
dto/TrajectoryDto.cpp
api/ApiProjectRepository.cpp
api/ApiDatasetRepository.cpp
api/DatasetLoadHandles.cpp

View File

@ -11,6 +11,7 @@
#include "dto/DatasetChartDto.hpp"
#include "dto/GrMeasurementDto.hpp"
#include "dto/MeasurementDto.hpp"
#include "dto/TrajectoryDto.hpp"
#include "model/detail/DetailPayloads.hpp"
namespace geopro::data {
@ -112,6 +113,25 @@ QJsonArray grDataArray(const QList<net::ApiResponse>& r) {
return r[0].data.value(QStringLiteral("value")).toArray();
}
// 轨迹trajectory列表与高程同一端点 trajectory/rows(GETquery 参数)。
// 响应 data = {rowList[40], gridHeaderDisplay[14], ...}。
net::ApiBatch* trajectoryRowsBatch(net::ApiClient& api, const std::string& dsId) {
QList<net::IApiCall*> calls{
api.getAsync(QStringLiteral("/business/dd/ert/trajectory/rows?dsObjectId=%1").arg(enc(dsId))),
};
return new net::ApiBatch(calls, &isFailure);
}
// 轨迹地图:单请求 trajectory/line(GETquery 参数)。frontCrsCode=EPSG:4326原版发 EPSG%3A4326
// 此处用 enc() 对整串 url 编码故 ':' → %3A。响应 data = {electrodelList[...]}。
net::ApiBatch* trajectoryLineBatch(net::ApiClient& api, const std::string& dsId) {
QList<net::IApiCall*> calls{
api.getAsync(QStringLiteral("/business/dd/ert/trajectory/line?dsObjectId=%1&frontCrsCode=%2")
.arg(enc(dsId), enc("EPSG:4326"))),
};
return new net::ApiBatch(calls, &isFailure);
}
} // namespace
ApiDatasetRepository::ApiDatasetRepository(net::ApiClient& api) : api_(api) {}
@ -123,6 +143,9 @@ DetailLoad* ApiDatasetRepository::loadAsync(const std::string& loaderKey, const
if (loaderKey == "ert_measurement.rows") return makeMeasurementRows(dsId);
if (loaderKey == "gr.bar") return makeGrBar(dsId);
if (loaderKey == "gr.rows") return makeGrRows(dsId);
if (loaderKey == "traj.rows") return makeTrajectoryRows(dsId);
if (loaderKey == "traj.elev") return makeTrajectoryElevation(dsId);
if (loaderKey == "traj.map") return makeTrajectoryMap(dsId);
throw std::runtime_error("unknown loaderKey: " + loaderKey);
}
@ -166,4 +189,22 @@ DetailLoad* ApiDatasetRepository::makeGrRows(const std::string& dsId) {
});
}
DetailLoad* ApiDatasetRepository::makeTrajectoryRows(const std::string& dsId) {
return new ApiDetailLoad(trajectoryRowsBatch(api_, dsId), [](const QList<net::ApiResponse>& r) {
return QVariant::fromValue(dto::parseTrajectoryTable(r[0].data));
});
}
DetailLoad* ApiDatasetRepository::makeTrajectoryElevation(const std::string& dsId) {
return new ApiDetailLoad(trajectoryRowsBatch(api_, dsId), [](const QList<net::ApiResponse>& r) {
return QVariant::fromValue(dto::parseTrajectoryElevation(r[0].data));
});
}
DetailLoad* ApiDatasetRepository::makeTrajectoryMap(const std::string& dsId) {
return new ApiDetailLoad(trajectoryLineBatch(api_, dsId), [](const QList<net::ApiResponse>& r) {
return QVariant::fromValue(dto::parseTrajectoryMap(r[0].data));
});
}
} // namespace geopro::data

View File

@ -15,6 +15,9 @@ private:
DetailLoad* makeMeasurementRows(const std::string& dsId);
DetailLoad* makeGrBar(const std::string& dsId);
DetailLoad* makeGrRows(const std::string& dsId);
DetailLoad* makeTrajectoryRows(const std::string& dsId);
DetailLoad* makeTrajectoryElevation(const std::string& dsId);
DetailLoad* makeTrajectoryMap(const std::string& dsId);
net::ApiClient& api_;
};
} // namespace geopro::data

View File

@ -0,0 +1,111 @@
#include "dto/TrajectoryDto.hpp"
#include <algorithm>
#include <QJsonArray>
#include <QJsonValue>
namespace geopro::data::dto {
using namespace geopro::core;
namespace {
// ECharts 默认蓝(折线色,数据色——浅/暗主题一致)。
const QString kLineColor = QStringLiteral("#5470c6");
// 把 JSON 值预格式化为单元格 QString整数不带小数点null/缺省→空串)。
QString cellText(const QJsonValue& v) {
if (v.isDouble()) {
const double d = v.toDouble();
if (d == static_cast<double>(static_cast<long long>(d)))
return QString::number(static_cast<long long>(d));
return QString::number(d, 'g', 10);
}
if (v.isString()) return v.toString();
if (v.isBool()) return v.toBool() ? QStringLiteral("true") : QStringLiteral("false");
return QString(); // null/undefined
}
} // namespace
TablePayload parseGridHeaderTable(const QJsonObject& data) {
TablePayload t;
// 列定义gridHeaderDisplay按 columnSort 升序。
QJsonArray header = data.value(QStringLiteral("gridHeaderDisplay")).toArray();
std::vector<QJsonObject> cols;
cols.reserve(header.size());
for (const auto& e : header) cols.push_back(e.toObject());
std::stable_sort(cols.begin(), cols.end(), [](const QJsonObject& a, const QJsonObject& b) {
return a.value(QStringLiteral("columnSort")).toInt() <
b.value(QStringLiteral("columnSort")).toInt();
});
for (const auto& c : cols) {
TableColumn col;
col.code = c.value(QStringLiteral("columnCode")).toString();
col.title = c.value(QStringLiteral("columnNameChn")).toString();
if (col.title.isEmpty()) col.title = col.code;
col.width = c.value(QStringLiteral("columnWidth")).toInt();
col.sort = c.value(QStringLiteral("columnSort")).toInt();
t.columns.push_back(col);
}
// 行:每格按列码取行对象顶层值。
const QJsonArray rowList = data.value(QStringLiteral("rowList")).toArray();
for (const auto& e : rowList) {
const QJsonObject obj = e.toObject();
std::vector<QString> cells;
cells.reserve(t.columns.size());
for (const auto& col : t.columns) cells.push_back(cellText(obj.value(col.code)));
t.rows.push_back(std::move(cells));
}
// 总数分页用trajectory 用 __rowTotal回退本批行数。
t.total = data.value(QStringLiteral("__rowTotal")).toInt(static_cast<int>(t.rows.size()));
return t;
}
TablePayload parseTrajectoryTable(const QJsonObject& data) {
return parseGridHeaderTable(data);
}
LinePayload parseTrajectoryElevation(const QJsonObject& data) {
LinePayload p;
p.seriesName = QStringLiteral("高程");
p.color = kLineColor;
p.xTitle = QStringLiteral("电极号");
p.yTitle = QStringLiteral("高程");
p.smooth = true;
const QJsonArray rowList = data.value(QStringLiteral("rowList")).toArray();
for (const auto& e : rowList) {
const QJsonObject obj = e.toObject();
p.categories.push_back(QStringLiteral("#") +
QString::number(obj.value(QStringLiteral("electrodeNo")).toInt()));
p.y.push_back(obj.value(QStringLiteral("elevation")).toDouble());
}
return p;
}
MapPayload parseTrajectoryMap(const QJsonObject& data) {
MapPayload m;
const QJsonArray list = data.value(QStringLiteral("electrodelList")).toArray();
for (const auto& e : list) {
const QJsonObject obj = e.toObject();
const QJsonObject coord = obj.value(QStringLiteral("electrodeCoordinate")).toObject();
const QJsonArray geom = coord.value(QStringLiteral("geometryCoordinates")).toArray();
if (geom.isEmpty()) continue; // 跳过缺坐标
const QJsonObject pt = geom.first().toObject();
const QJsonValue xv = pt.value(QStringLiteral("x"));
const QJsonValue yv = pt.value(QStringLiteral("y"));
if (!xv.isDouble() || !yv.isDouble()) continue; // 跳过 null/非数值
MapPoint p;
p.electrodeNo = obj.value(QStringLiteral("electrodeNo")).toInt();
p.lon = xv.toDouble();
p.lat = yv.toDouble();
m.points.push_back(p);
}
return m;
}
} // namespace geopro::data::dto

View File

@ -0,0 +1,29 @@
#pragma once
#include <QJsonObject>
#include "model/detail/DetailPayloads.hpp"
namespace geopro::data::dto {
// dd_trajectory_dataERT 电极坐标 轨迹)。三页签同一端点 dd/ert/trajectory/rows
// data 形如 {rowList[40], gridHeaderDisplay[14]})。
// 通用「gridHeaderDisplay + rowList → TablePayload」解析器可复用trajectory 列表 / 未来 dd_grid
// 列定义来自 gridHeaderDisplay按 columnSort 升序title=columnNameChn、code=columnCode、
// width=columnWidth行来自 rowList每格按 columnCode 取顶层值,预格式化为 QStringnull→空串
// total 优先取 data.__rowTotal回退本批行数。
geopro::core::TablePayload parseGridHeaderTable(const QJsonObject& data);
// 列表页签直接复用通用解析器14 列:电极号/平距/斜距/经度·东坐标/纬度·北坐标/高程/线号/
// 道号/里程/深度/水深/日期/时间/解状态)。
geopro::core::TablePayload parseTrajectoryTable(const QJsonObject& data);
// 高程页签:单条平滑折线。类目 "#"+electrodeNo"#1".."#40"y=elevation
// x 轴标题「电极号」、y 轴标题「高程」,线色 #5470c6ECharts 默认蓝smooth=true。
geopro::core::LinePayload parseTrajectoryElevation(const QJsonObject& data);
// 地图页签:电极经纬点。来自 dd/ert/trajectory/line 端点
// data.electrodelList[].electrodeCoordinate.geometryCoordinates[0] → {x=lon, y=lat})。
// 跳过缺失/null 坐标的电极。服务端直接返回 EPSG:4326 经纬,无需投影变换。
geopro::core::MapPayload parseTrajectoryMap(const QJsonObject& data);
} // namespace geopro::data::dto

View File

@ -40,6 +40,7 @@ target_sources(geopro_tests PRIVATE data/test_nav_dto.cpp)
target_sources(geopro_tests PRIVATE data/test_dataset_chart_dto.cpp)
target_sources(geopro_tests PRIVATE data/test_measurement_dto.cpp)
target_sources(geopro_tests PRIVATE data/test_gr_dto.cpp)
target_sources(geopro_tests PRIVATE data/test_trajectory_dto.cpp)
target_sources(geopro_tests PRIVATE data/test_dataset_load_handles.cpp)
# 线loadAsync + QVariant payload round-trip
target_sources(geopro_tests PRIVATE data/test_async_repo_dispatch.cpp)

View File

@ -3,6 +3,7 @@
#include "IDatasetChartStrategy.hpp" // geopro::controller控制器层
#include "panels/chart/MeasurementStrategy.hpp"
#include "panels/chart/GrMeasurementStrategy.hpp"
#include "panels/chart/TrajectoryStrategy.hpp"
using namespace geopro::controller;
namespace {
struct Fake : IDatasetChartStrategy {
@ -70,3 +71,25 @@ TEST(GrMeasurementStrategy, DrivesBarAndTableTabs) {
EXPECT_TRUE(tabs[1].lazy);
EXPECT_EQ(tabs[1].loaderKey.toStdString(), "gr.rows");
}
TEST(TrajectoryStrategy, DrivesMapTableElevationTabs) {
geopro::app::TrajectoryStrategy s;
EXPECT_EQ(s.ddCode(), "dd_trajectory_data");
const auto tabs = s.tabs();
ASSERT_EQ(tabs.size(), 3u);
// 地图WebMap首位/非 lazy默认页loaderKey traj.map → MapPayload端点 trajectory/line
EXPECT_EQ(tabs[0].title.toStdString(), std::string("地图"));
EXPECT_EQ(tabs[0].kind, ViewKind::WebMap);
EXPECT_FALSE(tabs[0].lazy);
EXPECT_EQ(tabs[0].loaderKey.toStdString(), "traj.map");
// 列表Tablelazy。
EXPECT_EQ(tabs[1].title.toStdString(), std::string("列表"));
EXPECT_EQ(tabs[1].kind, ViewKind::Table);
EXPECT_TRUE(tabs[1].lazy);
EXPECT_EQ(tabs[1].loaderKey.toStdString(), "traj.rows");
// 高程LineProfile非 lazy。
EXPECT_EQ(tabs[2].title.toStdString(), std::string("高程"));
EXPECT_EQ(tabs[2].kind, ViewKind::LineProfile);
EXPECT_FALSE(tabs[2].lazy);
EXPECT_EQ(tabs[2].loaderKey.toStdString(), "traj.elev");
}

View File

@ -53,6 +53,18 @@ TEST(AsyncRepoDispatch, KnownKeysReturnNonNullHandle) {
DetailLoad* grRows = repo.loadAsync("gr.rows", "ds1");
ASSERT_NE(grRows, nullptr);
grRows->abort();
DetailLoad* trajRows = repo.loadAsync("traj.rows", "ds1");
ASSERT_NE(trajRows, nullptr);
trajRows->abort();
DetailLoad* trajElev = repo.loadAsync("traj.elev", "ds1");
ASSERT_NE(trajElev, nullptr);
trajElev->abort();
DetailLoad* trajMap = repo.loadAsync("traj.map", "ds1");
ASSERT_NE(trajMap, nullptr);
trajMap->abort();
}
// 未知 loaderKey 抛 std::runtime_error。

View File

@ -0,0 +1,179 @@
#include <gtest/gtest.h>
#include <QJsonDocument>
#include <QJsonObject>
#include "dto/TrajectoryDto.hpp"
using namespace geopro::data::dto;
using geopro::core::TableColumnKind;
namespace {
// 取自真实夹具 tests/fixtures/dd/ert-trajectory-rows.json 的 datarowList 裁剪至 20 行,
// gridHeaderDisplay 全量 14 列,逐字一致)。内联避免引入 fixture 路径编译定义。
const char* kTrajData = R"({
"gridHeaderDisplay": [
{ "columnCode": "electrodeNo", "columnNameChn": "电极号", "columnSort": 1, "columnWidth": 10 },
{ "columnCode": "horizontalDistance", "columnNameChn": "平距", "columnSort": 2, "columnWidth": 10 },
{ "columnCode": "slopeDistance", "columnNameChn": "斜距", "columnSort": 3, "columnWidth": 10 },
{ "columnCode": "projectX", "columnNameChn": "经度/东坐标", "columnSort": 4, "columnWidth": 10 },
{ "columnCode": "projectY", "columnNameChn": "纬度/北坐标", "columnSort": 5, "columnWidth": 10 },
{ "columnCode": "elevation", "columnNameChn": "高程", "columnSort": 6, "columnWidth": 10 },
{ "columnCode": "lineNo", "columnNameChn": "线号", "columnSort": 7, "columnWidth": 10 },
{ "columnCode": "channelNo", "columnNameChn": "道号", "columnSort": 8, "columnWidth": 10 },
{ "columnCode": "measuredDistance", "columnNameChn": "里程", "columnSort": 9, "columnWidth": 10 },
{ "columnCode": "depth", "columnNameChn": "深度", "columnSort": 10, "columnWidth": 10 },
{ "columnCode": "depthOfWater", "columnNameChn": "水深", "columnSort": 11, "columnWidth": 10 },
{ "columnCode": "date", "columnNameChn": "日期", "columnSort": 12, "columnWidth": 10 },
{ "columnCode": "time", "columnNameChn": "时间", "columnSort": 13, "columnWidth": 10 },
{ "columnCode": "resultStatus", "columnNameChn": "解状态", "columnSort": 14, "columnWidth": 10 }
],
"__rowTotal": 40,
"rowList": [
{ "electrodeNo": 1, "horizontalDistance": 0, "slopeDistance": 0, "projectX": 516838.884, "projectY": 2494251.836, "elevation": 26.172, "lineNo": "ert1", "channelNo": "ERT1-1", "measuredDistance": null, "depth": null, "depthOfWater": null, "date": null, "time": null, "resultStatus": "固定解" },
{ "electrodeNo": 2, "horizontalDistance": 1.08, "slopeDistance": 1.24, "projectX": 516840.099, "projectY": 2494252.112, "elevation": 25.551, "lineNo": "ert1", "channelNo": "ERT1-2", "measuredDistance": null, "depth": null, "depthOfWater": null, "date": null, "time": null, "resultStatus": "固定解" },
{ "electrodeNo": 3, "horizontalDistance": 3.14, "slopeDistance": 3.31, "projectX": 516842.07, "projectY": 2494252.756, "elevation": 25.425, "lineNo": "ert1", "channelNo": "ERT1-3", "measuredDistance": null, "depth": null, "depthOfWater": null, "date": null, "time": null, "resultStatus": "固定解" },
{ "electrodeNo": 4, "horizontalDistance": 4.94, "slopeDistance": 5.12, "projectX": 516843.874, "projectY": 2494252.792, "elevation": 25.625, "lineNo": "ert1", "channelNo": "ERT1-4", "measuredDistance": null, "depth": null, "depthOfWater": null, "date": null, "time": null, "resultStatus": "固定解" },
{ "electrodeNo": 5, "horizontalDistance": 7.09, "slopeDistance": 7.28, "projectX": 516846.009, "projectY": 2494253.098, "elevation": 25.615, "lineNo": "ert1", "channelNo": "ERT1-5", "measuredDistance": null, "depth": null, "depthOfWater": null, "date": null, "time": null, "resultStatus": "固定解" },
{ "electrodeNo": 6, "horizontalDistance": 8.91, "slopeDistance": 9.09, "projectX": 516847.815, "projectY": 2494253.249, "elevation": 25.627, "lineNo": "ert1", "channelNo": "ERT1-6", "measuredDistance": null, "depth": null, "depthOfWater": null, "date": null, "time": null, "resultStatus": "固定解" },
{ "electrodeNo": 7, "horizontalDistance": 11.04, "slopeDistance": 11.22, "projectX": 516849.899, "projectY": 2494253.713, "elevation": 25.549, "lineNo": "ert1", "channelNo": "ERT1-7", "measuredDistance": null, "depth": null, "depthOfWater": null, "date": null, "time": null, "resultStatus": "固定解" },
{ "electrodeNo": 8, "horizontalDistance": 13.09, "slopeDistance": 13.27, "projectX": 516851.911, "projectY": 2494254.1, "elevation": 25.49, "lineNo": "ert1", "channelNo": "ERT1-8", "measuredDistance": null, "depth": null, "depthOfWater": null, "date": null, "time": null, "resultStatus": "固定解" },
{ "electrodeNo": 9, "horizontalDistance": 15.03, "slopeDistance": 15.26, "projectX": 516853.844, "projectY": 2494254.561, "elevation": 25.922, "lineNo": "ert1", "channelNo": "ERT1-9", "measuredDistance": null, "depth": null, "depthOfWater": null, "date": null, "time": null, "resultStatus": "固定解" },
{ "electrodeNo": 10, "horizontalDistance": 16.95, "slopeDistance": 17.26, "projectX": 516855.736, "projectY": 2494255.216, "elevation": 25.363, "lineNo": "ert1", "channelNo": "ERT1-10", "measuredDistance": null, "depth": null, "depthOfWater": null, "date": null, "time": null, "resultStatus": "固定解" },
{ "electrodeNo": 11, "horizontalDistance": 18.92, "slopeDistance": 19.24, "projectX": 516857.548, "projectY": 2494255.999, "elevation": 25.354, "lineNo": "ert1", "channelNo": "ERT1-11", "measuredDistance": null, "depth": null, "depthOfWater": null, "date": null, "time": null, "resultStatus": "固定解" },
{ "electrodeNo": 12, "horizontalDistance": 20.97, "slopeDistance": 21.28, "projectX": 516859.35, "projectY": 2494256.96, "elevation": 25.39, "lineNo": "ert1", "channelNo": "ERT1-12", "measuredDistance": null, "depth": null, "depthOfWater": null, "date": null, "time": null, "resultStatus": "固定解" },
{ "electrodeNo": 13, "horizontalDistance": 22.92, "slopeDistance": 23.24, "projectX": 516861.033, "projectY": 2494257.96, "elevation": 25.444, "lineNo": "ert1", "channelNo": "ERT1-13", "measuredDistance": null, "depth": null, "depthOfWater": null, "date": null, "time": null, "resultStatus": "固定解" },
{ "electrodeNo": 14, "horizontalDistance": 24.81, "slopeDistance": 25.12, "projectX": 516862.667, "projectY": 2494258.906, "elevation": 25.48, "lineNo": "ert1", "channelNo": "ERT1-14", "measuredDistance": null, "depth": null, "depthOfWater": null, "date": null, "time": null, "resultStatus": "固定解" },
{ "electrodeNo": 15, "horizontalDistance": 26.92, "slopeDistance": 27.24, "projectX": 516864.416, "projectY": 2494260.092, "elevation": 25.384, "lineNo": "ert1", "channelNo": "ERT1-15", "measuredDistance": null, "depth": null, "depthOfWater": null, "date": null, "time": null, "resultStatus": "固定解" },
{ "electrodeNo": 16, "horizontalDistance": 28.68, "slopeDistance": 29.00, "projectX": 516865.639, "projectY": 2494261.366, "elevation": 25.5, "lineNo": "ert1", "channelNo": "ERT1-16", "measuredDistance": null, "depth": null, "depthOfWater": null, "date": null, "time": null, "resultStatus": "固定解" },
{ "electrodeNo": 17, "horizontalDistance": 30.88, "slopeDistance": 31.19, "projectX": 516867.233, "projectY": 2494262.869, "elevation": 25.54, "lineNo": "ert1", "channelNo": "ERT1-17", "measuredDistance": null, "depth": null, "depthOfWater": null, "date": null, "time": null, "resultStatus": "固定解" },
{ "electrodeNo": 18, "horizontalDistance": 32.76, "slopeDistance": 33.08, "projectX": 516868.457, "projectY": 2494264.304, "elevation": 25.505, "lineNo": "ert1", "channelNo": "ERT1-18", "measuredDistance": null, "depth": null, "depthOfWater": null, "date": null, "time": null, "resultStatus": "固定解" },
{ "electrodeNo": 19, "horizontalDistance": 34.71, "slopeDistance": 35.03, "projectX": 516869.61, "projectY": 2494265.873, "elevation": 25.576, "lineNo": "ert1", "channelNo": "ERT1-19", "measuredDistance": null, "depth": null, "depthOfWater": null, "date": null, "time": null, "resultStatus": "固定解" },
{ "electrodeNo": 20, "horizontalDistance": 36.64, "slopeDistance": 36.96, "projectX": 516870.684, "projectY": 2494267.48, "elevation": 25.567, "lineNo": "ert1", "channelNo": "ERT1-20", "measuredDistance": null, "depth": null, "depthOfWater": null, "date": null, "time": null, "resultStatus": "固定解" }
]
})";
QJsonObject trajData() { return QJsonDocument::fromJson(kTrajData).object(); }
// 取自真实夹具 tests/fixtures/dd/ert-trajectory-line.json 的 dataelectrodelList 20 个电极,
// 逐字一致;服务端直接返回 EPSG:4326 经纬 x=lon/y=lat。内联避免引入 fixture 路径编译定义。
const char* kTrajLineData = R"({
"id": null,
"electrodelList": [
{ "electrodeNo": 1, "electrodeCoordinate": { "crsCode": "EPSG:4326", "closure": null, "geometryType": 1, "geometryCoordinates": [{ "x": 114.16370297649078, "y": 22.54583186479361, "z": 0 }] } },
{ "electrodeNo": 2, "electrodeCoordinate": { "crsCode": "EPSG:4326", "closure": null, "geometryType": 1, "geometryCoordinates": [{ "x": 114.16371479128762, "y": 22.54583434513919, "z": 0 }] } },
{ "electrodeNo": 3, "electrodeCoordinate": { "crsCode": "EPSG:4326", "closure": null, "geometryType": 1, "geometryCoordinates": [{ "x": 114.16373395960512, "y": 22.545840141157978, "z": 0 }] } },
{ "electrodeNo": 4, "electrodeCoordinate": { "crsCode": "EPSG:4326", "closure": null, "geometryType": 1, "geometryCoordinates": [{ "x": 114.16375149792411, "y": 22.545840448398355, "z": 0 }] } },
{ "electrodeNo": 5, "electrodeCoordinate": { "crsCode": "EPSG:4326", "closure": null, "geometryType": 1, "geometryCoordinates": [{ "x": 114.16377225700053, "y": 22.545843190544776, "z": 0 }] } },
{ "electrodeNo": 6, "electrodeCoordinate": { "crsCode": "EPSG:4326", "closure": null, "geometryType": 1, "geometryCoordinates": [{ "x": 114.16378981598876, "y": 22.54584453624687, "z": 0 }] } },
{ "electrodeNo": 7, "electrodeCoordinate": { "crsCode": "EPSG:4326", "closure": null, "geometryType": 1, "geometryCoordinates": [{ "x": 114.16381008094395, "y": 22.54584870568223, "z": 0 }] } },
{ "electrodeNo": 8, "electrodeCoordinate": { "crsCode": "EPSG:4326", "closure": null, "geometryType": 1, "geometryCoordinates": [{ "x": 114.16382964511763, "y": 22.545852180493952, "z": 0 }] } },
{ "electrodeNo": 9, "electrodeCoordinate": { "crsCode": "EPSG:4326", "closure": null, "geometryType": 1, "geometryCoordinates": [{ "x": 114.16384844206722, "y": 22.54585632432848, "z": 0 }] } },
{ "electrodeNo": 10, "electrodeCoordinate": { "crsCode": "EPSG:4326", "closure": null, "geometryType": 1, "geometryCoordinates": [{ "x": 114.16386684249656, "y": 22.545862220446992, "z": 0 }] } },
{ "electrodeNo": 11, "electrodeCoordinate": { "crsCode": "EPSG:4326", "closure": null, "geometryType": 1, "geometryCoordinates": [{ "x": 114.16388446655657, "y": 22.545869273235397, "z": 0 }] } },
{ "electrodeNo": 12, "electrodeCoordinate": { "crsCode": "EPSG:4326", "closure": null, "geometryType": 1, "geometryCoordinates": [{ "x": 114.16390199529941, "y": 22.545877933516117, "z": 0 }] } },
{ "electrodeNo": 13, "electrodeCoordinate": { "crsCode": "EPSG:4326", "closure": null, "geometryType": 1, "geometryCoordinates": [{ "x": 114.16391836757842, "y": 22.545886947155804, "z": 0 }] } },
{ "electrodeNo": 14, "electrodeCoordinate": { "crsCode": "EPSG:4326", "closure": null, "geometryType": 1, "geometryCoordinates": [{ "x": 114.16393426292046, "y": 22.545895473642467, "z": 0 }] } },
{ "electrodeNo": 15, "electrodeCoordinate": { "crsCode": "EPSG:4326", "closure": null, "geometryType": 1, "geometryCoordinates": [{ "x": 114.16395127881961, "y": 22.545906166262615, "z": 0 }] } },
{ "electrodeNo": 16, "electrodeCoordinate": { "crsCode": "EPSG:4326", "closure": null, "geometryType": 1, "geometryCoordinates": [{ "x": 114.16396318204642, "y": 22.545917658760136, "z": 0 }] } },
{ "electrodeNo": 17, "electrodeCoordinate": { "crsCode": "EPSG:4326", "closure": null, "geometryType": 1, "geometryCoordinates": [{ "x": 114.16397869446884, "y": 22.545931215521435, "z": 0 }] } },
{ "electrodeNo": 18, "electrodeCoordinate": { "crsCode": "EPSG:4326", "closure": null, "geometryType": 1, "geometryCoordinates": [{ "x": 114.16399060913946, "y": 22.545944161886897, "z": 0 }] } },
{ "electrodeNo": 19, "electrodeCoordinate": { "crsCode": "EPSG:4326", "closure": null, "geometryType": 1, "geometryCoordinates": [{ "x": 114.16400183500123, "y": 22.54595831901666, "z": 0 }] } },
{ "electrodeNo": 20, "electrodeCoordinate": { "crsCode": "EPSG:4326", "closure": null, "geometryType": 1, "geometryCoordinates": [{ "x": 114.16401229325622, "y": 22.545972820080397, "z": 0 }] } }
]
})";
QJsonObject trajLineData() { return QJsonDocument::fromJson(kTrajLineData).object(); }
} // namespace
TEST(TrajectoryDto, ParsesElevationLineSeries) {
const QJsonObject data = trajData();
auto line = parseTrajectoryElevation(data);
// 类目 "#1".."#20"(来自 electrodeNo
ASSERT_EQ(line.categories.size(), 20u);
EXPECT_EQ(line.categories.front().toStdString(), "#1");
EXPECT_EQ(line.categories.back().toStdString(), "#20");
// y = elevation首值 26.172。
ASSERT_EQ(line.y.size(), 20u);
EXPECT_DOUBLE_EQ(line.y.front(), 26.172);
EXPECT_DOUBLE_EQ(line.y[1], 25.551);
// 轴标题 / 系列 / 色 / 平滑。
EXPECT_EQ(line.xTitle.toStdString(), std::string("电极号"));
EXPECT_EQ(line.yTitle.toStdString(), std::string("高程"));
EXPECT_EQ(line.seriesName.toStdString(), std::string("高程"));
EXPECT_EQ(line.color.toStdString(), "#5470c6");
EXPECT_TRUE(line.smooth);
}
TEST(TrajectoryDto, ParsesTableFourteenColumnsOrderedBySort) {
const QJsonObject data = trajData();
auto t = parseTrajectoryTable(data);
// 14 列,按 columnSort 升序title=columnNameChn首列 电极号。
ASSERT_EQ(t.columns.size(), 14u);
EXPECT_EQ(t.columns[0].title.toStdString(), std::string("电极号"));
EXPECT_EQ(t.columns[0].code.toStdString(), "electrodeNo");
EXPECT_EQ(t.columns[1].title.toStdString(), std::string("平距"));
EXPECT_EQ(t.columns[2].title.toStdString(), std::string("斜距"));
EXPECT_EQ(t.columns[5].title.toStdString(), std::string("高程"));
EXPECT_EQ(t.columns[13].title.toStdString(), std::string("解状态"));
// 无 Toggle 列trajectory 走 gridHeaderDisplay不追加开关列
for (const auto& c : t.columns) EXPECT_EQ(c.kind, TableColumnKind::Text);
// 20 行row0电极号=1 / projectX=516838.884 / elevation=26.172 / resultStatus=固定解。
ASSERT_EQ(t.rows.size(), 20u);
ASSERT_EQ(t.rows[0].size(), 14u);
EXPECT_EQ(t.rows[0][0].toStdString(), "1"); // electrodeNo
EXPECT_EQ(t.rows[0][3].toStdString(), "516838.884"); // projectX
EXPECT_EQ(t.rows[0][5].toStdString(), "26.172"); // elevation
EXPECT_EQ(t.rows[0][13].toStdString(), std::string("固定解")); // resultStatus
// null 单元 → 空串(里程/深度/水深/日期/时间)。
EXPECT_TRUE(t.rows[0][8].isEmpty()); // measuredDistance null
EXPECT_TRUE(t.rows[0][9].isEmpty()); // depth null
EXPECT_TRUE(t.rows[0][11].isEmpty()); // date null
// total 取 __rowTotal=40非本批 20
EXPECT_EQ(t.total, 40);
}
TEST(TrajectoryDto, ParsesEmptyToValidEmptyPayloads) {
const QJsonObject empty;
auto line = parseTrajectoryElevation(empty);
EXPECT_EQ(line.categories.size(), 0u);
EXPECT_EQ(line.y.size(), 0u);
EXPECT_EQ(line.xTitle.toStdString(), std::string("电极号"));
EXPECT_TRUE(line.smooth);
auto t = parseTrajectoryTable(empty);
EXPECT_EQ(t.columns.size(), 0u);
EXPECT_EQ(t.rows.size(), 0u);
EXPECT_EQ(t.total, 0);
auto m = parseTrajectoryMap(empty);
EXPECT_EQ(m.points.size(), 0u);
}
TEST(TrajectoryDto, ParsesMapPointsFromLineEndpoint) {
const QJsonObject data = trajLineData();
auto m = parseTrajectoryMap(data);
// 20 个电极点electrodelList
ASSERT_EQ(m.points.size(), 20u);
// point0electrodeNo=1lon≈114.16370lat≈22.54583x=lon、y=latEPSG:4326 直返)。
EXPECT_EQ(m.points[0].electrodeNo, 1);
EXPECT_NEAR(m.points[0].lon, 114.16370297649078, 1e-9);
EXPECT_NEAR(m.points[0].lat, 22.54583186479361, 1e-9);
// point19electrodeNo=20。
EXPECT_EQ(m.points[19].electrodeNo, 20);
EXPECT_NEAR(m.points[19].lon, 114.16401229325622, 1e-9);
EXPECT_NEAR(m.points[19].lat, 22.545972820080397, 1e-9);
}

View File

@ -0,0 +1,29 @@
{
"code": 200,
"msg": "成功",
"data": {
"id": null,
"electrodelList": [
{ "electrodeNo": 1, "electrodeCoordinate": { "crsCode": "EPSG:4326", "closure": null, "geometryType": 1, "geometryCoordinates": [{ "x": 114.16370297649078, "y": 22.54583186479361, "z": 0 }] } },
{ "electrodeNo": 2, "electrodeCoordinate": { "crsCode": "EPSG:4326", "closure": null, "geometryType": 1, "geometryCoordinates": [{ "x": 114.16371479128762, "y": 22.54583434513919, "z": 0 }] } },
{ "electrodeNo": 3, "electrodeCoordinate": { "crsCode": "EPSG:4326", "closure": null, "geometryType": 1, "geometryCoordinates": [{ "x": 114.16373395960512, "y": 22.545840141157978, "z": 0 }] } },
{ "electrodeNo": 4, "electrodeCoordinate": { "crsCode": "EPSG:4326", "closure": null, "geometryType": 1, "geometryCoordinates": [{ "x": 114.16375149792411, "y": 22.545840448398355, "z": 0 }] } },
{ "electrodeNo": 5, "electrodeCoordinate": { "crsCode": "EPSG:4326", "closure": null, "geometryType": 1, "geometryCoordinates": [{ "x": 114.16377225700053, "y": 22.545843190544776, "z": 0 }] } },
{ "electrodeNo": 6, "electrodeCoordinate": { "crsCode": "EPSG:4326", "closure": null, "geometryType": 1, "geometryCoordinates": [{ "x": 114.16378981598876, "y": 22.54584453624687, "z": 0 }] } },
{ "electrodeNo": 7, "electrodeCoordinate": { "crsCode": "EPSG:4326", "closure": null, "geometryType": 1, "geometryCoordinates": [{ "x": 114.16381008094395, "y": 22.54584870568223, "z": 0 }] } },
{ "electrodeNo": 8, "electrodeCoordinate": { "crsCode": "EPSG:4326", "closure": null, "geometryType": 1, "geometryCoordinates": [{ "x": 114.16382964511763, "y": 22.545852180493952, "z": 0 }] } },
{ "electrodeNo": 9, "electrodeCoordinate": { "crsCode": "EPSG:4326", "closure": null, "geometryType": 1, "geometryCoordinates": [{ "x": 114.16384844206722, "y": 22.54585632432848, "z": 0 }] } },
{ "electrodeNo": 10, "electrodeCoordinate": { "crsCode": "EPSG:4326", "closure": null, "geometryType": 1, "geometryCoordinates": [{ "x": 114.16386684249656, "y": 22.545862220446992, "z": 0 }] } },
{ "electrodeNo": 11, "electrodeCoordinate": { "crsCode": "EPSG:4326", "closure": null, "geometryType": 1, "geometryCoordinates": [{ "x": 114.16388446655657, "y": 22.545869273235397, "z": 0 }] } },
{ "electrodeNo": 12, "electrodeCoordinate": { "crsCode": "EPSG:4326", "closure": null, "geometryType": 1, "geometryCoordinates": [{ "x": 114.16390199529941, "y": 22.545877933516117, "z": 0 }] } },
{ "electrodeNo": 13, "electrodeCoordinate": { "crsCode": "EPSG:4326", "closure": null, "geometryType": 1, "geometryCoordinates": [{ "x": 114.16391836757842, "y": 22.545886947155804, "z": 0 }] } },
{ "electrodeNo": 14, "electrodeCoordinate": { "crsCode": "EPSG:4326", "closure": null, "geometryType": 1, "geometryCoordinates": [{ "x": 114.16393426292046, "y": 22.545895473642467, "z": 0 }] } },
{ "electrodeNo": 15, "electrodeCoordinate": { "crsCode": "EPSG:4326", "closure": null, "geometryType": 1, "geometryCoordinates": [{ "x": 114.16395127881961, "y": 22.545906166262615, "z": 0 }] } },
{ "electrodeNo": 16, "electrodeCoordinate": { "crsCode": "EPSG:4326", "closure": null, "geometryType": 1, "geometryCoordinates": [{ "x": 114.16396318204642, "y": 22.545917658760136, "z": 0 }] } },
{ "electrodeNo": 17, "electrodeCoordinate": { "crsCode": "EPSG:4326", "closure": null, "geometryType": 1, "geometryCoordinates": [{ "x": 114.16397869446884, "y": 22.545931215521435, "z": 0 }] } },
{ "electrodeNo": 18, "electrodeCoordinate": { "crsCode": "EPSG:4326", "closure": null, "geometryType": 1, "geometryCoordinates": [{ "x": 114.16399060913946, "y": 22.545944161886897, "z": 0 }] } },
{ "electrodeNo": 19, "electrodeCoordinate": { "crsCode": "EPSG:4326", "closure": null, "geometryType": 1, "geometryCoordinates": [{ "x": 114.16400183500123, "y": 22.54595831901666, "z": 0 }] } },
{ "electrodeNo": 20, "electrodeCoordinate": { "crsCode": "EPSG:4326", "closure": null, "geometryType": 1, "geometryCoordinates": [{ "x": 114.16401229325622, "y": 22.545972820080397, "z": 0 }] } }
]
}
}

View File

@ -0,0 +1,46 @@
{
"code": 200,
"msg": "成功",
"data": {
"gridHeaderDisplay": [
{ "columnCode": "electrodeNo", "columnNameChn": "电极号", "columnNameEng": "Electrode No", "columnWidth": 10, "columnSort": 1 },
{ "columnCode": "horizontalDistance", "columnNameChn": "平距", "columnNameEng": "Horizontal Distance", "columnWidth": 10, "columnSort": 2 },
{ "columnCode": "slopeDistance", "columnNameChn": "斜距", "columnNameEng": "Slope Distance", "columnWidth": 10, "columnSort": 3 },
{ "columnCode": "projectX", "columnNameChn": "经度/东坐标", "columnNameEng": "Longitude/EastCoordinate", "columnWidth": 10, "columnSort": 4 },
{ "columnCode": "projectY", "columnNameChn": "纬度/北坐标", "columnNameEng": "Latitude/NorthCoordinate", "columnWidth": 10, "columnSort": 5 },
{ "columnCode": "elevation", "columnNameChn": "高程", "columnNameEng": "Elevation", "columnWidth": 10, "columnSort": 6 },
{ "columnCode": "lineNo", "columnNameChn": "线号", "columnNameEng": "Line No", "columnWidth": 10, "columnSort": 7 },
{ "columnCode": "channelNo", "columnNameChn": "道号", "columnNameEng": "Channel No", "columnWidth": 10, "columnSort": 8 },
{ "columnCode": "measuredDistance", "columnNameChn": "里程", "columnNameEng": "Measured Distance", "columnWidth": 10, "columnSort": 9 },
{ "columnCode": "depth", "columnNameChn": "深度", "columnNameEng": "Depth", "columnWidth": 10, "columnSort": 10 },
{ "columnCode": "depthOfWater", "columnNameChn": "水深", "columnNameEng": "Depth of Water", "columnWidth": 10, "columnSort": 11 },
{ "columnCode": "date", "columnNameChn": "日期", "columnNameEng": "Date", "columnWidth": 10, "columnSort": 12 },
{ "columnCode": "time", "columnNameChn": "时间", "columnNameEng": "Time", "columnWidth": 10, "columnSort": 13 },
{ "columnCode": "resultStatus", "columnNameChn": "解状态", "columnNameEng": "Result Status", "columnWidth": 10, "columnSort": 14 }
],
"trajectoryDataModelHead": null,
"__rowTotal": 40,
"rowList": [
{ "id": "1438893959585792", "electrodeNo": 1, "horizontalDistance": 0, "slopeDistance": 0, "longitude": 0, "latitude": null, "projectX": 516838.884, "projectY": 2494251.836, "elevation": 26.172, "lineNo": "ert1", "channelNo": "ERT1-1", "measuredDistance": null, "depth": null, "depthOfWater": null, "date": null, "time": null, "resultStatus": "固定解" },
{ "id": "1438893959593984", "electrodeNo": 2, "horizontalDistance": 1.0801616223246715, "slopeDistance": 1.2459494894830485, "longitude": 0, "latitude": null, "projectX": 516840.099, "projectY": 2494252.112, "elevation": 25.551, "lineNo": "ert1", "channelNo": "ERT1-2", "measuredDistance": null, "depth": null, "depthOfWater": null, "date": null, "time": null, "resultStatus": "固定解" },
{ "id": "1438893959593985", "electrodeNo": 3, "horizontalDistance": 3.1498647133801647, "slopeDistance": 3.3194843662393243, "longitude": 0, "latitude": null, "projectX": 516842.07, "projectY": 2494252.756, "elevation": 25.425, "lineNo": "ert1", "channelNo": "ERT1-3", "measuredDistance": null, "depth": null, "depthOfWater": null, "date": null, "time": null, "resultStatus": "固定解" },
{ "id": "1438893959593986", "electrodeNo": 4, "horizontalDistance": 4.943098993517602, "slopeDistance": 5.123837209632222, "longitude": 0, "latitude": null, "projectX": 516843.874, "projectY": 2494252.792, "elevation": 25.625, "lineNo": "ert1", "channelNo": "ERT1-4", "measuredDistance": null, "depth": null, "depthOfWater": null, "date": null, "time": null, "resultStatus": "固定解" },
{ "id": "1438893959593987", "electrodeNo": 5, "horizontalDistance": 7.0998855833076515, "slopeDistance": 7.280646981934403, "longitude": 0, "latitude": null, "projectX": 516846.009, "projectY": 2494253.098, "elevation": 25.615, "lineNo": "ert1", "channelNo": "ERT1-5", "measuredDistance": null, "depth": null, "depthOfWater": null, "date": null, "time": null, "resultStatus": "固定解" },
{ "id": "1438893959593988", "electrodeNo": 6, "horizontalDistance": 8.912141076960786, "slopeDistance": 9.092942204649502, "longitude": 0, "latitude": null, "projectX": 516847.815, "projectY": 2494253.249, "elevation": 25.627, "lineNo": "ert1", "channelNo": "ERT1-6", "measuredDistance": null, "depth": null, "depthOfWater": null, "date": null, "time": null, "resultStatus": "固定解" },
{ "id": "1438893959593989", "electrodeNo": 7, "horizontalDistance": 11.045738049466081, "slopeDistance": 11.227964462398532, "longitude": 0, "latitude": null, "projectX": 516849.899, "projectY": 2494253.713, "elevation": 25.549, "lineNo": "ert1", "channelNo": "ERT1-7", "measuredDistance": null, "depth": null, "depthOfWater": null, "date": null, "time": null, "resultStatus": "固定解" },
{ "id": "1438893959593990", "electrodeNo": 8, "horizontalDistance": 13.093762113301716, "slopeDistance": 13.276838193512734, "longitude": 0, "latitude": null, "projectX": 516851.911, "projectY": 2494254.1, "elevation": 25.49, "lineNo": "ert1", "channelNo": "ERT1-8", "measuredDistance": null, "depth": null, "depthOfWater": null, "date": null, "time": null, "resultStatus": "固定解" },
{ "id": "1438893959593991", "electrodeNo": 9, "horizontalDistance": 15.033442059887646, "slopeDistance": 15.264042837022707, "longitude": 0, "latitude": null, "projectX": 516853.844, "projectY": 2494254.561, "elevation": 25.922, "lineNo": "ert1", "channelNo": "ERT1-9", "measuredDistance": null, "depth": null, "depthOfWater": null, "date": null, "time": null, "resultStatus": "固定解" },
{ "id": "1438893959593992", "electrodeNo": 10, "horizontalDistance": 16.955987211873353, "slopeDistance": 17.266206881606404, "longitude": 0, "latitude": null, "projectX": 516855.736, "projectY": 2494255.216, "elevation": 25.363, "lineNo": "ert1", "channelNo": "ERT1-10", "measuredDistance": null, "depth": null, "depthOfWater": null, "date": null, "time": null, "resultStatus": "固定解" },
{ "id": "1438893959593993", "electrodeNo": 11, "horizontalDistance": 18.929898218381553, "slopeDistance": 19.24013840565029, "longitude": 0, "latitude": null, "projectX": 516857.548, "projectY": 2494255.999, "elevation": 25.354, "lineNo": "ert1", "channelNo": "ERT1-11", "measuredDistance": null, "depth": null, "depthOfWater": null, "date": null, "time": null, "resultStatus": "固定解" },
{ "id": "1438893959593994", "electrodeNo": 12, "horizontalDistance": 20.971809019338927, "slopeDistance": 21.282366531758235, "longitude": 0, "latitude": null, "projectX": 516859.35, "projectY": 2494256.96, "elevation": 25.39, "lineNo": "ert1", "channelNo": "ERT1-12", "measuredDistance": null, "depth": null, "depthOfWater": null, "date": null, "time": null, "resultStatus": "固定解" },
{ "id": "1438893959593995", "electrodeNo": 13, "horizontalDistance": 22.92873162714211, "slopeDistance": 23.240034045135115, "longitude": 0, "latitude": null, "projectX": 516861.033, "projectY": 2494257.96, "elevation": 25.444, "lineNo": "ert1", "channelNo": "ERT1-13", "measuredDistance": null, "depth": null, "depthOfWater": null, "date": null, "time": null, "resultStatus": "固定解" },
{ "id": "1438893959593996", "electrodeNo": 14, "horizontalDistance": 24.816468622227266, "slopeDistance": 25.12811427717324, "longitude": 0, "latitude": null, "projectX": 516862.667, "projectY": 2494258.906, "elevation": 25.48, "lineNo": "ert1", "channelNo": "ERT1-14", "measuredDistance": null, "depth": null, "depthOfWater": null, "date": null, "time": null, "resultStatus": "固定解" },
{ "id": "1438893959593997", "electrodeNo": 15, "horizontalDistance": 26.92747540642802, "slopeDistance": 27.241302778689754, "longitude": 0, "latitude": null, "projectX": 516864.416, "projectY": 2494260.092, "elevation": 25.384, "lineNo": "ert1", "channelNo": "ERT1-15", "measuredDistance": null, "depth": null, "depthOfWater": null, "date": null, "time": null, "resultStatus": "固定解" },
{ "id": "1438893959593998", "electrodeNo": 16, "horizontalDistance": 28.68966923369523, "slopeDistance": 29.007310447092884, "longitude": 0, "latitude": null, "projectX": 516865.639, "projectY": 2494261.366, "elevation": 25.5, "lineNo": "ert1", "channelNo": "ERT1-16", "measuredDistance": null, "depth": null, "depthOfWater": null, "date": null, "time": null, "resultStatus": "固定解" },
{ "id": "1438893959593999", "electrodeNo": 17, "horizontalDistance": 30.880151207446215, "slopeDistance": 31.19815760682996, "longitude": 0, "latitude": null, "projectX": 516867.233, "projectY": 2494262.869, "elevation": 25.54, "lineNo": "ert1", "channelNo": "ERT1-17", "measuredDistance": null, "depth": null, "depthOfWater": null, "date": null, "time": null, "resultStatus": "固定解" },
{ "id": "1438893959594000", "electrodeNo": 18, "horizontalDistance": 32.7659271758147, "slopeDistance": 33.084258347213705, "longitude": 0, "latitude": null, "projectX": 516868.457, "projectY": 2494264.304, "elevation": 25.505, "lineNo": "ert1", "channelNo": "ERT1-18", "measuredDistance": null, "depth": null, "depthOfWater": null, "date": null, "time": null, "resultStatus": "固定解" },
{ "id": "1438893959594001", "electrodeNo": 19, "horizontalDistance": 34.71171810948364, "slopeDistance": 35.03134421012479, "longitude": 0, "latitude": null, "projectX": 516869.61, "projectY": 2494265.873, "elevation": 25.576, "lineNo": "ert1", "channelNo": "ERT1-19", "measuredDistance": null, "depth": null, "depthOfWater": null, "date": null, "time": null, "resultStatus": "固定解" },
{ "id": "1438893959594002", "electrodeNo": 20, "horizontalDistance": 36.644544469129066, "slopeDistance": 36.96419152342716, "longitude": 0, "latitude": null, "projectX": 516870.684, "projectY": 2494267.48, "elevation": 25.567, "lineNo": "ert1", "channelNo": "ERT1-20", "measuredDistance": null, "depth": null, "depthOfWater": null, "date": null, "time": null, "resultStatus": "固定解" }
]
}
}