1276 lines
80 KiB
C++
1276 lines
80 KiB
C++
#include "Adminive_Config.h"
|
||
#include "Global.h"
|
||
#include "adminive/adminive.hpp"
|
||
#include "adminive/adapters/drogon.hpp"
|
||
#include "adminive/adapters/magic_enum.hpp"
|
||
#include "adminive/adapters/nlohmann_json.hpp"
|
||
#include <algorithm>
|
||
#include <charconv>
|
||
#include <cmath>
|
||
#include <cstdint>
|
||
#include <map>
|
||
#include <memory>
|
||
#include <mutex>
|
||
#include <stdexcept>
|
||
#include <string>
|
||
#include <utility>
|
||
#include <vector>
|
||
namespace ecap::adminive_config {
|
||
enum class Data_Source_Type {
|
||
Serial_Data_Source,
|
||
TCP_Client_Data_Source,
|
||
File_Data_Source,
|
||
Dll_Data_Source,
|
||
Shared_Memory_Data_Source
|
||
};
|
||
enum class Data_Feed_Type {
|
||
Data_Feed_TCP_Server,
|
||
Data_Feed_UDP_Server,
|
||
Data_Feed_TCP_Client,
|
||
Data_Feed_UDP_Client
|
||
};
|
||
struct Data_Source_Row {
|
||
std::string key;
|
||
Data_Source_Type type = Data_Source_Type::TCP_Client_Data_Source;
|
||
bool enable{};
|
||
Data_Source_Data config;
|
||
std::string ip;
|
||
std::uint16_t port{};
|
||
std::string port_name;
|
||
Baud_Rate_Type baud_rate{};
|
||
std::string file_path;
|
||
File_Data_Type data_type = File_Data_Type::BIN_Blank_Text;
|
||
Play_Mode play_mode = Play_Mode::one;
|
||
std::string library_path;
|
||
std::string function_name;
|
||
std::size_t buffer_size{};
|
||
std::string shared_memory_name;
|
||
std::uint64_t shared_memory_size{};
|
||
std::string state;
|
||
};
|
||
struct Data_Feed_Row {
|
||
std::string key;
|
||
Data_Feed_Type type = Data_Feed_Type::Data_Feed_TCP_Server;
|
||
bool enable{};
|
||
std::string source_key;
|
||
Output_Format output_format;
|
||
Output_Data_Format format = Output_Data_Format::BIN;
|
||
std::uint16_t port{};
|
||
std::string url;
|
||
std::size_t connect_user_buffer_size = 409600;
|
||
std::size_t connect_system_buffer_size = 409600;
|
||
std::string state;
|
||
};
|
||
struct External_Resource_Row {
|
||
std::string name;
|
||
std::string description;
|
||
std::string download_url;
|
||
std::uint64_t update_interval_seconds{};
|
||
std::int64_t last_updated_at{};
|
||
std::size_t row_count{};
|
||
std::string state;
|
||
};
|
||
}
|
||
namespace adminive {
|
||
template <>
|
||
struct Type_Descriptor<ecap::adminive_config::External_Resource_Row> {
|
||
static auto get() {
|
||
using T = ecap::adminive_config::External_Resource_Row;
|
||
return object<T>("external_resource", "外部数据源管理",
|
||
ADMINIVE_FIELD_LABEL(T, name, "数据表"),
|
||
ADMINIVE_FIELD_LABEL(T, description, "说明"),
|
||
ADMINIVE_FIELD_LABEL(T, download_url, "在线来源"),
|
||
ADMINIVE_FIELD_LABEL(T, update_interval_seconds, "建议更新周期(秒)"),
|
||
ADMINIVE_FIELD_LABEL(T, last_updated_at, "上次更新时间"),
|
||
ADMINIVE_FIELD_LABEL(T, row_count, "行数"),
|
||
ADMINIVE_FIELD_LABEL(T, state, "状态"));
|
||
}
|
||
};
|
||
template <>
|
||
struct Type_View_Descriptor<ecap::adminive_config::External_Resource_Row> {
|
||
static Table_View table() {
|
||
using T = ecap::adminive_config::External_Resource_Row;
|
||
return table_view<T>(column<&T::name>("数据表").search(), column<&T::description>("说明"), column<&T::download_url>("在线来源"), column<&T::row_count>("行数").sort(), column<&T::last_updated_at>("上次更新时间").sort(), column<&T::update_interval_seconds>("建议更新周期(秒)"), column<&T::state>("状态")).titled("外部数据源管理");
|
||
}
|
||
};
|
||
template <>
|
||
struct Type_Descriptor<ecap::adminive_config::Data_Source_Row> {
|
||
static auto get() {
|
||
using T = ecap::adminive_config::Data_Source_Row;
|
||
using E = ecap::adminive_config::Data_Source_Type;
|
||
auto type = ADMINIVE_FIELD_LABEL(T, type, "数据源类型").creatable().required().select_input();
|
||
type = type.enum_label<E::Serial_Data_Source>("串口数据源")
|
||
.enum_label<E::TCP_Client_Data_Source>("TCP 客户端数据源")
|
||
.enum_label<E::File_Data_Source>("文件数据源")
|
||
.enum_label<E::Dll_Data_Source>("DLL 数据源")
|
||
.enum_label<E::Shared_Memory_Data_Source>("共享内存数据源");
|
||
return object<T>("data_source", "数据源",
|
||
ADMINIVE_FIELD_LABEL(T, key, "名称").creatable().required().text_input(),
|
||
type,
|
||
ADMINIVE_FIELD_LABEL(T, enable, "启用").creatable().editable().boolean_input(),
|
||
ADMINIVE_FIELD_LABEL(T, config, "公共与地图配置").creatable().editable(),
|
||
ADMINIVE_FIELD_LABEL(T, ip, "IP 地址").creatable().editable().text_input().visible_on("${$self.type == 'TCP_Client_Data_Source'}"),
|
||
ADMINIVE_FIELD_LABEL(T, port, "端口").creatable().editable().number_input().visible_on("${$self.type == 'TCP_Client_Data_Source'}"),
|
||
ADMINIVE_FIELD_LABEL(T, port_name, "串口名称").creatable().editable().text_input().visible_on("${$self.type == 'Serial_Data_Source'}"),
|
||
ADMINIVE_FIELD_LABEL(T, baud_rate, "波特率").creatable().editable().select_input().visible_on("${$self.type == 'Serial_Data_Source'}"),
|
||
ADMINIVE_FIELD_LABEL(T, file_path, "文件路径").creatable().editable().text_input().visible_on("${$self.type == 'File_Data_Source'}"),
|
||
ADMINIVE_FIELD_LABEL(T, data_type, "数据格式").creatable().editable().select_input().visible_on("${$self.type == 'File_Data_Source' || $self.type == 'Dll_Data_Source' || $self.type == 'Shared_Memory_Data_Source'}"),
|
||
ADMINIVE_FIELD_LABEL(T, play_mode, "播放模式").creatable().editable().select_input().visible_on("${$self.type == 'File_Data_Source'}"),
|
||
ADMINIVE_FIELD_LABEL(T, library_path, "库路径").creatable().editable().text_input().visible_on("${$self.type == 'Dll_Data_Source'}"),
|
||
ADMINIVE_FIELD_LABEL(T, function_name, "函数名").creatable().editable().text_input().visible_on("${$self.type == 'Dll_Data_Source'}"),
|
||
ADMINIVE_FIELD_LABEL(T, buffer_size, "读取缓冲区大小").creatable().editable().number_input().visible_on("${$self.type == 'Dll_Data_Source'}"),
|
||
ADMINIVE_FIELD_LABEL(T, shared_memory_name, "共享内存名称").creatable().editable().text_input().visible_on("${$self.type == 'Shared_Memory_Data_Source'}"),
|
||
ADMINIVE_FIELD_LABEL(T, shared_memory_size, "共享内存大小").creatable().editable().number_input().visible_on("${$self.type == 'Shared_Memory_Data_Source'}"),
|
||
ADMINIVE_FIELD_LABEL(T, state, "运行状态").text_input());
|
||
}
|
||
};
|
||
template <>
|
||
struct Type_View_Descriptor<ecap::adminive_config::Data_Source_Row> {
|
||
static Table_View table() {
|
||
using T = ecap::adminive_config::Data_Source_Row;
|
||
return table_view<T>(column<&T::key>("名称").search(), column<&T::type>("类型").filter(), column<&T::enable>("启用").filter(), column<&T::state>("状态"))
|
||
.titled("数据源")
|
||
.id_title("序号")
|
||
.create_title("添加数据源")
|
||
.edit_title("编辑")
|
||
.delete_title("删除")
|
||
.delete_confirmation("确定删除该数据源?仍有数据馈送连接时后端会拒绝删除。")
|
||
.reorderable()
|
||
.create_layout(vertical(grid(2, use<&T::key>(), use<&T::type>(), use<&T::enable>()), use<&T::config>(), grid(2, use<&T::ip>(), use<&T::port>(), use<&T::port_name>(), use<&T::baud_rate>(), use<&T::file_path>(), use<&T::data_type>(), use<&T::play_mode>(), use<&T::library_path>(), use<&T::function_name>(), use<&T::buffer_size>(), use<&T::shared_memory_name>(), use<&T::shared_memory_size>())))
|
||
.edit_layout(vertical(grid(2, use<&T::key>(), use<&T::type>(), use<&T::enable>(), use<&T::state>()), use<&T::config>(), grid(2, use<&T::ip>(), use<&T::port>(), use<&T::port_name>(), use<&T::baud_rate>(), use<&T::file_path>(), use<&T::data_type>(), use<&T::play_mode>(), use<&T::library_path>(), use<&T::function_name>(), use<&T::buffer_size>(), use<&T::shared_memory_name>(), use<&T::shared_memory_size>())));
|
||
}
|
||
};
|
||
template <>
|
||
struct Type_Descriptor<ecap::adminive_config::Data_Feed_Row> {
|
||
static auto get() {
|
||
using T = ecap::adminive_config::Data_Feed_Row;
|
||
using E = ecap::adminive_config::Data_Feed_Type;
|
||
auto type = ADMINIVE_FIELD_LABEL(T, type, "数据馈送类型").creatable().required().select_input();
|
||
type = type.enum_label<E::Data_Feed_TCP_Server>("TCP 服务端")
|
||
.enum_label<E::Data_Feed_UDP_Server>("UDP 服务端")
|
||
.enum_label<E::Data_Feed_TCP_Client>("TCP 客户端")
|
||
.enum_label<E::Data_Feed_UDP_Client>("UDP 客户端");
|
||
return object<T>("data_feed", "数据馈送",
|
||
ADMINIVE_FIELD_LABEL(T, key, "名称").creatable().required().text_input(),
|
||
type,
|
||
ADMINIVE_FIELD_LABEL(T, enable, "启用").creatable().editable().boolean_input(),
|
||
ADMINIVE_FIELD_LABEL(T, source_key, "连接的数据源").creatable().editable().select_input(),
|
||
ADMINIVE_FIELD_LABEL(T, output_format, "输出格式").creatable().editable(),
|
||
ADMINIVE_FIELD_LABEL(T, format, "数据格式").select_input(),
|
||
ADMINIVE_FIELD_LABEL(T, port, "端口").creatable().editable().number_input(),
|
||
ADMINIVE_FIELD_LABEL(T, url, "IP 地址").creatable().editable().text_input().visible_on("${$self.type == 'Data_Feed_TCP_Client' || $self.type == 'Data_Feed_UDP_Client'}"),
|
||
ADMINIVE_FIELD_LABEL(T, connect_user_buffer_size, "TCP 用户缓冲区").creatable().editable().number_input().visible_on("${$self.type == 'Data_Feed_TCP_Server'}"),
|
||
ADMINIVE_FIELD_LABEL(T, connect_system_buffer_size, "TCP 系统缓冲区").creatable().editable().number_input().visible_on("${$self.type == 'Data_Feed_TCP_Server'}"),
|
||
ADMINIVE_FIELD_LABEL(T, state, "运行状态").text_input());
|
||
}
|
||
};
|
||
template <>
|
||
struct Type_View_Descriptor<ecap::adminive_config::Data_Feed_Row> {
|
||
static Table_View table() {
|
||
using T = ecap::adminive_config::Data_Feed_Row;
|
||
return table_view<T>(column<&T::key>("名称").search(), column<&T::type>("类型").filter(), column<&T::source_key>("连接的数据源").filter(), column<&T::format>("输出格式").filter(), column<&T::port>("端口").sort(), column<&T::enable>("启用").filter(), column<&T::state>("状态"))
|
||
.titled("数据馈送")
|
||
.id_title("序号")
|
||
.create_title("添加数据馈送")
|
||
.edit_title("编辑")
|
||
.delete_title("删除")
|
||
.delete_confirmation("确定删除该数据馈送?对应拓扑连接会一并删除。")
|
||
.reorderable()
|
||
.create_layout(vertical(grid(2, use<&T::key>(), use<&T::type>(), use<&T::enable>(), use<&T::source_key>()), use<&T::output_format>(), grid(2, use<&T::url>(), use<&T::port>(), use<&T::connect_user_buffer_size>(), use<&T::connect_system_buffer_size>())))
|
||
.edit_layout(vertical(grid(2, use<&T::key>(), use<&T::type>(), use<&T::enable>(), use<&T::source_key>(), use<&T::state>()), use<&T::output_format>(), grid(2, use<&T::url>(), use<&T::port>(), use<&T::connect_user_buffer_size>(), use<&T::connect_system_buffer_size>())));
|
||
}
|
||
};
|
||
template <>
|
||
struct Type_View_Descriptor<Mode_ACS_Config_Data> {
|
||
static Form_View edit() {
|
||
using T = Mode_ACS_Config_Data;
|
||
return edit_form<T>(vertical(
|
||
group("基础时序", grid(2, use<&T::timeout_seconds>(), use<&T::air_pos_timeout>(), use<&T::surface_pos_timeout>())),
|
||
group("过滤参数", grid(2, use<&T::max_speed_m_s>(), use<&T::time_space_filter>(), use<&T::speed_filter>(), use<&T::aircraft_change_list_adsb_range_filter>(), use<&T::aircraft_change_list_adsb_range_factor>(), use<&T::adsb_theoretical_target_altitude_meters>())),
|
||
group("容量与上报", grid(2, use<&T::max_track_point_size>(), use<&T::mode_s_max_num>(), use<&T::mode_other_max_num>(), use<&T::report_data_feed_msg_mum>(), use<&T::default_min_aircraft_list_num>(), use<&T::aircraft_change_list_min_position_points>())),
|
||
group("运行开关", grid(2, use<&T::wait_process_msg>(), use<&T::monitor_msg_live>())))).submit("保存 Mode ACS 配置");
|
||
}
|
||
};
|
||
template <>
|
||
struct Type_View_Descriptor<Cesium_Graphics_Config_Data> {
|
||
static Form_View edit() {
|
||
using T = Cesium_Graphics_Config_Data;
|
||
return edit_form<T>(vertical(
|
||
group("渲染质量", grid(2, use<&T::debug_show_frames_per_second>(), use<&T::target_frame_rate>(), use<&T::use_browser_recommended_resolution>(), use<&T::resolution_scale>(), use<&T::msaa_samples>(), use<&T::fxaa>(), use<&T::maximum_screen_space_error>())),
|
||
group("光照与阴影", grid(2, use<&T::shadows>(), use<&T::enable_lighting>(), use<&T::solar_lighting>(), use<&T::solar_light_intensity>())),
|
||
group("地形", grid(2, use<&T::terrain_enabled_in_3d>(), use<&T::terrain_exaggeration>(), use<&T::terrain_limit_maximum_level>(), use<&T::terrain_maximum_level>(), use<&T::terrain_cache_tiles>(), use<&T::terrain_occlusion_enabled>(), use<&T::occlusion_sample_spacing_meters>(), use<&T::occlusion_clearance_margin_meters>())),
|
||
group("辅助显示", grid(2, use<&T::surface_navigation_reference_visible>(), use<&T::view_axes_visible>(), use<&T::performance_panel_x>(), use<&T::performance_panel_y>(), use<&T::view_axes_panel_x>(), use<&T::view_axes_panel_y>(), use<&T::view_axes_panel_size>())))).submit("保存 Cesium 图形配置");
|
||
}
|
||
};
|
||
template <>
|
||
struct Type_View_Descriptor<Data_feed_Config_Data> {
|
||
static Form_View edit() {
|
||
using T = Data_feed_Config_Data;
|
||
return edit_form<T>(grid(2, use<&T::packet_byte_size>())).submit("保存数据馈送全局配置");
|
||
}
|
||
};
|
||
template <>
|
||
struct Type_View_Descriptor<MLAT_Config_Data> {
|
||
static Form_View edit() {
|
||
using T = MLAT_Config_Data;
|
||
return edit_form<T>(grid(2, use<&T::merge>())).submit("保存 MLAT 配置");
|
||
}
|
||
};
|
||
}
|
||
namespace ecap::adminive_config {
|
||
namespace {
|
||
using Json = nlohmann::json;
|
||
const std::string data_topology_path = "/api/adminive/config/data_topology";
|
||
class Conflict_Error final : public std::runtime_error {
|
||
public:
|
||
using std::runtime_error::runtime_error;
|
||
};
|
||
class Bad_Request_Error final : public std::runtime_error {
|
||
public:
|
||
using std::runtime_error::runtime_error;
|
||
};
|
||
template <class Model>
|
||
adminive::Resource_Transaction<Model> value_section_transaction(Config_Section section) {
|
||
adminive::Resource_Transaction<Model> transaction;
|
||
transaction.commit = [section](const Model& value, const adminive::Request_Context&) {
|
||
Config::save(section, value.to_base_json());
|
||
};
|
||
transaction.rollback = transaction.commit;
|
||
return transaction;
|
||
}
|
||
adminive::Resource_Transaction<Mode_ACS_Config_Data> mode_acs_transaction() {
|
||
adminive::Resource_Transaction<Mode_ACS_Config_Data> transaction;
|
||
const auto persist = [](const Mode_ACS_Config_Data& value, const adminive::Request_Context&) {
|
||
SSR::Monitor_Mode_ACS_Message_Num = value.monitor_msg_live;
|
||
Config::merge(Config_Section::mode_acs, value.to_base_json());
|
||
};
|
||
transaction.commit = persist;
|
||
transaction.rollback = persist;
|
||
return transaction;
|
||
}
|
||
adminive::Resource_Transaction<Data_feed_Config_Data> data_feed_settings_transaction() {
|
||
adminive::Resource_Transaction<Data_feed_Config_Data> transaction;
|
||
const auto persist = [](const Data_feed_Config_Data& value, const adminive::Request_Context&) {
|
||
auto patch = Psc::JSON::object();
|
||
patch.append({"data_feed", value.to_base_json()});
|
||
Config::merge(Config_Section::mode_acs, patch);
|
||
};
|
||
transaction.commit = persist;
|
||
transaction.rollback = persist;
|
||
return transaction;
|
||
}
|
||
adminive::Resource_Transaction<Map_View_Config> map_view_transaction(Global* global) {
|
||
adminive::Resource_Transaction<Map_View_Config> transaction;
|
||
transaction.prepare = [global](const Map_View_Config& value, const adminive::Request_Context&) {
|
||
if(!Map_View_Config::is_scene_mode(value.scene_mode))
|
||
throw adminive::Field_Validation_Error("scene_mode", "场景模式必须为 2d 或 3d");
|
||
global->map_resources_config.read([&](const auto& resources) {
|
||
if(!resources.is_imagery_key(value.map2d.current_imagery_key))
|
||
throw adminive::Field_Validation_Error("map2d.current_imagery_key", "影像源不存在");
|
||
if(!resources.is_terrain_key(value.map2d.current_terrain_key))
|
||
throw adminive::Field_Validation_Error("map2d.current_terrain_key", "地形源不存在");
|
||
if(!resources.is_imagery_key(value.map3d.current_imagery_key))
|
||
throw adminive::Field_Validation_Error("map3d.current_imagery_key", "影像源不存在");
|
||
if(!resources.is_terrain_key(value.map3d.current_terrain_key))
|
||
throw adminive::Field_Validation_Error("map3d.current_terrain_key", "地形源不存在");
|
||
});
|
||
};
|
||
const auto persist = [global](const Map_View_Config& value, const adminive::Request_Context&) {
|
||
global->map_resources_config.write([&](auto& resources) {
|
||
resources.current_imagery_key = value.scene_mode == "2d" ? value.map2d.current_imagery_key : value.map3d.current_imagery_key;
|
||
});
|
||
Config::save(Config_Section::map_view, value.to_base_json());
|
||
Config::save(Config_Section::map_resources);
|
||
};
|
||
transaction.commit = persist;
|
||
transaction.rollback = persist;
|
||
return transaction;
|
||
}
|
||
void save_data_sources(Global* global) {
|
||
Config::save(Config_Section::mode_acs, global->mode_acs.to_base_json());
|
||
}
|
||
void save_data_feeds(Global* global) {
|
||
Config::save(Config_Section::mode_acs, global->mode_acs.to_base_json());
|
||
}
|
||
template <class Function>
|
||
adminive::Http_Response<Json> business_response(Function&& function) noexcept {
|
||
try {
|
||
return std::forward<Function>(function)();
|
||
} catch(const Bad_Request_Error& error) {
|
||
return adminive::make_http_error<Json>(400, error.what());
|
||
} catch(const Conflict_Error& error) {
|
||
return adminive::make_http_error<Json>(409, error.what());
|
||
} catch(const std::out_of_range& error) {
|
||
return adminive::make_http_error<Json>(404, error.what());
|
||
} catch(const std::invalid_argument& error) {
|
||
return adminive::make_http_error<Json>(422, error.what());
|
||
} catch(const std::exception& error) {
|
||
return adminive::make_http_error<Json>(500, error.what());
|
||
} catch(...) {
|
||
return adminive::make_http_error<Json>(500, "unknown server error");
|
||
}
|
||
}
|
||
Json read_request_body(std::string_view body) {
|
||
try {
|
||
auto input = adminive::parse_json<Json>(body);
|
||
adminive::Json_Adapter<Json>::erase(input, "id");
|
||
return input;
|
||
} catch(const std::exception& error) {
|
||
throw Bad_Request_Error(error.what());
|
||
}
|
||
}
|
||
Data_Topology_View_Config read_topology_view(Json input, Global* global) {
|
||
if(input.value("protocol", std::string{}) != "ecap.data-topology-view" || input.value("protocol_version", 0) != 1)
|
||
throw std::invalid_argument("数据拓扑视图协议或版本不受支持");
|
||
const auto finite_number = [&input](std::string_view name) {
|
||
const auto field = input.find(std::string(name));
|
||
if(field == input.end() || !field->is_number())
|
||
throw std::invalid_argument("数据拓扑视图缺少数值字段: " + std::string(name));
|
||
const auto value = field->get<double>();
|
||
if(!std::isfinite(value))
|
||
throw std::invalid_argument("数据拓扑视图字段不是有限数值: " + std::string(name));
|
||
return value;
|
||
};
|
||
Data_Topology_View_Config result;
|
||
result.initialized = true;
|
||
result.layout_revision = input.value("layout_revision", 0U);
|
||
result.zoom = finite_number("zoom");
|
||
result.pan_x = finite_number("pan_x");
|
||
result.pan_y = finite_number("pan_y");
|
||
if(result.zoom <= 0.0 || result.zoom > 100.0)
|
||
throw std::invalid_argument("数据拓扑视图参数无效");
|
||
std::map<std::string, bool> valid_nodes;
|
||
for(const auto& source : global->mode_acs.data_source_config.map.list())
|
||
valid_nodes.emplace("data_source:" + source->key, true);
|
||
for(const auto& feed : global->mode_acs.data_feed_config.map.list())
|
||
valid_nodes.emplace("data_feed:" + feed->key, true);
|
||
const auto positions = input.find("node_positions");
|
||
if(positions == input.end() || !positions->is_object())
|
||
throw std::invalid_argument("节点位置必须是对象");
|
||
for(const auto& [id, position] : positions->items()) {
|
||
if(!valid_nodes.contains(id) || !position.is_object())
|
||
continue;
|
||
const auto x_field = position.find("x");
|
||
const auto y_field = position.find("y");
|
||
if(x_field == position.end() || y_field == position.end() || !x_field->is_number() || !y_field->is_number())
|
||
throw std::invalid_argument("节点位置缺少数值坐标: " + id);
|
||
const auto x = x_field->get<double>();
|
||
const auto y = y_field->get<double>();
|
||
if(!std::isfinite(x) || !std::isfinite(y))
|
||
throw std::invalid_argument("节点位置不是有限数值: " + id);
|
||
result.node_positions.emplace(id, Data_Topology_Node_Position{x, y});
|
||
}
|
||
return result;
|
||
}
|
||
template <class Function>
|
||
void complete_business_request(std::function<void(const drogon::HttpResponsePtr&)>& callback, Function&& function) noexcept {
|
||
callback(adminive::make_drogon_response(business_response(std::forward<Function>(function))));
|
||
}
|
||
adminive::Collection_Query read_collection_query(const drogon::HttpRequestPtr& request) {
|
||
adminive::Collection_Query query;
|
||
query.page = adminive::read_drogon_size_parameter(request, "page", 1);
|
||
query.per_page = adminive::read_drogon_size_parameter(request, "perPage", 20);
|
||
query.order_by = request->getParameter("orderBy");
|
||
query.order_dir = request->getParameter("orderDir");
|
||
for(const auto& [name, value] : request->getParameters()) {
|
||
if(name != "page" && name != "perPage" && name != "orderBy" && name != "orderDir")
|
||
query.fields.insert_or_assign(name, value);
|
||
}
|
||
return query;
|
||
}
|
||
std::vector<std::uint64_t> read_order_ids(std::string_view body) {
|
||
const Json input = adminive::parse_json<Json>(body);
|
||
if(!input.contains("ids"))
|
||
throw std::invalid_argument("missing ids");
|
||
const auto& value = input.at("ids");
|
||
std::vector<std::uint64_t> result;
|
||
if(value.is_array()) {
|
||
result.reserve(value.size());
|
||
for(const auto& item : value)
|
||
result.push_back(item.is_string() ? std::stoull(item.get<std::string>()) : item.get<std::uint64_t>());
|
||
return result;
|
||
}
|
||
const std::string text = value.get<std::string>();
|
||
std::size_t begin{};
|
||
while(begin < text.size()) {
|
||
const auto end = text.find(',', begin);
|
||
const auto token = std::string_view(text).substr(begin, end == std::string::npos ? text.size() - begin : end - begin);
|
||
std::uint64_t id{};
|
||
const auto [position, error] = std::from_chars(token.data(), token.data() + token.size(), id);
|
||
if(error != std::errc{} || position != token.data() + token.size())
|
||
throw std::invalid_argument("invalid order id");
|
||
result.push_back(id);
|
||
if(end == std::string::npos)
|
||
break;
|
||
begin = end + 1;
|
||
}
|
||
return result;
|
||
}
|
||
template <class Value>
|
||
std::shared_ptr<Value> item_by_id(const std::vector<std::shared_ptr<Value>>& list, std::uint64_t id) {
|
||
if(id == 0 || id > list.size())
|
||
throw std::out_of_range("record not found");
|
||
return list[id - 1];
|
||
}
|
||
template <class Value>
|
||
std::vector<std::string> ordered_keys(const std::vector<std::shared_ptr<Value>>& list, const std::vector<std::uint64_t>& ids) {
|
||
if(ids.size() != list.size())
|
||
throw std::invalid_argument("order must contain every record");
|
||
std::vector<bool> used(list.size());
|
||
std::vector<std::string> keys;
|
||
keys.reserve(ids.size());
|
||
for(const auto id : ids) {
|
||
if(id == 0 || id > list.size() || used[id - 1])
|
||
throw std::invalid_argument("invalid order id");
|
||
used[id - 1] = true;
|
||
keys.push_back(list[id - 1]->key);
|
||
}
|
||
return keys;
|
||
}
|
||
std::string source_type_name(Data_Source_Type value) {
|
||
return std::string(magic_enum::enum_name(value));
|
||
}
|
||
std::string feed_type_name(Data_Feed_Type value) {
|
||
return std::string(magic_enum::enum_name(value));
|
||
}
|
||
Data_Source_Type source_type(std::string_view value) {
|
||
const auto result = magic_enum::enum_cast<Data_Source_Type>(value);
|
||
if(!result)
|
||
throw std::invalid_argument("未知数据源类型: " + std::string(value));
|
||
return *result;
|
||
}
|
||
Data_Feed_Type feed_type(std::string_view value) {
|
||
const auto result = magic_enum::enum_cast<Data_Feed_Type>(value);
|
||
if(!result)
|
||
throw std::invalid_argument("未知数据馈送类型: " + std::string(value));
|
||
return *result;
|
||
}
|
||
Data_Source_Row source_row(const std::shared_ptr<Data_Source>& source) {
|
||
Data_Source_Row row;
|
||
row.key = source->key;
|
||
row.type = source_type(source->type);
|
||
row.enable = source->enabled();
|
||
source->settings.read([&](const auto& value) {
|
||
row.config = value;
|
||
});
|
||
row.state = Psc::to_string(source->state.load());
|
||
if(const auto source_value = dynamic_cast<TCP_Client_Data_Source*>(source.get())) {
|
||
source_value->specific.read([&](const auto& value) {
|
||
row.ip = value.ip;
|
||
row.port = value.port;
|
||
});
|
||
} else if(const auto source_value = dynamic_cast<Serial_Data_Source*>(source.get())) {
|
||
source_value->specific.read([&](const auto& value) {
|
||
row.port_name = value.port_name;
|
||
row.baud_rate = value.baud_rate;
|
||
});
|
||
} else if(const auto source_value = dynamic_cast<File_Data_Source*>(source.get())) {
|
||
source_value->specific.read([&](const auto& value) {
|
||
row.file_path = value.file_path;
|
||
row.data_type = value.data_type;
|
||
row.play_mode = value.play_mode;
|
||
});
|
||
} else if(const auto source_value = dynamic_cast<Dll_Data_Source*>(source.get())) {
|
||
source_value->specific.read([&](const auto& value) {
|
||
row.library_path = value.library_path;
|
||
row.function_name = value.function_name;
|
||
row.data_type = value.data_type;
|
||
row.buffer_size = value.buffer_size;
|
||
});
|
||
} else if(const auto source_value = dynamic_cast<Shared_Memory_Data_Source*>(source.get())) {
|
||
source_value->specific.read([&](const auto& value) {
|
||
row.shared_memory_name = value.shared_memory_name;
|
||
row.shared_memory_size = value.shared_memory_size;
|
||
row.data_type = value.data_type;
|
||
});
|
||
}
|
||
return row;
|
||
}
|
||
void apply_source_row(const std::shared_ptr<Data_Source>& source, const Data_Source_Row& row) {
|
||
source->set_enabled(row.enable);
|
||
source->settings.write([&](auto& value) {
|
||
value = row.config;
|
||
});
|
||
if(auto source_value = dynamic_cast<TCP_Client_Data_Source*>(source.get())) {
|
||
source_value->specific.write([&](auto& value) {
|
||
value.ip = row.ip;
|
||
value.port = row.port;
|
||
});
|
||
} else if(auto source_value = dynamic_cast<Serial_Data_Source*>(source.get())) {
|
||
source_value->specific.write([&](auto& value) {
|
||
value.port_name = row.port_name;
|
||
value.baud_rate = row.baud_rate;
|
||
});
|
||
} else if(auto source_value = dynamic_cast<File_Data_Source*>(source.get())) {
|
||
source_value->specific.write([&](auto& value) {
|
||
value.file_path = row.file_path;
|
||
value.data_type = row.data_type;
|
||
value.play_mode = row.play_mode;
|
||
});
|
||
} else if(auto source_value = dynamic_cast<Dll_Data_Source*>(source.get())) {
|
||
source_value->specific.write([&](auto& value) {
|
||
value.library_path = row.library_path;
|
||
value.function_name = row.function_name;
|
||
value.data_type = row.data_type;
|
||
value.buffer_size = row.buffer_size;
|
||
});
|
||
source_value->buffer.resize(row.buffer_size);
|
||
} else if(auto source_value = dynamic_cast<Shared_Memory_Data_Source*>(source.get())) {
|
||
source_value->specific.write([&](auto& value) {
|
||
value.shared_memory_name = row.shared_memory_name;
|
||
value.shared_memory_size = row.shared_memory_size;
|
||
value.data_type = row.data_type;
|
||
});
|
||
}
|
||
}
|
||
Data_Feed_Row feed_row(const std::shared_ptr<Data_Feed>& feed) {
|
||
Data_Feed_Row row;
|
||
row.key = feed->key;
|
||
row.type = feed_type(feed->type);
|
||
row.enable = feed->enabled();
|
||
row.source_key = feed->source_key;
|
||
feed->output_format.read([&](const auto& value) {
|
||
row.output_format = value;
|
||
row.format = value.type;
|
||
});
|
||
row.state = Psc::to_string(feed->state.load());
|
||
if(const auto feed_value = dynamic_cast<Data_Feed_TCP_Server*>(feed.get())) {
|
||
feed_value->specific.read([&](const auto& value) {
|
||
row.port = value.port;
|
||
row.connect_user_buffer_size = value.connect_user_buffer_size;
|
||
row.connect_system_buffer_size = value.connect_system_buffer_size;
|
||
});
|
||
} else if(const auto feed_value = dynamic_cast<Data_Feed_UDP_Server*>(feed.get())) {
|
||
feed_value->specific.read([&](const auto& value) {
|
||
row.port = value.port;
|
||
});
|
||
} else if(const auto feed_value = dynamic_cast<Data_Feed_TCP_Client*>(feed.get())) {
|
||
feed_value->specific.read([&](const auto& value) {
|
||
row.url = value.url;
|
||
row.port = value.port;
|
||
});
|
||
} else if(const auto feed_value = dynamic_cast<Data_Feed_UDP_Client*>(feed.get())) {
|
||
feed_value->specific.read([&](const auto& value) {
|
||
row.url = value.url;
|
||
row.port = value.port;
|
||
});
|
||
}
|
||
return row;
|
||
}
|
||
void apply_feed_row(const std::shared_ptr<Data_Feed>& feed, const Data_Feed_Row& row) {
|
||
feed->set_enabled(row.enable);
|
||
feed->source_key = row.source_key;
|
||
feed->output_format.write([&](auto& value) {
|
||
value = row.output_format;
|
||
});
|
||
if(auto feed_value = dynamic_cast<Data_Feed_TCP_Server*>(feed.get())) {
|
||
feed_value->specific.write([&](auto& value) {
|
||
value.port = row.port;
|
||
value.connect_user_buffer_size = row.connect_user_buffer_size;
|
||
value.connect_system_buffer_size = row.connect_system_buffer_size;
|
||
});
|
||
} else if(auto feed_value = dynamic_cast<Data_Feed_UDP_Server*>(feed.get())) {
|
||
feed_value->specific.member<&Data_Feed_UDP_Server_Data::port>().write([&](auto& value) {
|
||
value = row.port;
|
||
});
|
||
} else if(auto feed_value = dynamic_cast<Data_Feed_TCP_Client*>(feed.get())) {
|
||
feed_value->specific.write([&](auto& value) {
|
||
value.url = row.url;
|
||
value.port = row.port;
|
||
});
|
||
} else if(auto feed_value = dynamic_cast<Data_Feed_UDP_Client*>(feed.get())) {
|
||
feed_value->specific.write([&](auto& value) {
|
||
value.url = row.url;
|
||
value.port = row.port;
|
||
});
|
||
}
|
||
}
|
||
template <class Row>
|
||
adminive::Http_Response<Json> list_response(std::string path, std::vector<Row> rows, adminive::Collection_Query query) {
|
||
adminive::Collection_Service<Row, Json> collection(std::move(path), std::move(rows));
|
||
return collection.list_response(std::move(query));
|
||
}
|
||
template <class Row>
|
||
adminive::Http_Response<Json> item_response(std::string path, std::vector<Row> rows, std::uint64_t id) {
|
||
adminive::Collection_Service<Row, Json> collection(std::move(path), std::move(rows));
|
||
return collection.item_response(id);
|
||
}
|
||
template <class Row, class Descriptor_Factory, class Rows_Factory, class Schema_Factory>
|
||
void bind_collection_read_handlers(drogon::HttpAppFramework& app, std::string path,
|
||
Descriptor_Factory descriptor_factory, Rows_Factory rows_factory,
|
||
Schema_Factory schema_factory, bool bind_item) {
|
||
const auto get = adminive::make_drogon_constraints(drogon::Get, {});
|
||
app.registerHandler(path + "/descriptor", [descriptor_factory](const drogon::HttpRequestPtr&, std::function<void(const drogon::HttpResponsePtr&)>&& callback) {
|
||
callback(adminive::make_drogon_response(adminive::make_http_success<Json>(descriptor_factory())));
|
||
}, get);
|
||
app.registerHandler(path + "/view", [](const drogon::HttpRequestPtr&, std::function<void(const drogon::HttpResponsePtr&)>&& callback) {
|
||
callback(adminive::make_drogon_response(adminive::make_http_success<Json>(adminive::to_view_json<Json, Row>(adminive::describe_table_view<Row>()))));
|
||
}, get);
|
||
app.registerHandler(path + "/amis", [schema_factory](const drogon::HttpRequestPtr&, std::function<void(const drogon::HttpResponsePtr&)>&& callback) {
|
||
callback(adminive::make_drogon_response(adminive::make_http_success<Json>(schema_factory())));
|
||
}, get);
|
||
app.registerHandler(path, [path, rows_factory](const drogon::HttpRequestPtr& request, std::function<void(const drogon::HttpResponsePtr&)>&& callback) {
|
||
callback(adminive::make_drogon_response(list_response<Row>(path, rows_factory(), read_collection_query(request))));
|
||
}, get);
|
||
if(bind_item) {
|
||
app.registerHandler(path + "/{1:id}", [path, rows_factory](const drogon::HttpRequestPtr&, std::function<void(const drogon::HttpResponsePtr&)>&& callback, std::uint64_t id) {
|
||
callback(adminive::make_drogon_response(item_response<Row>(path, rows_factory(), id)));
|
||
}, get);
|
||
}
|
||
}
|
||
std::vector<Data_Source_Row> source_rows(Global* global) {
|
||
std::vector<Data_Source_Row> rows;
|
||
for(const auto& source : global->mode_acs.data_source_config.map.list())
|
||
rows.push_back(source_row(source));
|
||
return rows;
|
||
}
|
||
std::vector<Data_Feed_Row> feed_rows(Global* global) {
|
||
std::vector<Data_Feed_Row> rows;
|
||
for(const auto& feed : global->mode_acs.data_feed_config.map.list())
|
||
rows.push_back(feed_row(feed));
|
||
return rows;
|
||
}
|
||
Json data_topology_view(Global* global) {
|
||
auto result = global->mode_acs.data_topology_view.read([](const auto& value) {
|
||
return adminive::parse_json<Json>(value.to_json().to_json_string());
|
||
});
|
||
result["protocol"] = "ecap.data-topology-view";
|
||
result["protocol_version"] = 1;
|
||
return result;
|
||
}
|
||
Json data_topology_connection_status(Global* global, std::uint64_t id) {
|
||
const auto feed = item_by_id(global->mode_acs.data_feed_config.map.list(), id);
|
||
const auto source = global->mode_acs.data_source_config.map.get(feed->source_key);
|
||
Json result{{"连接", feed->source_key + " → " + feed->key}, {"数据馈送状态", Psc::to_string(feed->state.load())}, {"数据馈送启用", feed->enabled()}};
|
||
if(!source) {
|
||
result["连接状态"] = "数据源不存在";
|
||
result["已在数据源注册"] = false;
|
||
return result;
|
||
}
|
||
result["数据源状态"] = Psc::to_string(source.value()->state.load());
|
||
result["数据源启用"] = source.value()->enabled();
|
||
const auto all_status = adminive::parse_json<Json>(source.value()->get_all_connect_feed_status().to_json_string());
|
||
const auto status = all_status.find(feed->key);
|
||
result["已在数据源注册"] = status != all_status.end();
|
||
result["连接状态"] = status != all_status.end() && source.value()->enabled() && feed->enabled() ? "运行中" : "未运行";
|
||
if(status != all_status.end())
|
||
result["缓存统计"] = *status;
|
||
result["数据馈送观测"] = adminive::parse_json<Json>(feed->get_state_json().to_json_string());
|
||
return result;
|
||
}
|
||
Json data_topology_runtime_client_status(Global* global, std::uint64_t feed_id, std::uint64_t client_id) {
|
||
const auto feed = item_by_id(global->mode_acs.data_feed_config.map.list(), feed_id);
|
||
if(const auto tcp = dynamic_cast<Data_Feed_TCP_Server*>(feed.get())) {
|
||
const auto client = tcp->svr.get_client(static_cast<Psc::asio_socket::Socket_FD>(client_id));
|
||
if(!client)
|
||
throw std::out_of_range("TCP client is no longer connected");
|
||
return Json{{"connection_state", client->closing.load() ? "closing" : "connected"},
|
||
{"remote", client->info.sockaddr.to_string()}, {"fd", client->info.fd},
|
||
{"statistics", adminive::parse_json<Json>(client->statistics.to_json().to_json_string())}};
|
||
}
|
||
if(const auto udp = dynamic_cast<Data_Feed_UDP_Server*>(feed.get())) {
|
||
const auto client = udp->svr.get_client(static_cast<std::size_t>(client_id));
|
||
if(!client)
|
||
throw std::out_of_range("UDP client is no longer available");
|
||
const auto last_activity = client->statistics->last_activity_unix_ms();
|
||
const auto idle_ms = last_activity == 0 ? 0 : Psc::get_current_millisecond_timestamp() - last_activity;
|
||
return Json{{"connection_state", last_activity != 0 && idle_ms <= 30000 ? "active" : "idle"}, {"remote", client->address.to_string()}, {"idle_ms", idle_ms},
|
||
{"statistics", adminive::parse_json<Json>(client->statistics->to_json().to_json_string())}};
|
||
}
|
||
throw std::invalid_argument("data feed does not expose runtime clients");
|
||
}
|
||
Json data_topology(Global* global) {
|
||
const auto sources = global->mode_acs.data_source_config.map.list();
|
||
const auto feeds = global->mode_acs.data_feed_config.map.list();
|
||
std::map<std::string, std::shared_ptr<Data_Source>> source_by_key;
|
||
Json nodes = Json::array();
|
||
Json edges = Json::array();
|
||
for(std::size_t index = 0; index < sources.size(); ++index) {
|
||
const auto& source = sources[index];
|
||
const auto id = "data_source:" + source->key;
|
||
source_by_key.emplace(source->key, source);
|
||
auto config = adminive::to_frontend_json<Json>(source_row(source));
|
||
config["id"] = index + 1;
|
||
Json details = Json::array();
|
||
details.push_back(Json{{"label", "运行状态"}, {"value", Psc::to_string(source->state.load())}});
|
||
if(const auto value = dynamic_cast<TCP_Client_Data_Source*>(source.get())) {
|
||
value->specific.read([&](const auto& config) { details.push_back(Json{{"label", "目标"}, {"value", config.ip + ":" + std::to_string(config.port)}}); });
|
||
} else if(const auto value = dynamic_cast<Serial_Data_Source*>(source.get())) {
|
||
value->specific.read([&](const auto& config) { details.push_back(Json{{"label", "串口"}, {"value", config.port_name}}); });
|
||
} else if(const auto value = dynamic_cast<File_Data_Source*>(source.get())) {
|
||
value->specific.read([&](const auto& config) { details.push_back(Json{{"label", "文件"}, {"value", config.file_path}}); });
|
||
} else if(const auto value = dynamic_cast<Dll_Data_Source*>(source.get())) {
|
||
value->specific.read([&](const auto& config) { details.push_back(Json{{"label", "动态库"}, {"value", config.library_path}}); });
|
||
} else if(const auto value = dynamic_cast<Shared_Memory_Data_Source*>(source.get())) {
|
||
value->specific.read([&](const auto& config) { details.push_back(Json{{"label", "共享内存"}, {"value", config.shared_memory_name}}); });
|
||
}
|
||
nodes.push_back(Json{{"id", id}, {"kind", "data_source"}, {"entity_id", index + 1},
|
||
{"key", source->key}, {"label", source->key}, {"type", source->type},
|
||
{"enabled", source->enabled()}, {"config", std::move(config)}, {"details", std::move(details)}, {"status_api", data_topology_path + "/status/data_source/" + std::to_string(index + 1)}});
|
||
}
|
||
for(std::size_t index = 0; index < feeds.size(); ++index) {
|
||
const auto& feed = feeds[index];
|
||
const auto id = "data_feed:" + feed->key;
|
||
auto config = adminive::to_frontend_json<Json>(feed_row(feed));
|
||
config["id"] = index + 1;
|
||
Json details = Json::array({Json{{"label", "运行状态"}, {"value", Psc::to_string(feed->state.load())}}});
|
||
feed->output_format.read([&](const auto& value) { details.push_back(Json{{"label", "数据格式"}, {"value", std::string(magic_enum::enum_name(value.type))}}); });
|
||
if(const auto value = dynamic_cast<Data_Feed_TCP_Server*>(feed.get())) {
|
||
value->specific.read([&](const auto& config) { details.push_back(Json{{"label", "监听端口"}, {"value", config.port}}); });
|
||
} else if(const auto value = dynamic_cast<Data_Feed_UDP_Server*>(feed.get())) {
|
||
value->specific.read([&](const auto& config) { details.push_back(Json{{"label", "监听端口"}, {"value", config.port}}); });
|
||
} else if(const auto value = dynamic_cast<Data_Feed_TCP_Client*>(feed.get())) {
|
||
value->specific.read([&](const auto& config) { details.push_back(Json{{"label", "目标"}, {"value", config.url + ":" + std::to_string(config.port)}}); });
|
||
} else if(const auto value = dynamic_cast<Data_Feed_UDP_Client*>(feed.get())) {
|
||
value->specific.read([&](const auto& config) { details.push_back(Json{{"label", "目标"}, {"value", config.url + ":" + std::to_string(config.port)}}); });
|
||
}
|
||
nodes.push_back(Json{{"id", id}, {"kind", "data_feed"}, {"entity_id", index + 1},
|
||
{"key", feed->key}, {"label", feed->key}, {"type", feed->type},
|
||
{"enabled", feed->enabled()}, {"config", std::move(config)}, {"details", std::move(details)}, {"status_api", data_topology_path + "/status/data_feed/" + std::to_string(index + 1)}});
|
||
const auto source = source_by_key.find(feed->source_key);
|
||
if(source != source_by_key.end()) {
|
||
edges.push_back(Json{{"id", "source-feed:" + feed->key}, {"kind", "configuration"},
|
||
{"source", "data_source:" + feed->source_key}, {"target", id},
|
||
{"active", source->second->enabled() && feed->enabled()},
|
||
{"status_api", data_topology_path + "/status/connection/" + std::to_string(index + 1)}});
|
||
}
|
||
if(const auto value = dynamic_cast<Data_Feed_TCP_Server*>(feed.get())) {
|
||
for(const auto& client : value->svr.get_all_clients()) {
|
||
const auto client_id = "client:tcp:" + feed->key + ":" + std::to_string(client->info.fd);
|
||
const auto endpoint = client->info.sockaddr.to_string();
|
||
const auto status_api = data_topology_path + "/status/runtime_client/" + std::to_string(index + 1) + "/" + std::to_string(client->info.fd);
|
||
nodes.push_back(Json{{"id", client_id}, {"kind", "client"}, {"entity_id", 0}, {"key", endpoint}, {"label", endpoint}, {"type", "TCP 客户端"}, {"enabled", true}, {"status_api", status_api}, {"details", Json::array({Json{{"label", "文件描述符"}, {"value", client->info.fd}}, Json{{"label", "远端"}, {"value", endpoint}}})}});
|
||
edges.push_back(Json{{"id", "feed-client:" + feed->key + ":" + std::to_string(client->info.fd)}, {"kind", "runtime"}, {"source", id}, {"target", client_id}, {"active", !client->closing.load()}, {"status_api", status_api}, {"details", Json::array({Json{{"label", "连接状态"}, {"value", "已连接"}}, Json{{"label", "远端"}, {"value", endpoint}}})}});
|
||
}
|
||
} else if(const auto value = dynamic_cast<Data_Feed_UDP_Server*>(feed.get())) {
|
||
const auto clients = value->svr.get_all_clients();
|
||
for(std::size_t client_index = 0; client_index < clients.size(); ++client_index) {
|
||
const auto& client = clients[client_index];
|
||
const auto endpoint = client.address.to_string();
|
||
const auto last_activity = client.statistics->last_activity_unix_ms();
|
||
const auto active = last_activity != 0 && Psc::get_current_millisecond_timestamp() - last_activity <= 30000;
|
||
const auto client_id = "client:udp:" + feed->key + ":" + endpoint;
|
||
const auto status_api = data_topology_path + "/status/runtime_client/" + std::to_string(index + 1) + "/" + std::to_string(client_index);
|
||
nodes.push_back(Json{{"id", client_id}, {"kind", "client"}, {"entity_id", 0}, {"key", endpoint}, {"label", endpoint}, {"type", "UDP 客户端"}, {"enabled", true}, {"status_api", status_api}, {"details", Json::array({Json{{"label", "远端"}, {"value", endpoint}}})}});
|
||
edges.push_back(Json{{"id", "feed-client:" + feed->key + ":" + endpoint}, {"kind", "runtime"}, {"source", id}, {"target", client_id}, {"active", active}, {"status_api", status_api}, {"details", Json::array({Json{{"label", "连接状态"}, {"value", active ? "活跃" : "空闲"}}, Json{{"label", "远端"}, {"value", endpoint}}})}});
|
||
}
|
||
}
|
||
}
|
||
return Json{{"protocol", "ecap.data-topology"}, {"protocol_version", 1},
|
||
{"nodes", std::move(nodes)}, {"edges", std::move(edges)}, {"view", data_topology_view(global)}};
|
||
}
|
||
std::vector<External_Resource_Row> external_resource_rows(Global* global) {
|
||
std::map<std::string, External_Resource_Status> status_by_name;
|
||
for(auto status : global->external_resources_manager.external_database_status())
|
||
status_by_name.emplace(status.name, std::move(status));
|
||
const auto config = global->external_resources_manager.to_base_json();
|
||
const auto* list = config.get("list");
|
||
std::vector<External_Resource_Row> rows;
|
||
if(!list)
|
||
return rows;
|
||
rows.reserve(list->children.size());
|
||
for(const auto& item : list->children) {
|
||
External_Resources_Data value;
|
||
value.from_base_json(&item, false);
|
||
External_Resource_Row row;
|
||
row.name = value.name;
|
||
row.description = value.description;
|
||
row.download_url = value.download_url;
|
||
row.update_interval_seconds = value.update_interval_seconds;
|
||
row.last_updated_at = static_cast<std::int64_t>(value.last_updated_at);
|
||
const auto status = status_by_name.find(value.name);
|
||
if(status != status_by_name.end()) {
|
||
row.row_count = status->second.row_count;
|
||
row.state = status->second.message;
|
||
}
|
||
rows.push_back(std::move(row));
|
||
}
|
||
return rows;
|
||
}
|
||
Json source_amis_schema() {
|
||
return adminive::to_amis_table_schema<Json, Data_Source_Row>("/api/adminive/config/data_sources", adminive::describe_table_view<Data_Source_Row>());
|
||
}
|
||
void set_select_options(Json& value, std::string_view field_name, const Json& options) {
|
||
if(value.is_object()) {
|
||
const auto name = value.find("name");
|
||
if(name != value.end() && name->is_string() && name->get<std::string>() == field_name) {
|
||
value["type"] = "select";
|
||
value["options"] = options;
|
||
value["clearable"] = true;
|
||
value["placeholder"] = "未连接";
|
||
}
|
||
for(auto& [key, child] : value.items()) {
|
||
static_cast<void>(key);
|
||
set_select_options(child, field_name, options);
|
||
}
|
||
} else if(value.is_array()) {
|
||
for(auto& child : value)
|
||
set_select_options(child, field_name, options);
|
||
}
|
||
}
|
||
Json feed_amis_schema(Global* global) {
|
||
auto schema = adminive::to_amis_table_schema<Json, Data_Feed_Row>("/api/adminive/config/data_feeds", adminive::describe_table_view<Data_Feed_Row>());
|
||
Json options = Json::array();
|
||
for(const auto& source : global->mode_acs.data_source_config.map.list())
|
||
options.push_back(Json{{"label", source->key}, {"value", source->key}});
|
||
set_select_options(schema, "source_key", options);
|
||
return schema;
|
||
}
|
||
Json topology_editor(std::string kind, std::string title, std::string base_api, Json create_data, Json collection_schema) {
|
||
const auto& crud = collection_schema["body"][0];
|
||
Json create_schema;
|
||
for(const auto& item : crud["headerToolbar"]) {
|
||
if(item.is_object() && item.value("actionType", std::string{}) == "dialog") {
|
||
create_schema = item["dialog"]["body"];
|
||
break;
|
||
}
|
||
}
|
||
const auto& columns = crud["columns"];
|
||
Json edit_schema = columns.back()["buttons"][0]["dialog"]["body"];
|
||
create_schema.erase("reload");
|
||
edit_schema.erase("reload");
|
||
edit_schema["initApi"] = Json{{"method", "get"}, {"url", base_api + "/${id}"}};
|
||
return Json{{"kind", std::move(kind)}, {"title", std::move(title)}, {"create_data", std::move(create_data)}, {"create_schema", std::move(create_schema)}, {"edit_schema", std::move(edit_schema)}};
|
||
}
|
||
Json data_topology_schema(Global* global) {
|
||
auto source_create_data = adminive::to_frontend_json<Json>(Data_Source_Row{});
|
||
source_create_data.erase("state");
|
||
auto feed_create_data = adminive::to_frontend_json<Json>(Data_Feed_Row{});
|
||
feed_create_data.erase("format");
|
||
feed_create_data.erase("state");
|
||
return Json{
|
||
{"title", "数据源与数据馈送拓扑"},
|
||
{"data_api", data_topology_path},
|
||
{"view_api", data_topology_path + "/view"},
|
||
{"graph", Json{
|
||
{"height", 620},
|
||
{"layout", Json{{"type", "dagre"}, {"rankdir", "TB"}, {"align", "UL"}, {"nodesep", 42}, {"ranksep", 92}}},
|
||
{"layout_revision", 2},
|
||
{"poll_interval_ms", 2000},
|
||
{"behaviors", Json::array({"drag-canvas", "zoom-canvas", "drag-element"})},
|
||
{"active_edge_color", "#52c41a"},
|
||
{"inactive_edge_color", "#bfbfbf"}
|
||
}},
|
||
{"node_kinds", Json::array({
|
||
Json{{"kind", "data_source"}, {"title", "数据源"}, {"fill", "#e6f4ff"}, {"stroke", "#1677ff"}},
|
||
Json{{"kind", "data_feed"}, {"title", "数据馈送"}, {"fill", "#f6ffed"}, {"stroke", "#52c41a"}},
|
||
Json{{"kind", "client"}, {"title", "已连接客户端"}, {"fill", "#fff7e6"}, {"stroke", "#fa8c16"}}
|
||
})},
|
||
{"status", Json{{"title", "运行状态"}, {"poll_interval_ms", 1000},
|
||
{"collapsed_hint", "展开后才向后台轮询,收起立即停止"}}},
|
||
{"editors", Json::array({
|
||
topology_editor("data_source", "数据源", "/api/adminive/config/data_sources", std::move(source_create_data), source_amis_schema()),
|
||
topology_editor("data_feed", "数据馈送", "/api/adminive/config/data_feeds", std::move(feed_create_data), feed_amis_schema(global))
|
||
})}
|
||
};
|
||
}
|
||
Json external_resource_amis_schema() {
|
||
auto schema = adminive::to_amis_table_schema<Json, External_Resource_Row>("/api/adminive/config/external_resources", adminive::describe_table_view<External_Resource_Row>());
|
||
auto& crud = schema["body"][0];
|
||
const std::string crud_id = "external_resource_table_crud";
|
||
crud["headerToolbar"] = Json::array({Json{{"type", "columns-toggler"}, {"draggable", true}}, "reload", Json{{"type", "button"}, {"label", "全部后端下载并导入"}, {"level", "primary"}, {"actionType", "ajax"}, {"api", Json{{"method", "post"}, {"url", "/api/refresh_external_databases"}}}, {"reload", crud_id}}});
|
||
auto& columns = crud["columns"];
|
||
if(!columns.empty())
|
||
columns.erase(columns.end() - 1);
|
||
const auto refresh_button = Json{{"type", "button"}, {"label", "后端下载并导入"}, {"level", "link"}, {"actionType", "ajax"}, {"api", Json{{"method", "post"}, {"url", "/api/refresh_external_database"}, {"data", Json{{"name", "${name}"}}}}}, {"reload", crud_id}};
|
||
const auto download_button = Json{{"type", "button"}, {"label", "前端下载"}, {"level", "link"}, {"actionType", "url"}, {"url", "${download_url}"}, {"blank", true}, {"disabledOn", "!this.download_url"}};
|
||
const auto upload_button = Json{{"type", "button"}, {"label", "离线导入"}, {"level", "link"}, {"actionType", "dialog"}, {"dialog", Json{{"title", "离线导入 ${name}"}, {"body", Json{{"type", "input-file"}, {"name", "file"}, {"label", "数据文件"}, {"accept", ".csv,.gz,.zip,.txt"}, {"receiver", Json{{"method", "post"}, {"url", "/api/upload_external_database"}, {"data", Json{{"name", "${name}"}}}}}}}}}};
|
||
const auto clear_button = Json{{"type", "button"}, {"label", "清空数据表"}, {"level", "link"}, {"className", "text-danger"}, {"actionType", "ajax"}, {"confirmText", "确定清空 ${name}?"}, {"api", Json{{"method", "post"}, {"url", "/api/clear_external_database_table"}, {"data", Json{{"name", "${name}"}}}}}, {"reload", crud_id}};
|
||
columns.push_back(Json{{"type", "operation"}, {"label", "操作"}, {"buttons", Json::array({refresh_button, download_button, upload_button, clear_button})}});
|
||
return schema;
|
||
}
|
||
Json backend_config_schema() {
|
||
return Json{{"title", "后端完整配置(只读)"}, {"data_api", "/api/adminive/config/backend_config"}};
|
||
}
|
||
Json archive_operations_schema() {
|
||
const auto formats = Json::array({Json{{"label", "ZIP (.zip)"}, {"value", "zip"}, {"format", "zip"}, {"filter", ""}, {"extension", "zip"}}, Json{{"label", "TAR (.tar)"}, {"value", "tar"}, {"format", "pax"}, {"filter", ""}, {"extension", "tar"}}, Json{{"label", "TAR.GZ (.tar.gz)"}, {"value", "tar.gz"}, {"format", "pax"}, {"filter", "gzip"}, {"extension", "tar.gz"}}, Json{{"label", "TAR.XZ (.tar.xz)"}, {"value", "tar.xz"}, {"format", "pax"}, {"filter", "xz"}, {"extension", "tar.xz"}}, Json{{"label", "TAR.ZST (.tar.zst)"}, {"value", "tar.zst"}, {"format", "pax"}, {"filter", "zstd"}, {"extension", "tar.zst"}}, Json{{"label", "TAR.BZ2 (.tar.bz2)"}, {"value", "tar.bz2"}, {"format", "pax"}, {"filter", "bzip2"}, {"extension", "tar.bz2"}}, Json{{"label", "TAR.LZ4 (.tar.lz4)"}, {"value", "tar.lz4"}, {"format", "pax"}, {"filter", "lz4"}, {"extension", "tar.lz4"}}, Json{{"label", "7Z (.7z)"}, {"value", "7z"}, {"format", "7zip"}, {"filter", ""}, {"extension", "7z"}}});
|
||
return Json{{"title", "导入导出"}, {"formats", std::move(formats)}, {"exports", Json::array({Json{{"name", "版本"}, {"api", "/api/export_version"}, {"allow_version_selection", true}}, Json{{"name", "日志"}, {"api", "/api/export_logs"}, {"allow_version_selection", false}}})}, {"import_api", "/api/import_version"}, {"import_accept", ".tar.zstd,.tar.zst,.tar.gz,.tgz,.tar.xz,.txz,.tar.bz2,.tbz2,.tar.lz4,.zip,.7z,.rar,.tar,.cpio,.iso,.xar"}};
|
||
}
|
||
Json data_source_descriptor() {
|
||
auto descriptor = adminive::to_descriptor_json<Json, Data_Source_Row>();
|
||
const auto tile_view_descriptor = adminive::to_descriptor_json<Json, Map_Tile_View_Config>();
|
||
const auto map_view_descriptor = adminive::to_descriptor_json<Json, Map_View_Config>();
|
||
const auto graphics_descriptor = adminive::to_descriptor_json<Json, Cesium_Graphics_Config_Data>();
|
||
descriptor["sidebar"] = Json{
|
||
{"source", Json{
|
||
{"title", "数据源配置"},
|
||
{"sections", Json::array({
|
||
Json{{"layout", Json::array({Json{{"kind", "cards"}, {"fields", Json::array({"update_form_gps", "base_station_has_valid_position", "keep_mode", "ignore_msg_time"})}}})}},
|
||
Json{{"layout", Json::array({Json{{"kind", "vertical"}, {"fields", Json::array({"lat", "lon", "alt"})}}})}},
|
||
Json{
|
||
{"title", "2D显示配置"},
|
||
{"modes", Json::array({"map2d"})},
|
||
{"object_path", "map_display.map2d"},
|
||
{"layout", Json::array({
|
||
Json{{"kind", "cards"}, {"fields", Json::array({"aircraft_show", "base_station_show", "show_icao", "show_call_sign", "show_fly_status"})}},
|
||
Json{{"kind", "vertical"}, {"fields", Json::array({"aircraft_pixel_size", "base_station_pixel_size", "track_point_pixel_size", "color", "track_point_color", "base_station_color"})}}
|
||
})}
|
||
},
|
||
Json{
|
||
{"title", "3D显示配置"},
|
||
{"modes", Json::array({"map3d"})},
|
||
{"object_path", "map_display.map3d"},
|
||
{"layout", Json::array({
|
||
Json{{"kind", "cards"}, {"fields", Json::array({"aircraft_show", "base_station_show", "constant_screen_size", "show_icao", "show_call_sign", "show_fly_status"})}},
|
||
Json{{"kind", "vertical"}, {"fields", Json::array({"aircraft_scale", "base_station_scale", "track_point_pixel_size", "color", "track_point_color", "base_station_color"})}}
|
||
})}
|
||
}
|
||
})},
|
||
{"rules", Json::array({
|
||
Json{{"field", "base_station_has_valid_position"}, {"disabled_when", Json{{"field", "update_form_gps"}, {"equals", true}}}},
|
||
Json{{"field", "lat"}, {"disabled_when", Json{{"field", "update_form_gps"}, {"equals", true}}}},
|
||
Json{{"field", "lon"}, {"disabled_when", Json{{"field", "update_form_gps"}, {"equals", true}}}},
|
||
Json{{"field", "alt"}, {"disabled_when", Json{{"field", "update_form_gps"}, {"equals", true}}}}
|
||
})}
|
||
}},
|
||
{"tiles", Json{
|
||
{"title", "瓦片显示"},
|
||
{"layout", Json::array({Json{{"kind", "vertical"}, {"fields", Json::array({"tile_display_maximum_level"})}}})},
|
||
{"descriptor_fields", tile_view_descriptor.at("fields")}
|
||
}},
|
||
{"view3d", Json{
|
||
{"graphics", Json{
|
||
{"title", "辅助显示"},
|
||
{"layout", Json::array({Json{{"kind", "cards"}, {"fields", Json::array({"surface_navigation_reference_visible", "view_axes_visible"})}}})},
|
||
{"descriptor_fields", graphics_descriptor.at("fields")}
|
||
}},
|
||
{"map_view", Json{
|
||
{"title", "定位视图"},
|
||
{"layout", Json::array({Json{{"kind", "vertical"}, {"fields", Json::array({"base_station_fly_to_height_offset_meters"})}}})},
|
||
{"descriptor_fields", map_view_descriptor.at("fields")}
|
||
}}
|
||
}}
|
||
};
|
||
return descriptor;
|
||
}
|
||
Json cesium_models_schema(Global* global) {
|
||
Json aircraft_types = Json::array();
|
||
global->map_model_config.read([&](const auto& config) {
|
||
for(const auto& [key, value] : config.aircraft_models) {
|
||
static_cast<void>(value);
|
||
auto label = key;
|
||
for(std::uint8_t raw = 1; raw <= 21; ++raw) {
|
||
const auto type = static_cast<SSR::Vortex_Type>(raw);
|
||
if(SSR::Vortex_Type_to_key(type) == key) {
|
||
label = SSR::Vortex_Type_to_label(type);
|
||
break;
|
||
}
|
||
}
|
||
aircraft_types.push_back(Json{{"key", key}, {"label", label}});
|
||
}
|
||
});
|
||
const auto item_descriptor = adminive::to_descriptor_json<Json, Map_Model_Item_Config>();
|
||
return Json{
|
||
{"title", "Cesium 模型设置"},
|
||
{"data_api", "/map/models"},
|
||
{"upload_accept", ".glb"},
|
||
{"item_descriptor", item_descriptor},
|
||
{"groups", Json::array({
|
||
Json{{"title", "默认飞机模型"}, {"path", "aircraft_model"}, {"upload", Json{{"model_type", "aircraft"}, {"label", "上传飞机模型"}, {"success_message", "默认飞机模型已更新"}}}},
|
||
Json{{"title", "基站模型"}, {"path", "base_station_model"}, {"upload", Json{{"model_type", "base_station"}, {"label", "上传基站模型"}, {"success_message", "基站模型已更新"}}}},
|
||
Json{{"title", "设备模型"}, {"path", "device_model"}, {"upload", Json{{"model_type", "device"}, {"label", "上传设备模型"}, {"success_message", "设备模型已更新"}}}}
|
||
})},
|
||
{"collections", Json::array({
|
||
Json{{"title", "按涡流/目标类型选择飞机模型"}, {"path", "aircraft_models"}, {"upload_model_type", "aircraft_type"}, {"upload_label", "上传 GLB"}, {"upload_success_message", "分类飞机模型已更新"}, {"items", std::move(aircraft_types)}}
|
||
})}
|
||
};
|
||
}
|
||
Json system_actions_schema() {
|
||
return Json{{"type", "panel"}, {"title", "设备操作"}, {"body", Json::array({Json{{"type", "button"}, {"label", "重启 FPGA 设备"}, {"level", "primary"}, {"actionType", "ajax"}, {"confirmText", "确定重启 FPGA 设备?"}, {"api", Json{{"method", "post"}, {"url", "/api/restart_device"}}}}})}};
|
||
}
|
||
class Adminive_Config_Resources {
|
||
public:
|
||
explicit Adminive_Config_Resources(Global* global) : global_(global),
|
||
mode_acs_resource_(global->mode_acs.settings, mode_acs_path, mode_acs_transaction()),
|
||
data_feed_settings_resource_(global->mode_acs.data_feed_config.settings, data_feed_settings_path, data_feed_settings_transaction()),
|
||
map_view_resource_(global->map_view_config, map_view_path, map_view_transaction(global)),
|
||
mlat_resource_(global->mlat.settings, mlat_path, value_section_transaction<MLAT_Config_Data>(Config_Section::mlat)),
|
||
cesium_graphics_resource_(global->cesium_graphics_config.settings, cesium_graphics_path, value_section_transaction<Cesium_Graphics_Config_Data>(Config_Section::cesium_graphics)) {
|
||
}
|
||
void bind() {
|
||
auto& app = drogon::app();
|
||
mode_acs_resource_.bind(app);
|
||
data_feed_settings_resource_.bind(app);
|
||
map_view_resource_.bind(app);
|
||
mlat_resource_.bind(app);
|
||
cesium_graphics_resource_.bind(app);
|
||
bind_data_sources(app);
|
||
bind_data_feeds(app);
|
||
bind_data_topology(app);
|
||
bind_external_resources(app);
|
||
bind_backend_config(app);
|
||
app.registerHandler(manifest_path, [this](const drogon::HttpRequestPtr&, std::function<void(const drogon::HttpResponsePtr&)>&& callback) {
|
||
callback(adminive::make_drogon_response(adminive::make_http_success<Json>(manifest())));
|
||
}, {drogon::Get});
|
||
}
|
||
private:
|
||
void bind_data_topology(drogon::HttpAppFramework& app) {
|
||
const auto get = adminive::make_drogon_constraints(drogon::Get, {});
|
||
const auto put = adminive::make_drogon_constraints(drogon::Put, {});
|
||
app.registerHandler(data_topology_path + "/schema", [this](const drogon::HttpRequestPtr&, std::function<void(const drogon::HttpResponsePtr&)>&& callback) {
|
||
complete_business_request(callback, [this] {
|
||
std::lock_guard lock(topology_mutex_);
|
||
return adminive::make_http_success<Json>(data_topology_schema(global_));
|
||
});
|
||
}, get);
|
||
app.registerHandler(data_topology_path, [this](const drogon::HttpRequestPtr&, std::function<void(const drogon::HttpResponsePtr&)>&& callback) {
|
||
complete_business_request(callback, [this] {
|
||
std::lock_guard lock(topology_mutex_);
|
||
return adminive::make_http_success<Json>(data_topology(global_));
|
||
});
|
||
}, get);
|
||
app.registerHandler(data_topology_path + "/view", [this](const drogon::HttpRequestPtr&, std::function<void(const drogon::HttpResponsePtr&)>&& callback) {
|
||
complete_business_request(callback, [this] {
|
||
std::lock_guard lock(topology_mutex_);
|
||
return adminive::make_http_success<Json>(data_topology_view(global_));
|
||
});
|
||
}, get);
|
||
app.registerHandler(data_topology_path + "/view", [this](const drogon::HttpRequestPtr& request, std::function<void(const drogon::HttpResponsePtr&)>&& callback) {
|
||
complete_business_request(callback, [this, body = std::string(request->getBody())] {
|
||
std::lock_guard lock(topology_mutex_);
|
||
auto value = read_topology_view(read_request_body(body), global_);
|
||
global_->mode_acs.data_topology_view.write([&](auto& current) { current = std::move(value); });
|
||
Config::save(Config_Section::mode_acs, global_->mode_acs.to_base_json());
|
||
return adminive::make_http_success<Json>(data_topology_view(global_), "saved");
|
||
});
|
||
}, put);
|
||
app.registerHandler(data_topology_path + "/status/data_source/{1:id}", [this](const drogon::HttpRequestPtr&, std::function<void(const drogon::HttpResponsePtr&)>&& callback, std::uint64_t id) {
|
||
complete_business_request(callback, [this, id] {
|
||
const auto source = item_by_id(global_->mode_acs.data_source_config.map.list(), id);
|
||
return adminive::make_http_success<Json>(adminive::parse_json<Json>(source->get_state().to_json_string()));
|
||
});
|
||
}, get);
|
||
app.registerHandler(data_topology_path + "/status/data_feed/{1:id}", [this](const drogon::HttpRequestPtr&, std::function<void(const drogon::HttpResponsePtr&)>&& callback, std::uint64_t id) {
|
||
complete_business_request(callback, [this, id] {
|
||
const auto feed = item_by_id(global_->mode_acs.data_feed_config.map.list(), id);
|
||
return adminive::make_http_success<Json>(adminive::parse_json<Json>(feed->get_state_json().to_json_string()));
|
||
});
|
||
}, get);
|
||
app.registerHandler(data_topology_path + "/status/connection/{1:id}", [this](const drogon::HttpRequestPtr&, std::function<void(const drogon::HttpResponsePtr&)>&& callback, std::uint64_t id) {
|
||
complete_business_request(callback, [this, id] {
|
||
return adminive::make_http_success<Json>(data_topology_connection_status(global_, id));
|
||
});
|
||
}, get);
|
||
app.registerHandler(data_topology_path + "/status/runtime_client/{1:feed_id}/{2:client_id}", [this](const drogon::HttpRequestPtr&, std::function<void(const drogon::HttpResponsePtr&)>&& callback, std::uint64_t feed_id, std::uint64_t client_id) {
|
||
complete_business_request(callback, [this, feed_id, client_id] {
|
||
return adminive::make_http_success<Json>(data_topology_runtime_client_status(global_, feed_id, client_id));
|
||
});
|
||
}, get);
|
||
}
|
||
void bind_backend_config(drogon::HttpAppFramework& app) {
|
||
const auto get = adminive::make_drogon_constraints(drogon::Get, {});
|
||
app.registerHandler(backend_config_path, [this](const drogon::HttpRequestPtr&, std::function<void(const drogon::HttpResponsePtr&)>&& callback) {
|
||
callback(adminive::make_drogon_response(adminive::make_http_success<Json>(Json::parse(global_->toJson().to_json_string()))));
|
||
}, get);
|
||
}
|
||
void bind_external_resources(drogon::HttpAppFramework& app) {
|
||
bind_collection_read_handlers<External_Resource_Row>(app, external_resources_path,
|
||
[] { return adminive::to_descriptor_json<Json, External_Resource_Row>(); },
|
||
[this] { return external_resource_rows(global_); },
|
||
[] { return external_resource_amis_schema(); }, false);
|
||
}
|
||
void bind_data_sources(drogon::HttpAppFramework& app) {
|
||
const auto post = adminive::make_drogon_constraints(drogon::Post, {});
|
||
const auto put = adminive::make_drogon_constraints(drogon::Put, {});
|
||
const auto patch = adminive::make_drogon_constraints(drogon::Patch, {});
|
||
const auto remove = adminive::make_drogon_constraints(drogon::Delete, {});
|
||
bind_collection_read_handlers<Data_Source_Row>(app, data_sources_path,
|
||
[] { return data_source_descriptor(); }, [this] { return source_rows(global_); },
|
||
[] { return source_amis_schema(); }, true);
|
||
app.registerHandler(data_sources_path, [this](const drogon::HttpRequestPtr& request, std::function<void(const drogon::HttpResponsePtr&)>&& callback) {
|
||
complete_business_request(callback, [this, body = std::string(request->getBody())] {
|
||
std::lock_guard lock(topology_mutex_);
|
||
auto input = read_request_body(body);
|
||
Data_Source_Row row;
|
||
const auto result = adminive::apply_frontend_create<Json>(row, input);
|
||
if(!result.success)
|
||
return adminive::make_http_error<Json>(422, result);
|
||
if(global_->mode_acs.data_source_config.map.get(row.key))
|
||
return adminive::make_http_error<Json>(409, "数据源名称已存在: " + row.key);
|
||
auto source = create_data_source_from_type(source_type_name(row.type));
|
||
source->key = row.key;
|
||
apply_source_row(source, row);
|
||
global_->mode_acs.data_source_config.map.push_back(source);
|
||
Data_feed_Config::set_connections_need_refresh();
|
||
save_data_sources(global_);
|
||
return adminive::make_http_success<Json>(adminive::to_frontend_json<Json>(source_row(source)), "created");
|
||
});
|
||
}, post);
|
||
const auto make_update = [this] {
|
||
return [this](const drogon::HttpRequestPtr& request, std::function<void(const drogon::HttpResponsePtr&)>&& callback, std::uint64_t id) {
|
||
complete_business_request(callback, [this, id, body = std::string(request->getBody())] {
|
||
std::lock_guard lock(topology_mutex_);
|
||
const auto list = global_->mode_acs.data_source_config.map.list();
|
||
auto source = item_by_id(list, id);
|
||
auto input = read_request_body(body);
|
||
auto row = source_row(source);
|
||
const auto result = adminive::apply_frontend_patch<Json>(row, input);
|
||
if(!result.success)
|
||
return adminive::make_http_error<Json>(422, result);
|
||
apply_source_row(source, row);
|
||
Data_feed_Config::set_connections_need_refresh();
|
||
save_data_sources(global_);
|
||
return adminive::make_http_success<Json>(adminive::to_frontend_json<Json>(source_row(source)), result.message);
|
||
});
|
||
};
|
||
};
|
||
app.registerHandler(data_sources_path + "/{1:id}", make_update(), put);
|
||
app.registerHandler(data_sources_path + "/{1:id}", make_update(), patch);
|
||
app.registerHandler(data_sources_path + "/{1:id}", [this](const drogon::HttpRequestPtr&, std::function<void(const drogon::HttpResponsePtr&)>&& callback, std::uint64_t id) {
|
||
complete_business_request(callback, [this, id] {
|
||
std::lock_guard lock(topology_mutex_);
|
||
const auto list = global_->mode_acs.data_source_config.map.list();
|
||
auto source = item_by_id(list, id);
|
||
if(global_->mode_acs.data_feed_config.source_has_connection(source->key))
|
||
throw Conflict_Error("数据源仍被数据馈送连接,不能删除: " + source->key);
|
||
source->async_stop();
|
||
if(!global_->mode_acs.data_source_config.map.remove(id - 1))
|
||
throw std::out_of_range("record not found");
|
||
Data_feed_Config::set_connections_need_refresh();
|
||
save_data_sources(global_);
|
||
return adminive::make_http_success<Json>(adminive::json_object<Json>(), "deleted");
|
||
});
|
||
}, remove);
|
||
app.registerHandler(data_sources_path + "/order", [this](const drogon::HttpRequestPtr& request, std::function<void(const drogon::HttpResponsePtr&)>&& callback) {
|
||
complete_business_request(callback, [this, body = std::string(request->getBody())] {
|
||
std::lock_guard lock(topology_mutex_);
|
||
const auto list = global_->mode_acs.data_source_config.map.list();
|
||
const auto keys = ordered_keys(list, read_order_ids(body));
|
||
if(!global_->mode_acs.data_source_config.map.set_order(keys))
|
||
throw std::invalid_argument("invalid order");
|
||
Data_feed_Config::set_connections_need_refresh();
|
||
save_data_sources(global_);
|
||
return adminive::make_http_success<Json>(adminive::json_object<Json>(), "reordered");
|
||
});
|
||
}, post);
|
||
}
|
||
void bind_data_feeds(drogon::HttpAppFramework& app) {
|
||
const auto post = adminive::make_drogon_constraints(drogon::Post, {});
|
||
const auto put = adminive::make_drogon_constraints(drogon::Put, {});
|
||
const auto patch = adminive::make_drogon_constraints(drogon::Patch, {});
|
||
const auto remove = adminive::make_drogon_constraints(drogon::Delete, {});
|
||
bind_collection_read_handlers<Data_Feed_Row>(app, data_feeds_path,
|
||
[] { return adminive::to_descriptor_json<Json, Data_Feed_Row>(); },
|
||
[this] { return feed_rows(global_); }, [this] { return feed_amis_schema(global_); }, true);
|
||
app.registerHandler(data_feeds_path, [this](const drogon::HttpRequestPtr& request, std::function<void(const drogon::HttpResponsePtr&)>&& callback) {
|
||
complete_business_request(callback, [this, body = std::string(request->getBody())] {
|
||
std::lock_guard lock(topology_mutex_);
|
||
auto input = read_request_body(body);
|
||
Data_Feed_Row row;
|
||
const auto result = adminive::apply_frontend_create<Json>(row, input);
|
||
if(!result.success)
|
||
return adminive::make_http_error<Json>(422, result);
|
||
if(global_->mode_acs.data_feed_config.map.get(row.key))
|
||
return adminive::make_http_error<Json>(409, "数据馈送名称已存在: " + row.key);
|
||
if(!row.source_key.empty() && !global_->mode_acs.data_source_config.map.get(row.source_key))
|
||
throw std::invalid_argument("连接的数据源不存在: " + row.source_key);
|
||
auto feed = create_data_feed_from_type(feed_type_name(row.type));
|
||
feed->key = row.key;
|
||
apply_feed_row(feed, row);
|
||
global_->mode_acs.data_feed_config.map.push_back(feed);
|
||
Data_feed_Config::set_connections_need_refresh();
|
||
save_data_feeds(global_);
|
||
return adminive::make_http_success<Json>(adminive::to_frontend_json<Json>(feed_row(feed)), "created");
|
||
});
|
||
}, post);
|
||
const auto make_update = [this] {
|
||
return [this](const drogon::HttpRequestPtr& request, std::function<void(const drogon::HttpResponsePtr&)>&& callback, std::uint64_t id) {
|
||
complete_business_request(callback, [this, id, body = std::string(request->getBody())] {
|
||
std::lock_guard lock(topology_mutex_);
|
||
const auto list = global_->mode_acs.data_feed_config.map.list();
|
||
auto feed = item_by_id(list, id);
|
||
auto input = read_request_body(body);
|
||
auto row = feed_row(feed);
|
||
const auto result = adminive::apply_frontend_patch<Json>(row, input);
|
||
if(!result.success)
|
||
return adminive::make_http_error<Json>(422, result);
|
||
if(!row.source_key.empty() && !global_->mode_acs.data_source_config.map.get(row.source_key))
|
||
throw std::invalid_argument("连接的数据源不存在: " + row.source_key);
|
||
apply_feed_row(feed, row);
|
||
Data_feed_Config::set_connections_need_refresh();
|
||
save_data_feeds(global_);
|
||
return adminive::make_http_success<Json>(adminive::to_frontend_json<Json>(feed_row(feed)), result.message);
|
||
});
|
||
};
|
||
};
|
||
app.registerHandler(data_feeds_path + "/{1:id}", make_update(), put);
|
||
app.registerHandler(data_feeds_path + "/{1:id}", make_update(), patch);
|
||
app.registerHandler(data_feeds_path + "/{1:id}", [this](const drogon::HttpRequestPtr&, std::function<void(const drogon::HttpResponsePtr&)>&& callback, std::uint64_t id) {
|
||
complete_business_request(callback, [this, id] {
|
||
std::lock_guard lock(topology_mutex_);
|
||
const auto list = global_->mode_acs.data_feed_config.map.list();
|
||
auto feed = item_by_id(list, id);
|
||
feed->async_stop();
|
||
if(!global_->mode_acs.data_feed_config.map.remove(id - 1))
|
||
throw std::out_of_range("record not found");
|
||
Data_feed_Config::set_connections_need_refresh();
|
||
save_data_feeds(global_);
|
||
return adminive::make_http_success<Json>(adminive::json_object<Json>(), "deleted");
|
||
});
|
||
}, remove);
|
||
app.registerHandler(data_feeds_path + "/order", [this](const drogon::HttpRequestPtr& request, std::function<void(const drogon::HttpResponsePtr&)>&& callback) {
|
||
complete_business_request(callback, [this, body = std::string(request->getBody())] {
|
||
std::lock_guard lock(topology_mutex_);
|
||
const auto list = global_->mode_acs.data_feed_config.map.list();
|
||
const auto keys = ordered_keys(list, read_order_ids(body));
|
||
if(!global_->mode_acs.data_feed_config.map.set_order(keys))
|
||
throw std::invalid_argument("invalid order");
|
||
Data_feed_Config::set_connections_need_refresh();
|
||
save_data_feeds(global_);
|
||
return adminive::make_http_success<Json>(adminive::json_object<Json>(), "reordered");
|
||
});
|
||
}, post);
|
||
}
|
||
Json manifest() const {
|
||
const auto view = adminive::composition_view("ecap_config", adminive::compose::frontend(adminive::compose::slot("mode_acs"), adminive::compose::slot("data_feed_settings"), adminive::compose::slot("mlat"), adminive::compose::slot("map_view"), adminive::compose::slot("cesium_graphics"), adminive::compose::slot("external_resources"), adminive::compose::slot("archive_operations"), adminive::compose::slot("cesium_models"), adminive::compose::slot("backend_config"), adminive::compose::slot("system_actions"))).titled("ECAP 配置");
|
||
const std::vector<adminive::Composition_Slot_Contract> contracts = {
|
||
{"mode_acs", "amis_schema", mode_acs_path + "/descriptor", mode_acs_path + "/view", mode_acs_path + "/data", mode_acs_path + "/amis"},
|
||
{"data_feed_settings", "amis_schema", data_feed_settings_path + "/descriptor", data_feed_settings_path + "/view", data_feed_settings_path + "/data", data_feed_settings_path + "/amis"},
|
||
{"mlat", "amis_schema", mlat_path + "/descriptor", mlat_path + "/view", mlat_path + "/data", mlat_path + "/amis"},
|
||
{"map_view", "amis_schema", map_view_path + "/descriptor", map_view_path + "/view", map_view_path + "/data", map_view_path + "/amis"},
|
||
{"cesium_graphics", "amis_schema", cesium_graphics_path + "/descriptor", cesium_graphics_path + "/view", cesium_graphics_path + "/data", cesium_graphics_path + "/amis"},
|
||
{"external_resources", "amis_schema", external_resources_path + "/descriptor", external_resources_path + "/view", external_resources_path, external_resources_path + "/amis"},
|
||
{"archive_operations", "ecap_archive_operations", {}, {}, {}, {}},
|
||
{"cesium_models", "ecap_cesium_models", {}, {}, "/map/models", {}},
|
||
{"backend_config", "ecap_backend_config", {}, {}, backend_config_path, {}},
|
||
{"system_actions", "amis_schema", {}, {}, {}, {}}
|
||
};
|
||
const std::map<std::string, Json> schemas = {
|
||
{"mode_acs", mode_acs_resource_.amis_schema()},
|
||
{"data_feed_settings", data_feed_settings_resource_.amis_schema()},
|
||
{"mlat", mlat_resource_.amis_schema()},
|
||
{"map_view", map_view_resource_.amis_schema()},
|
||
{"cesium_graphics", cesium_graphics_resource_.amis_schema()},
|
||
{"external_resources", external_resource_amis_schema()},
|
||
{"archive_operations", archive_operations_schema()},
|
||
{"cesium_models", cesium_models_schema(global_)},
|
||
{"backend_config", backend_config_schema()},
|
||
{"system_actions", system_actions_schema()}
|
||
};
|
||
return adminive::to_composition_manifest_json<Json>(view, contracts, schemas);
|
||
}
|
||
static inline const std::string mode_acs_path = "/api/adminive/config/mode_acs";
|
||
static inline const std::string data_feed_settings_path = "/api/adminive/config/data_feed_settings";
|
||
static inline const std::string map_view_path = "/api/adminive/config/map_view";
|
||
static inline const std::string data_sources_path = "/api/adminive/config/data_sources";
|
||
static inline const std::string data_feeds_path = "/api/adminive/config/data_feeds";
|
||
static inline const std::string external_resources_path = "/api/adminive/config/external_resources";
|
||
static inline const std::string backend_config_path = "/api/adminive/config/backend_config";
|
||
static inline const std::string mlat_path = "/api/adminive/config/mlat";
|
||
static inline const std::string cesium_graphics_path = "/api/adminive/config/cesium_graphics";
|
||
static inline const std::string manifest_path = "/api/adminive/config/manifest";
|
||
Global* global_;
|
||
std::mutex topology_mutex_;
|
||
adminive::Drogon_Resource<Mode_ACS_Config_Data, Json> mode_acs_resource_;
|
||
adminive::Drogon_Resource<Data_feed_Config_Data, Json> data_feed_settings_resource_;
|
||
adminive::Drogon_Resource<Map_View_Config, Json> map_view_resource_;
|
||
adminive::Drogon_Resource<MLAT_Config_Data, Json> mlat_resource_;
|
||
adminive::Drogon_Resource<Cesium_Graphics_Config_Data, Json> cesium_graphics_resource_;
|
||
};
|
||
}
|
||
void register_adminive_config(Global* global) {
|
||
static auto resources = std::make_unique<Adminive_Config_Resources>(global);
|
||
resources->bind();
|
||
}
|
||
}
|