feat/dataset-detail-chart #5
|
|
@ -6,8 +6,9 @@ add_library(geopro_data STATIC
|
||||||
dto/NavDto.cpp
|
dto/NavDto.cpp
|
||||||
dto/DatasetChartDto.cpp
|
dto/DatasetChartDto.cpp
|
||||||
api/ApiProjectRepository.cpp
|
api/ApiProjectRepository.cpp
|
||||||
api/ApiDatasetRepository.cpp)
|
api/ApiDatasetRepository.cpp
|
||||||
|
api/DatasetLoadHandles.cpp)
|
||||||
target_include_directories(geopro_data PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
|
target_include_directories(geopro_data PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
|
||||||
target_link_libraries(geopro_data PUBLIC geopro_core geopro_net Qt6::Core PRIVATE nlohmann_json::nlohmann_json)
|
target_link_libraries(geopro_data PUBLIC geopro_core geopro_net Qt6::Core PRIVATE nlohmann_json::nlohmann_json)
|
||||||
target_compile_features(geopro_data PUBLIC cxx_std_17)
|
target_compile_features(geopro_data PUBLIC cxx_std_17)
|
||||||
set_target_properties(geopro_data PROPERTIES AUTOMOC OFF AUTOUIC OFF AUTORCC OFF)
|
set_target_properties(geopro_data PROPERTIES AUTOMOC ON AUTOUIC OFF AUTORCC OFF)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,66 @@
|
||||||
|
#include "api/DatasetLoadHandles.hpp"
|
||||||
|
#include <stdexcept>
|
||||||
|
|
||||||
|
namespace geopro::data {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
QString reasonOf(const geopro::net::ApiResponse& r) {
|
||||||
|
return r.msg.isEmpty() ? r.rawError : r.msg;
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
ApiChartLoad::ApiChartLoad(geopro::net::ApiBatch* batch, Parser parse, QObject* parent)
|
||||||
|
: ChartLoad(parent), batch_(batch), parse_(std::move(parse)) {
|
||||||
|
QObject::connect(batch, &geopro::net::ApiBatch::succeeded, this,
|
||||||
|
[this](const QList<geopro::net::ApiResponse>& resps) {
|
||||||
|
if (aborted_) return; // §5.0
|
||||||
|
try {
|
||||||
|
emit done(parse_(resps));
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
emit failed(QString::fromUtf8(e.what()));
|
||||||
|
}
|
||||||
|
deleteLater();
|
||||||
|
});
|
||||||
|
QObject::connect(batch, &geopro::net::ApiBatch::failed, this,
|
||||||
|
[this](int, const geopro::net::ApiResponse& r) {
|
||||||
|
if (aborted_) return;
|
||||||
|
emit failed(reasonOf(r));
|
||||||
|
deleteLater();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void ApiChartLoad::abort() {
|
||||||
|
if (aborted_) return;
|
||||||
|
aborted_ = true;
|
||||||
|
if (batch_) batch_->abort();
|
||||||
|
deleteLater();
|
||||||
|
}
|
||||||
|
|
||||||
|
ApiGridLoad::ApiGridLoad(geopro::net::ApiBatch* batch, Parser parse, QObject* parent)
|
||||||
|
: GridLoad(parent), batch_(batch), parse_(std::move(parse)) {
|
||||||
|
QObject::connect(batch, &geopro::net::ApiBatch::succeeded, this,
|
||||||
|
[this](const QList<geopro::net::ApiResponse>& resps) {
|
||||||
|
if (aborted_) return;
|
||||||
|
try {
|
||||||
|
emit done(parse_(resps));
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
emit failed(QString::fromUtf8(e.what()));
|
||||||
|
}
|
||||||
|
deleteLater();
|
||||||
|
});
|
||||||
|
QObject::connect(batch, &geopro::net::ApiBatch::failed, this,
|
||||||
|
[this](int, const geopro::net::ApiResponse& r) {
|
||||||
|
if (aborted_) return;
|
||||||
|
emit failed(reasonOf(r));
|
||||||
|
deleteLater();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void ApiGridLoad::abort() {
|
||||||
|
if (aborted_) return;
|
||||||
|
aborted_ = true;
|
||||||
|
if (batch_) batch_->abort();
|
||||||
|
deleteLater();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace geopro::data
|
||||||
|
|
@ -0,0 +1,60 @@
|
||||||
|
#pragma once
|
||||||
|
#include <functional>
|
||||||
|
#include <QList>
|
||||||
|
#include <QObject>
|
||||||
|
#include <QPointer>
|
||||||
|
#include <QString>
|
||||||
|
#include "ApiBatch.hpp"
|
||||||
|
#include "DatasetLoads.hpp"
|
||||||
|
|
||||||
|
namespace geopro::data {
|
||||||
|
|
||||||
|
// ── 抽象句柄(可测试缝,类比 IApiCall):仓储返回基类指针,控制器/测试只依赖它 ──
|
||||||
|
class ChartLoad : public QObject {
|
||||||
|
Q_OBJECT
|
||||||
|
public:
|
||||||
|
using QObject::QObject;
|
||||||
|
~ChartLoad() override = default;
|
||||||
|
virtual void abort() = 0;
|
||||||
|
signals:
|
||||||
|
void done(const geopro::data::ChartParts& parts);
|
||||||
|
void failed(const QString& message);
|
||||||
|
};
|
||||||
|
|
||||||
|
class GridLoad : public QObject {
|
||||||
|
Q_OBJECT
|
||||||
|
public:
|
||||||
|
using QObject::QObject;
|
||||||
|
~GridLoad() override = default;
|
||||||
|
virtual void abort() = 0;
|
||||||
|
signals:
|
||||||
|
void done(const geopro::data::GridParts& parts);
|
||||||
|
void failed(const QString& message);
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── Api 实现:包一个 ApiBatch + 注入的解析函数。batch.succeeded→解析→done;failed→failed ──
|
||||||
|
class ApiChartLoad : public ChartLoad {
|
||||||
|
Q_OBJECT
|
||||||
|
public:
|
||||||
|
using Parser = std::function<ChartParts(const QList<geopro::net::ApiResponse>&)>;
|
||||||
|
ApiChartLoad(geopro::net::ApiBatch* batch, Parser parse, QObject* parent = nullptr);
|
||||||
|
void abort() override;
|
||||||
|
private:
|
||||||
|
QPointer<geopro::net::ApiBatch> batch_;
|
||||||
|
Parser parse_;
|
||||||
|
bool aborted_ = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
class ApiGridLoad : public GridLoad {
|
||||||
|
Q_OBJECT
|
||||||
|
public:
|
||||||
|
using Parser = std::function<GridParts(const QList<geopro::net::ApiResponse>&)>;
|
||||||
|
ApiGridLoad(geopro::net::ApiBatch* batch, Parser parse, QObject* parent = nullptr);
|
||||||
|
void abort() override;
|
||||||
|
private:
|
||||||
|
QPointer<geopro::net::ApiBatch> batch_;
|
||||||
|
Parser parse_;
|
||||||
|
bool aborted_ = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace geopro::data
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
#pragma once
|
||||||
|
#include <vector>
|
||||||
|
#include "model/Field.hpp"
|
||||||
|
#include "model/ColorScale.hpp"
|
||||||
|
#include "model/Anomaly.hpp"
|
||||||
|
|
||||||
|
namespace geopro::data {
|
||||||
|
|
||||||
|
// 原数据加载结果:scatter + 散点色阶(type1)。
|
||||||
|
struct ChartParts {
|
||||||
|
geopro::core::ScatterField scatter;
|
||||||
|
geopro::core::ColorScale scatterScale;
|
||||||
|
};
|
||||||
|
// 网格数据加载结果:grid(rows) + 网格色阶(type2) + 异常。
|
||||||
|
struct GridParts {
|
||||||
|
geopro::core::Grid grid{1, 1}; // Grid 无默认构造;占位
|
||||||
|
geopro::core::ColorScale gridScale;
|
||||||
|
std::vector<geopro::core::Anomaly> anomalies;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace geopro::data
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
#pragma once
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace geopro::data {
|
||||||
|
|
||||||
|
class ChartLoad;
|
||||||
|
class GridLoad;
|
||||||
|
|
||||||
|
// 数据集详情异步仓储抽象。返回自管理句柄(完成/失败后 deleteLater)。
|
||||||
|
class IAsyncDatasetRepository {
|
||||||
|
public:
|
||||||
|
virtual ~IAsyncDatasetRepository() = default;
|
||||||
|
virtual ChartLoad* loadChartAsync(const std::string& dsId) = 0; // scatter + 散点色阶(type1)
|
||||||
|
virtual GridLoad* loadGridAsync(const std::string& dsId) = 0; // grid(rows) + 色阶(type2) + 异常
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace geopro::data
|
||||||
|
|
@ -38,6 +38,7 @@ target_sources(geopro_tests PRIVATE data/test_parsers.cpp)
|
||||||
target_sources(geopro_tests PRIVATE data/test_local_repo.cpp)
|
target_sources(geopro_tests PRIVATE data/test_local_repo.cpp)
|
||||||
target_sources(geopro_tests PRIVATE data/test_nav_dto.cpp)
|
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_dataset_chart_dto.cpp)
|
||||||
|
target_sources(geopro_tests PRIVATE data/test_dataset_load_handles.cpp)
|
||||||
target_link_libraries(geopro_tests PRIVATE geopro_data)
|
target_link_libraries(geopro_tests PRIVATE geopro_data)
|
||||||
|
|
||||||
# net 层:RSA 加密器。测试需直接用 OpenSSL 生成/解密密钥,故显式 find_package
|
# net 层:RSA 加密器。测试需直接用 OpenSSL 生成/解密密钥,故显式 find_package
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,51 @@
|
||||||
|
#include <gtest/gtest.h>
|
||||||
|
#include <QSignalSpy>
|
||||||
|
#include "api/DatasetLoadHandles.hpp"
|
||||||
|
#include "net/FakeApiCall.hpp"
|
||||||
|
|
||||||
|
using namespace geopro::data;
|
||||||
|
using geopro::net::ApiBatch;
|
||||||
|
using geopro::net::ApiResponse;
|
||||||
|
using geopro::net::test::FakeApiCall;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
ApiResponse ok() { ApiResponse r; r.code = 200; r.httpStatus = 200; return r; }
|
||||||
|
ApiResponse bad() { ApiResponse r; r.code = 500; r.httpStatus = 200; r.msg = QStringLiteral("boom"); return r; }
|
||||||
|
auto isFailure = [](const ApiResponse& r) { return r.code != 200 || !r.rawError.isEmpty(); };
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(DatasetLoadHandles, ChartLoadEmitsDoneOnSuccess) {
|
||||||
|
auto* a = new FakeApiCall;
|
||||||
|
auto* b = new FakeApiCall;
|
||||||
|
auto* batch = new ApiBatch({a, b}, isFailure);
|
||||||
|
bool parsed = false;
|
||||||
|
auto* load = new ApiChartLoad(batch, [&](const QList<ApiResponse>& resps) {
|
||||||
|
parsed = (resps.size() == 2);
|
||||||
|
return ChartParts{};
|
||||||
|
});
|
||||||
|
QSignalSpy doneSpy(load, &ChartLoad::done);
|
||||||
|
a->fire(ok());
|
||||||
|
b->fire(ok());
|
||||||
|
EXPECT_EQ(doneSpy.count(), 1);
|
||||||
|
EXPECT_TRUE(parsed);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(DatasetLoadHandles, ChartLoadEmitsFailedOnBatchFailure) {
|
||||||
|
auto* a = new FakeApiCall;
|
||||||
|
auto* b = new FakeApiCall;
|
||||||
|
auto* batch = new ApiBatch({a, b}, isFailure);
|
||||||
|
auto* load = new ApiChartLoad(batch, [](const QList<ApiResponse>&) { return ChartParts{}; });
|
||||||
|
QSignalSpy failSpy(load, &ChartLoad::failed);
|
||||||
|
a->fire(bad());
|
||||||
|
EXPECT_EQ(failSpy.count(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(DatasetLoadHandles, GridLoadAbortSuppressesLateDone) {
|
||||||
|
auto* a = new FakeApiCall;
|
||||||
|
auto* batch = new ApiBatch({a}, isFailure);
|
||||||
|
auto* load = new ApiGridLoad(batch, [](const QList<ApiResponse>&) { return GridParts{}; });
|
||||||
|
QSignalSpy doneSpy(load, &GridLoad::done);
|
||||||
|
load->abort();
|
||||||
|
a->fire(ok()); // 迟到
|
||||||
|
EXPECT_EQ(doneSpy.count(), 0); // batch.aborted_ + load.aborted_ 双闸门
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue