From 8370ff9e7961b5191c4f63accff9a7e8afa91070 Mon Sep 17 00:00:00 2001 From: wyc <1104749580@qq.com> Date: Fri, 24 Jul 2026 18:09:37 +0800 Subject: [PATCH] 3D --- config/config.json | 83 +++- module/Local_Server/Aircraft/Flight_VTO.cpp | 53 +++ module/Local_Server/Aircraft/Flight_VTO.h | 56 +-- .../Local_Server/Aircraft/aircraftlist_json.h | 2 +- .../Local_Server/Data_Source/BaseStation.cpp | 10 +- module/Local_Server/Data_Source/BaseStation.h | 1 + module/Local_Server/Data_Source/Data_Source.h | 1 + module/Local_Server/Data_Source/Database.cpp | 6 +- module/Local_Server/server/Config.cpp | 276 +++++++++++ module/Local_Server/server/Config.h | 81 +++- module/Local_Server/server/server.cpp | 12 +- module/Local_Server/server/tiles.cpp | 435 +++++++++++++++--- module/dll_source/main.cpp | 170 ++++++- 13 files changed, 1028 insertions(+), 158 deletions(-) diff --git a/config/config.json b/config/config.json index 5d42831..a872a3c 100644 --- a/config/config.json +++ b/config/config.json @@ -38,8 +38,59 @@ "function_name": "adsb_read" }, "web_server": { - "webapp": "@/../third_party/eacp_webapp/wwwroot", - "tiles": "D:/tiles" + "webapp": "@/../third_party/eacp_webapp/wwwroot" + }, + "map_resources": { + "current_imagery_key": "google_imagery", + "imagery": { + "directories": [ + "D:/ae/tiles/Esri_World_Imagery_0-9", + "D:/ae/tiles/Esri_World_Imagery_10-12" + ], + "path_layout": "z/x/y", + "y_axis": "xyz", + "projection": "web_mercator", + "extension": ".png", + "tile_size": 256, + "minimum_level": 0, + "maximum_level": 11 + }, + "google_imagery": { + "directories": [ + "D:/tiles" + ], + "path_layout": "z/x/y", + "y_axis": "xyz", + "projection": "web_mercator", + "extension": ".png", + "tile_size": 256, + "minimum_level": 1, + "maximum_level": 10 + }, + "terrain": { + "directories": [ + "D:/ae/tiles/AWS_Terrarium_0-15" + ], + "path_layout": "z/x/y", + "y_axis": "xyz", + "projection": "web_mercator", + "extension": ".png", + "tile_size": 256, + "minimum_level": 0, + "maximum_level": 10, + "encoding": "terrarium" + } + }, + "map_view": { + "scene_mode": "3d", + "camera": { + "longitude": 121.27, + "latitude": 37.41, + "height": 300000, + "heading": 0, + "pitch": -90, + "roll": 0 + } }, "device": { "net": [ @@ -161,13 +212,14 @@ "key": "lzy_dll", "enable": true, "base_station_show": true, + "base_station_has_valid_position": false, "aircraft_show": true, "color": "#454641", - "aircraft_pixel_size": 28, + "aircraft_pixel_size": 100, "lat": 37.433547, "lon": 121.408730, - "alt": 23.000000, - "update_form_gps": true, + "alt": 2300.000000, + "update_form_gps": false, "show_icao": true, "show_call_sign": false, "show_fly_status": false, @@ -183,12 +235,13 @@ "key": "aaaa", "enable": false, "base_station_show": true, + "base_station_has_valid_position": false, "aircraft_show": true, "color": "#d1a54c", - "aircraft_pixel_size": 20, + "aircraft_pixel_size": 100, "lat": 37.000000, "lon": 121.000000, - "alt": 0.000000, + "alt": 1000.000000, "update_form_gps": false, "show_icao": true, "show_call_sign": true, @@ -303,5 +356,21 @@ "last_updated_at": 1780381804 } ] + }, + "cesium_graphics": { + "debug_show_frames_per_second": true, + "target_frame_rate": 30, + "use_browser_recommended_resolution": true, + "resolution_scale": 0.7, + "msaa_samples": 1, + "fxaa": false, + "shadows": false, + "enable_lighting": false, + "maximum_screen_space_error": 4, + "terrain_enabled_in_3d": false, + "terrain_exaggeration": 1.0, + "terrain_limit_maximum_level": true, + "terrain_maximum_level": 15, + "terrain_cache_tiles": 128 } } diff --git a/module/Local_Server/Aircraft/Flight_VTO.cpp b/module/Local_Server/Aircraft/Flight_VTO.cpp index e5c1cde..2a581b8 100644 --- a/module/Local_Server/Aircraft/Flight_VTO.cpp +++ b/module/Local_Server/Aircraft/Flight_VTO.cpp @@ -1 +1,54 @@ #include "../Aircraft/Flight_VTO.h" +Psc::JSON Flight_VTO::to_json() { + auto ret = Psc::JSON::object(); + ADD_Json(icao); + ADD_Json(mlat); + ret.children.emplace_back("time_stamp", time_to_string(time_stamp)); + if (pos.has_value()) { + ret.children.emplace_back("latitude", pos.value().lat); + ret.children.emplace_back("longitude", pos.value().lon); + } + else { + ret.children.emplace_back("latitude", nullptr); + ret.children.emplace_back("longitude", nullptr); + } + ADD_Json_RET(track); + ADD_Json_RET(speed); + ADD_Json_RET(altitude); + ADD_Json_RET(vert_speed); + ADD_Json_RET(call_sign); + ADD_Json_RET(squawk); + return ret; +} +std::string Flight_VTO::time_to_string(time_t time_stamp) { + const std::tm* utc_time = std::gmtime(&time_stamp); + char buffer[80]; + std::strftime(buffer, sizeof(buffer), "%H:%M:%S", utc_time); + return {buffer}; +} +Flight_VTO Flight_VTO::to_VTO(SSR::Aircraft_Info* info) { + Flight_VTO vto; + vto.icao = info->icao; + vto.mlat = info->mlat; + vto.time_stamp = info->timestamp; + vto.speed = info->speed(); + // 这个地 是相对于地的意思 + vto.track = info->surface_magnetic_heading(); + // vto.pos = info->pos(); + auto opt = info->air_pos_track_list.last(); + vto.pos = opt; + if (opt.has_value()) { + SSR::Position_Info& a = opt.value(); + vto.altitude = a.alt; + } + // auto val = info->surface_pos_track_list.last(); + // vto.pos = val; + // if (val.has_value()) { + // vto.altitude = val.value().alt; + // std::cout << "222222" << std::endl; + // } + vto.vert_speed = info->vertical_rate(); + vto.call_sign = info->bds20_call_sign(); + vto.squawk = info->squawk(); + return vto; +} diff --git a/module/Local_Server/Aircraft/Flight_VTO.h b/module/Local_Server/Aircraft/Flight_VTO.h index d7f7021..a69a655 100644 --- a/module/Local_Server/Aircraft/Flight_VTO.h +++ b/module/Local_Server/Aircraft/Flight_VTO.h @@ -1,7 +1,5 @@ #pragma once #include "global.h" - - struct Flight_VTO { std::string icao; // 飞机身份标识(四字节十六进制代码) time_t time_stamp{}; // 时间(原始时间数据,可能需要翻译显示) @@ -13,55 +11,7 @@ struct Flight_VTO { RET altitude; // 米 RET call_sign; RET squawk; - - - Psc::JSON toJson() { - auto ret = Psc::JSON::object(); - ADD_Json(icao); - ADD_Json(mlat); - ret.children.emplace_back("time_stamp", timeToString(time_stamp)); - if (pos.has_value()) - { - ret.children.emplace_back("latitude", pos.value().lat); - ret.children.emplace_back("longitude", pos.value().lon); - } else - { - ret.children.emplace_back("latitude", nullptr); - ret.children.emplace_back("longitude", nullptr); - } - ADD_Json_RET(track); - ADD_Json_RET(speed); - ADD_Json_RET(altitude); - ADD_Json_RET(vert_speed); - ADD_Json_RET(call_sign); - ADD_Json_RET(squawk); - return ret; - } - - std::string timeToString(time_t time_stamp) { - struct tm* utc_time = std::gmtime(&time_stamp); // 转换为 UTC 时间 - char buffer[80]; - - // 使用 strftime 格式化时间为字符串 - std::strftime(buffer, sizeof(buffer), "%H:%M:%S", utc_time); - - return std::string(buffer); // 返回格式化后的时间字符串 - } - static Flight_VTO to_VTO(SSR::Aircraft_Info* info) { - Flight_VTO vto; - vto.icao = info->icao; - vto.mlat = info->mlat; - vto.time_stamp = info->timestamp; - vto.speed = info->speed(); - // 这个地 是相对于地的意思 - vto.track = info->surface_magnetic_heading(); - // vto.pos = info->pos(); - vto.pos = info->air_pos_track_list.last(); - vto.altitude = info->altitude_meter(); - vto.vert_speed = info->vertical_rate(); - vto.call_sign = info->bds20_call_sign(); - vto.squawk = info->squawk(); - return vto; - } + Psc::JSON to_json(); + static std::string time_to_string(time_t time_stamp); + static Flight_VTO to_VTO(SSR::Aircraft_Info* info); }; - diff --git a/module/Local_Server/Aircraft/aircraftlist_json.h b/module/Local_Server/Aircraft/aircraftlist_json.h index 79a187b..ff313ac 100644 --- a/module/Local_Server/Aircraft/aircraftlist_json.h +++ b/module/Local_Server/Aircraft/aircraftlist_json.h @@ -129,7 +129,7 @@ struct Aircraft_List_JSON_Service_O { ret.src = "暂不支持"; // ret.cla ret.cla = "暂不支持"; - RET pos = info->pos(); + RET pos = info->air_pos_track_list.last(); if(pos.has_value()) { ret.lat = pos.value().lat; ret.lon = pos.value().lon; diff --git a/module/Local_Server/Data_Source/BaseStation.cpp b/module/Local_Server/Data_Source/BaseStation.cpp index 22ac487..0406bf0 100644 --- a/module/Local_Server/Data_Source/BaseStation.cpp +++ b/module/Local_Server/Data_Source/BaseStation.cpp @@ -6,8 +6,15 @@ struct HULC_Status_Message; std::optional BaseStation::get_pos() { std::lock_guard g(mtx); + if (!hulc.GPS_has_valid_fix()) { + return std::nullopt; + } return SSR::CPR::Position{hulc.get_latitude(), hulc.get_longitude()}; } +bool BaseStation::has_valid_position() { + std::lock_guard g(mtx); + return hulc.GPS_has_valid_fix(); +} void BaseStation::set_msg(const SSR::HULC_Status_Message &msg) { std::lock_guard g(mtx); hulc = msg; @@ -34,9 +41,10 @@ Psc::JSON BaseStation::to_Json() { ret.append({"GPS设备检测到", hulc.GPS_device_detected()}); ret.append({"GPS有效", hulc.GPS_valid()}); ret.append({"GPS有效定位", hulc.GPS_has_valid_fix()}); + ret.append({"has_valid_position", hulc.GPS_has_valid_fix()}); ret.append({"GPS高精度时间", hulc.GPS_high_accuracy_time()}); ret.append({"启动以来TX队列溢出", hulc.TX_queue_overflow_since_startup()}); ret.append({"过去一秒TX队列溢出", hulc.TX_queue_overflow_last_second()}); ret.append({"发现过多NMEA数据", hulc.excessive_NMEA_found()}); return ret; -} \ No newline at end of file +} diff --git a/module/Local_Server/Data_Source/BaseStation.h b/module/Local_Server/Data_Source/BaseStation.h index 432f649..d60c599 100644 --- a/module/Local_Server/Data_Source/BaseStation.h +++ b/module/Local_Server/Data_Source/BaseStation.h @@ -8,6 +8,7 @@ public: std::string to_string(); Psc::JSON to_Json(); std::optional get_pos(); + bool has_valid_position(); void set_msg(const SSR::HULC_Status_Message& msg); std::function handle_when_updated = nullptr; protected: diff --git a/module/Local_Server/Data_Source/Data_Source.h b/module/Local_Server/Data_Source/Data_Source.h index ebf6b36..cb7db6b 100644 --- a/module/Local_Server/Data_Source/Data_Source.h +++ b/module/Local_Server/Data_Source/Data_Source.h @@ -53,6 +53,7 @@ protected: class Data_Source_Data { public: Psc::Copyable_Atomic base_station_show{}; + Psc::Copyable_Atomic base_station_has_valid_position{}; Psc::Copyable_Atomic aircraft_show{}; std::string color = "#1677ff"; int aircraft_pixel_size{}; diff --git a/module/Local_Server/Data_Source/Database.cpp b/module/Local_Server/Data_Source/Database.cpp index 8c7a410..1e077fd 100644 --- a/module/Local_Server/Data_Source/Database.cpp +++ b/module/Local_Server/Data_Source/Database.cpp @@ -82,7 +82,7 @@ JSON DataBase::get_all_aircraft_json() { auto vto = Flight_VTO::to_VTO(it.get()); bool have_pos = !it.get()->air_pos_track_list.empty(); if (have_pos) { - json.children.push_back(vto.toJson()); + json.children.push_back(vto.to_json()); num++; } } @@ -97,7 +97,7 @@ JSON DataBase::get_aircraft_list_after(time_t timestamp) { auto vto = Flight_VTO::to_VTO(it.get()); bool have_pos = !it.get()->air_pos_track_list.empty(); if (have_pos && vto.time_stamp > timestamp) { - json.children.push_back(vto.toJson()); + json.children.push_back(vto.to_json()); } } return json; @@ -198,7 +198,7 @@ void database_server(Global *g) { JSON ret; if (aircraft != nullptr) { auto vto = Flight_VTO::to_VTO(aircraft.get()); - ret = vto.toJson(); + ret = vto.to_json(); } res->setBody(ret.to_json_string()); }); diff --git a/module/Local_Server/server/Config.cpp b/module/Local_Server/server/Config.cpp index 615ac1a..e92dc52 100644 --- a/module/Local_Server/server/Config.cpp +++ b/module/Local_Server/server/Config.cpp @@ -2,6 +2,7 @@ #include "Core/Base/Check.h" #include "Core/Net_Adapter/Net_Adapter.h" #include "Global.h" +#include #include #include #undef interface @@ -26,6 +27,281 @@ std::string get_config_path() { } return ret; } +static std::vector read_string_array(const Psc::JSON* json, std::string_view key) { + auto field = json->get(key); + if (field == nullptr || field->valueType != Psc::Array) { + throw Psc::json_assign_error(std::make_error_code(std::errc::invalid_argument), std::string(key)); + } + std::vector ret; + ret.reserve(field->children.size()); + for (const auto& child : field->children) { + if (child.valueType != Psc::String) { + throw Psc::json_assign_error(std::make_error_code(std::errc::invalid_argument), std::string(key)); + } + ret.emplace_back(child.val); + } + return ret; +} +static Psc::JSON write_string_array(std::string_view key, const std::vector& list) { + auto array = Psc::JSON::array(); + for (const auto& path : list) { + array.append(Psc::JSON(path)); + } + return Psc::JSON(key, array); +} +static std::string read_string_field(const Psc::JSON* json, std::string_view key) { + auto field = json->get(key); + if (field == nullptr || field->valueType != Psc::String) { + throw Psc::json_assign_error(std::make_error_code(std::errc::invalid_argument), std::string(key)); + } + return field->val; +} +static std::uint32_t read_uint32_field(const Psc::JSON* json, std::string_view key) { + auto field = json->get(key); + if (field == nullptr || field->valueType != Psc::Number) { + throw Psc::json_assign_error(std::make_error_code(std::errc::invalid_argument), std::string(key)); + } + return field->number_val(); +} +static std::string read_optional_string_field(const Psc::JSON* json, std::string_view key) { + auto field = json->get(key); + if (field == nullptr) { + return {}; + } + if (field->valueType != Psc::String) { + throw Psc::json_assign_error(std::make_error_code(std::errc::invalid_argument), std::string(key)); + } + return field->val; +} +static std::string read_optional_string_field(const Psc::JSON* json, std::string_view key, std::string_view fallback) { + auto field = json->get(key); + if (field == nullptr) { + return std::string(fallback); + } + if (field->valueType != Psc::String) { + throw Psc::json_assign_error(std::make_error_code(std::errc::invalid_argument), std::string(key)); + } + return field->val; +} +static bool read_optional_bool_field(const Psc::JSON* json, std::string_view key, bool fallback) { + auto field = json->get(key); + if (field == nullptr) { + return fallback; + } + if (field->valueType != Psc::Bool) { + throw Psc::json_assign_error(std::make_error_code(std::errc::invalid_argument), std::string(key)); + } + return field->bool_val(); +} +static std::uint32_t read_optional_uint32_field(const Psc::JSON* json, std::string_view key, std::uint32_t fallback) { + auto field = json->get(key); + if (field == nullptr) { + return fallback; + } + if (field->valueType != Psc::Number) { + throw Psc::json_assign_error(std::make_error_code(std::errc::invalid_argument), std::string(key)); + } + return field->number_val(); +} +static double read_optional_double_field(const Psc::JSON* json, std::string_view key, double fallback) { + auto field = json->get(key); + if (field == nullptr) { + return fallback; + } + if (field->valueType != Psc::Number) { + throw Psc::json_assign_error(std::make_error_code(std::errc::invalid_argument), std::string(key)); + } + return field->number_val(); +} +void Map_Tile_Config::from_base_json(const Psc::JSON* that_json) { + if (that_json == nullptr || that_json->valueType != Psc::Object) { + throw Psc::json_assign_error(std::make_error_code(std::errc::invalid_argument), "map tile config"); + } + directories = read_string_array(that_json, "directories"); + path_layout = read_string_field(that_json, "path_layout"); + y_axis = read_string_field(that_json, "y_axis"); + projection = read_string_field(that_json, "projection"); + extension = read_string_field(that_json, "extension"); + tile_size = read_uint32_field(that_json, "tile_size"); + minimum_level = read_uint32_field(that_json, "minimum_level"); + maximum_level = read_uint32_field(that_json, "maximum_level"); + encoding = read_optional_string_field(that_json, "encoding"); +} +Psc::JSON Map_Tile_Config::to_base_json() const { + auto ret = Psc::JSON::object(); + ret.append(write_string_array("directories", directories)); + ret.append({"path_layout", path_layout}); + ret.append({"y_axis", y_axis}); + ret.append({"projection", projection}); + ret.append({"extension", extension}); + ret.append({"tile_size", tile_size}); + ret.append({"minimum_level", minimum_level}); + ret.append({"maximum_level", maximum_level}); + if (!encoding.empty()) { + ret.append({"encoding", encoding}); + } + return ret; +} +Psc::JSON Map_Tile_Config::to_public_json(std::string_view url) const { + auto ret = Psc::JSON::object(); + ret.append({"url", std::string(url)}); + ret.append({"minimum_level", minimum_level}); + ret.append({"maximum_level", maximum_level}); + ret.append({"tile_size", tile_size}); + ret.append({"projection", projection}); + ret.append({"y_axis", y_axis}); + if (!encoding.empty()) { + ret.append({"encoding", encoding}); + } + return ret; +} +void Map_Resources_Config::from_base_json(const Psc::JSON* that_json) { + if (that_json == nullptr || that_json->valueType != Psc::Object) { + throw Psc::json_assign_error(std::make_error_code(std::errc::invalid_argument), "map_resources"); + } + current_imagery_key = read_optional_string_field(that_json, "current_imagery_key"); + if (current_imagery_key.empty()) { + current_imagery_key = "imagery"; + } + imagery.from_base_json(that_json->get("imagery")); + google_imagery.from_base_json(that_json->get("google_imagery")); + terrain.from_base_json(that_json->get("terrain")); + if (!is_imagery_key(current_imagery_key)) { + current_imagery_key = "imagery"; + } +} +Psc::JSON Map_Resources_Config::to_base_json() const { + auto ret = Psc::JSON::object(); + ret.append({"current_imagery_key", current_imagery_key}); + ret.append({"imagery", imagery.to_base_json()}); + ret.append({"google_imagery", google_imagery.to_base_json()}); + ret.append({"terrain", terrain.to_base_json()}); + return ret; +} +static Psc::JSON make_public_imagery_source(std::string_view key, std::string_view name, const Psc::JSON& resource) { + auto ret = Psc::JSON::object(); + ret.append({"key", std::string(key)}); + ret.append({"name", std::string(name)}); + ret.append({"resource", resource}); + return ret; +} +static Psc::JSON make_osm_public_resource() { + auto ret = Psc::JSON::object(); + ret.append({"url", "https://tile.openstreetmap.org/{z}/{x}/{y}.png"}); + ret.append({"minimum_level", 0}); + ret.append({"maximum_level", 19}); + ret.append({"tile_size", 256}); + ret.append({"projection", "web_mercator"}); + ret.append({"y_axis", "xyz"}); + ret.append({"credit", "OpenStreetMap contributors"}); + return ret; +} +Psc::JSON Map_Resources_Config::to_public_json() const { + auto ret = Psc::JSON::object(); + auto imagery_json = imagery.to_public_json("/map/imagery/{z}/{x}/{y}"); + auto google_imagery_json = google_imagery.to_public_json("/tiles/{z}/{x}/{y}"); + auto osm_json = make_osm_public_resource(); + auto sources = Psc::JSON::array(); + sources.append(make_public_imagery_source("imagery", "本地卫星影像", imagery_json)); + sources.append(make_public_imagery_source("google_imagery", "本地Google瓦片", google_imagery_json)); + sources.append(make_public_imagery_source("osm_online", "OpenStreetMap在线", osm_json)); + ret.append({"current_imagery_key", current_imagery_key}); + ret.append({"imagery_sources", sources}); + ret.append({"imagery", imagery_json}); + ret.append({"google_imagery", google_imagery_json}); + ret.append({"terrain", terrain.to_public_json("/map/terrain/{z}/{x}/{y}")}); + return ret; +} +bool Map_Resources_Config::is_imagery_key(std::string_view key) const { + return key == "imagery" || key == "google_imagery" || key == "osm_online"; +} +void Map_Camera_Config::from_base_json(const Psc::JSON* that_json) { + if (that_json == nullptr) { + return; + } + if (that_json->valueType != Psc::Object) { + throw Psc::json_assign_error(std::make_error_code(std::errc::invalid_argument), "map camera"); + } + longitude = read_optional_double_field(that_json, "longitude", longitude); + latitude = read_optional_double_field(that_json, "latitude", latitude); + height = read_optional_double_field(that_json, "height", height); + heading = read_optional_double_field(that_json, "heading", heading); + pitch = read_optional_double_field(that_json, "pitch", pitch); + roll = read_optional_double_field(that_json, "roll", roll); +} +Psc::JSON Map_Camera_Config::to_base_json() const { + auto ret = Psc::JSON::object(); + ret.append({"longitude", longitude}); + ret.append({"latitude", latitude}); + ret.append({"height", height}); + ret.append({"heading", heading}); + ret.append({"pitch", pitch}); + ret.append({"roll", roll}); + return ret; +} +bool Map_View_Config::is_scene_mode(std::string_view mode) { + return mode == "2d" || mode == "2.5d" || mode == "3d"; +} +void Map_View_Config::from_base_json(const Psc::JSON* that_json) { + if (that_json == nullptr) { + return; + } + if (that_json->valueType != Psc::Object) { + throw Psc::json_assign_error(std::make_error_code(std::errc::invalid_argument), "map_view"); + } + auto next_scene_mode = read_optional_string_field(that_json, "scene_mode", scene_mode); + if (!is_scene_mode(next_scene_mode)) { + throw Psc::json_assign_error(std::make_error_code(std::errc::invalid_argument), "scene_mode"); + } + scene_mode = next_scene_mode; + camera.from_base_json(that_json->get("camera")); +} +Psc::JSON Map_View_Config::to_base_json() const { + auto ret = Psc::JSON::object(); + ret.append({"scene_mode", scene_mode}); + ret.append({"camera", camera.to_base_json()}); + return ret; +} +void Cesium_Graphics_Config::from_base_json(const Psc::JSON* that_json) { + if (that_json == nullptr) { + return; + } + if (that_json->valueType != Psc::Object) { + throw Psc::json_assign_error(std::make_error_code(std::errc::invalid_argument), "cesium_graphics"); + } + debug_show_frames_per_second = read_optional_bool_field(that_json, "debug_show_frames_per_second", debug_show_frames_per_second); + target_frame_rate = std::clamp(read_optional_uint32_field(that_json, "target_frame_rate", target_frame_rate), 1u, 120u); + use_browser_recommended_resolution = read_optional_bool_field(that_json, "use_browser_recommended_resolution", use_browser_recommended_resolution); + resolution_scale = std::clamp(read_optional_double_field(that_json, "resolution_scale", resolution_scale), 0.2, 1.5); + msaa_samples = std::clamp(read_optional_uint32_field(that_json, "msaa_samples", msaa_samples), 1u, 8u); + fxaa = read_optional_bool_field(that_json, "fxaa", fxaa); + shadows = read_optional_bool_field(that_json, "shadows", shadows); + enable_lighting = read_optional_bool_field(that_json, "enable_lighting", enable_lighting); + maximum_screen_space_error = std::clamp(read_optional_uint32_field(that_json, "maximum_screen_space_error", maximum_screen_space_error), 1u, 16u); + terrain_enabled_in_3d = read_optional_bool_field(that_json, "terrain_enabled_in_3d", terrain_enabled_in_3d); + terrain_exaggeration = std::clamp(read_optional_double_field(that_json, "terrain_exaggeration", terrain_exaggeration), 0.0, 5.0); + terrain_limit_maximum_level = read_optional_bool_field(that_json, "terrain_limit_maximum_level", terrain_limit_maximum_level); + terrain_maximum_level = std::clamp(read_optional_uint32_field(that_json, "terrain_maximum_level", terrain_maximum_level), 0u, 24u); + terrain_cache_tiles = std::clamp(read_optional_uint32_field(that_json, "terrain_cache_tiles", terrain_cache_tiles), 16u, 2048u); +} +Psc::JSON Cesium_Graphics_Config::to_base_json() const { + auto ret = Psc::JSON::object(); + ret.append({"debug_show_frames_per_second", debug_show_frames_per_second}); + ret.append({"target_frame_rate", target_frame_rate}); + ret.append({"use_browser_recommended_resolution", use_browser_recommended_resolution}); + ret.append({"resolution_scale", resolution_scale}); + ret.append({"msaa_samples", msaa_samples}); + ret.append({"fxaa", fxaa}); + ret.append({"shadows", shadows}); + ret.append({"enable_lighting", enable_lighting}); + ret.append({"maximum_screen_space_error", maximum_screen_space_error}); + ret.append({"terrain_enabled_in_3d", terrain_enabled_in_3d}); + ret.append({"terrain_exaggeration", terrain_exaggeration}); + ret.append({"terrain_limit_maximum_level", terrain_limit_maximum_level}); + ret.append({"terrain_maximum_level", terrain_maximum_level}); + ret.append({"terrain_cache_tiles", terrain_cache_tiles}); + return ret; +} void Mode_ACS_Config::server(Global *g) { auto &svr = g->svr; diff --git a/module/Local_Server/server/Config.h b/module/Local_Server/server/Config.h index c0d5aa3..c64a742 100644 --- a/module/Local_Server/server/Config.h +++ b/module/Local_Server/server/Config.h @@ -7,6 +7,7 @@ #include "MLAT.h" #include "Source_Feed_Relation.h" #include +#include inline bool delete_log = false; extern std::string default_config_path; std::string get_config_path(); @@ -102,12 +103,75 @@ struct Web_Server_Config { [[nodiscard]] std::string get_true_webapp() const { return Psc::get_abs_path(webapp); } - [[nodiscard]] std::string get_true_tiles() const { - return Psc::get_abs_path(tiles); - } PSC_USE_JSON std::string webapp; - std::string tiles; +}; +struct Map_Tile_Config { + std::vector directories; + std::string path_layout; + std::string y_axis; + std::string projection; + std::string extension; + std::uint32_t tile_size{}; + std::uint32_t minimum_level{}; + std::uint32_t maximum_level{}; + std::string encoding; + [[nodiscard]] std::vector get_true_directories() const { + std::vector ret; + ret.reserve(directories.size()); + for (const auto& path : directories) { + ret.emplace_back(Psc::get_abs_path(path)); + } + return ret; + } + void from_base_json(const Psc::JSON* that_json); + [[nodiscard]] Psc::JSON to_base_json() const; + [[nodiscard]] Psc::JSON to_public_json(std::string_view url) const; +}; +struct Map_Resources_Config { + std::string current_imagery_key = "imagery"; + Map_Tile_Config imagery; + Map_Tile_Config google_imagery; + Map_Tile_Config terrain; + void from_base_json(const Psc::JSON* that_json); + [[nodiscard]] Psc::JSON to_base_json() const; + [[nodiscard]] Psc::JSON to_public_json() const; + [[nodiscard]] bool is_imagery_key(std::string_view key) const; +}; +struct Map_Camera_Config { + double longitude = 121.27; + double latitude = 37.41; + double height = 300000.0; + double heading = 0.0; + double pitch = -90.0; + double roll = 0.0; + void from_base_json(const Psc::JSON* that_json); + [[nodiscard]] Psc::JSON to_base_json() const; +}; +struct Map_View_Config { + std::string scene_mode = "3d"; + Map_Camera_Config camera; + void from_base_json(const Psc::JSON* that_json); + [[nodiscard]] Psc::JSON to_base_json() const; + [[nodiscard]] static bool is_scene_mode(std::string_view mode); +}; +struct Cesium_Graphics_Config { + bool debug_show_frames_per_second = true; + std::uint32_t target_frame_rate = 30; + bool use_browser_recommended_resolution = true; + double resolution_scale = 0.7; + std::uint32_t msaa_samples = 1; + bool fxaa = false; + bool shadows = false; + bool enable_lighting = false; + std::uint32_t maximum_screen_space_error = 4; + bool terrain_enabled_in_3d = false; + double terrain_exaggeration = 1.0; + bool terrain_limit_maximum_level = true; + std::uint32_t terrain_maximum_level = 15; + std::uint32_t terrain_cache_tiles = 128; + void from_base_json(const Psc::JSON* that_json); + [[nodiscard]] Psc::JSON to_base_json() const; }; struct Net_Config { std::string key{}; @@ -162,6 +226,9 @@ public: MLAT_Config mlat; Device_Config device_config; Web_Server_Config web_server_config; + Map_Resources_Config map_resources_config; + Map_View_Config map_view_config; + Cesium_Graphics_Config cesium_graphics_config; Mode_ACS_Config mode_acs; Log_Config log_config; Console_Config console_config{}; @@ -191,6 +258,9 @@ public: Get_J(version) mlat.init(that_json->get("mlat")); web_server_config.from_base_json(that_json->get("web_server")); + map_resources_config.from_base_json(that_json->get("map_resources")); + map_view_config.from_base_json(that_json->get("map_view")); + cesium_graphics_config.from_base_json(that_json->get("cesium_graphics")); device_config.init(that_json->get("device")); mode_acs.init(that_json->get("mode_acs")); console_config.from_base_json(that_json->get("console")); @@ -207,12 +277,15 @@ public: {"http_monitor", http_monitor_config.to_base_json()}, {"mlat", mlat.to_base_json()}, {"web_server", web_server_config.to_base_json()}, + {"map_resources", map_resources_config.to_base_json()}, + {"map_view", map_view_config.to_base_json()}, {"device", device_config.to_base_json()}, {"mode_acs", mode_acs.to_base_json()}, //{"ais", ais.to_json()}, {"log", log_config.to_base_json()}, {"console", console_config.to_base_json()}, {"external_database", external_resources_manager.to_base_json()}, + {"cesium_graphics", cesium_graphics_config.to_base_json()}, }); return ret; } diff --git a/module/Local_Server/server/server.cpp b/module/Local_Server/server/server.cpp index 1a226c2..66a945a 100644 --- a/module/Local_Server/server/server.cpp +++ b/module/Local_Server/server/server.cpp @@ -183,6 +183,7 @@ void Global::init_web_server() { {drogon::Get}); add_path(R"(^/settings)"); add_path(R"(^/map)"); + add_path(R"(^/map3d)"); add_path(R"(^/dsp)"); add_path(R"(^/aircraftlist)"); drogon::app().registerHandlerViaRegex( @@ -193,8 +194,15 @@ void Global::init_web_server() { std::cout << req->path() << std::endl; auto res = drogon::HttpResponse::newHttpResponse(); auto path = web_server.get_true_webapp(); - auto rp = "/" + req->path().substr(3); - if (rp == "/") { + auto rp = req->path().substr(3); + if (rp.empty()) { + rp = "/"; + } + auto is_spa_path = [](std::string_view value) { + return value == "/" || value == "/map" || value == "/map3d" || + value == "/settings" || value == "/dsp" || value == "/aircraftlist"; + }; + if (is_spa_path(rp)) { path += "/index.html"; } else { diff --git a/module/Local_Server/server/tiles.cpp b/module/Local_Server/server/tiles.cpp index c224615..ff40368 100644 --- a/module/Local_Server/server/tiles.cpp +++ b/module/Local_Server/server/tiles.cpp @@ -1,31 +1,52 @@ #include "Global.h" #include "Performance_Monitor.h" +#include +#include +#include +#include +#include #include #include #include #include +#include #include +#include #include #include #include -/** - * 瓦片文件缓存策略: - * * 首次请求通过 newFileResponse() 直接发送文件,避免先把整个文件读取到内存。 - * * 根据文件修改时间和大小生成弱 ETag,同时返回 Last-Modified。 - * * 浏览器缓存未过期时直接使用本地缓存,不再请求服务器。 - * * 缓存过期或强制刷新时,浏览器自动携带 If-None-Match 或 If-Modified-Since。 - * * 服务端优先验证 If-None-Match,文件未变化时返回 304,不重复传输文件内容。 - * * 404 响应使用 no-store,避免不存在的瓦片被浏览器长期缓存。 - * * max-age=31536000, immutable 仅适用于同一 URL 对应的文件永远不被原地修改; - * 如果瓦片可能在原路径更新,应移除 immutable 并使用较短的 max-age。 - */ +#include namespace { +constexpr std::uint64_t kMaxTileFileSize = 32ull * 1024ull * 1024ull; +enum class Tile_Load_Status { + Ready, + Not_Found, + Not_Modified +}; +struct Tile_Request { + std::uint32_t z; + std::uint32_t x; + std::uint32_t y; + std::string extension; +}; +struct Tile_Cache_Request { + std::string if_none_match; + std::string if_modified_since; + bool include_body; +}; struct File_Cache_Info { std::uint64_t size; std::time_t modified_time; std::string etag; std::string last_modified; }; +struct Tile_Load_Result { + Tile_Load_Status status; + std::string path; + std::string content; + std::string content_type; + File_Cache_Info cache_info; +}; std::string format_http_date(std::time_t time) { std::tm tm{}; #ifdef _WIN32 @@ -37,26 +58,22 @@ std::string format_http_date(std::time_t time) { auto size = std::strftime(buffer, sizeof(buffer), "%a, %d %b %Y %H:%M:%S GMT", &tm); return std::string(buffer, size); } -std::optional get_file_cache_info(const std::string& path) { - auto native_path = std::filesystem::path(path); +std::optional get_file_cache_info(const std::filesystem::path& path) { #ifdef _WIN32 struct _stati64 info{}; - if (_wstati64(native_path.c_str(), &info) != 0 || (info.st_mode & _S_IFMT) != _S_IFREG) { + if (_wstati64(path.c_str(), &info) != 0 || (info.st_mode & _S_IFMT) != _S_IFREG || info.st_size < 0) { return std::nullopt; } #else struct stat info{}; - if (::stat(native_path.c_str(), &info) != 0 || !S_ISREG(info.st_mode)) { + if (::stat(path.c_str(), &info) != 0 || !S_ISREG(info.st_mode) || info.st_size < 0) { return std::nullopt; } #endif File_Cache_Info result; result.size = static_cast(info.st_size); result.modified_time = info.st_mtime; - result.etag = std::format( - "W/\"{:x}-{:x}\"", - static_cast(result.modified_time), - result.size); + result.etag = std::format("W/\"{:x}-{:x}\"", static_cast(result.modified_time), result.size); result.last_modified = format_http_date(result.modified_time); return result; } @@ -89,73 +106,355 @@ bool etag_matches(std::string_view values, std::string_view current_etag) { } return false; } -void add_tile_cache_headers( - const drogon::HttpResponsePtr& response, - const File_Cache_Info& info) { +bool is_not_modified(const Tile_Cache_Request& request, const File_Cache_Info& info) { + if (!request.if_none_match.empty()) { + return etag_matches(request.if_none_match, info.etag); + } + return !request.if_modified_since.empty() && request.if_modified_since == info.last_modified; +} +void add_tile_cache_headers(const drogon::HttpResponsePtr& response, const File_Cache_Info& info, bool include_content_length) { response->addHeader("Cache-Control", "public, max-age=86400"); - //response->addHeader("Cache-Control", "public, max-age=31536000, immutable"); response->addHeader("ETag", info.etag); response->addHeader("Last-Modified", info.last_modified); + if (include_content_length) { + response->addHeader("Content-Length", std::to_string(info.size)); + } } -drogon::HttpResponsePtr make_tile_not_found_response(const std::string& path) { +bool is_unsigned_path_component(std::string_view value) { + if (value.empty()) { + return false; + } + return std::ranges::all_of(value, [](unsigned char ch) { + return std::isdigit(ch) != 0; + }); +} +std::uint32_t parse_uint32_component(std::string_view value) { + std::uint64_t result = 0; + for (char ch : value) { + result = result * 10 + static_cast(ch - '0'); + if (result > std::numeric_limits::max()) { + throw std::out_of_range("tile coordinate is too large"); + } + } + return static_cast(result); +} +std::optional parse_tile_request(std::string_view path, std::string_view prefix, std::string_view default_extension) { + if (!path.starts_with(prefix)) { + return std::nullopt; + } + path.remove_prefix(prefix.size()); + auto first = path.find('/'); + auto second = first == std::string_view::npos ? std::string_view::npos : path.find('/', first + 1); + if (first == std::string_view::npos || second == std::string_view::npos || path.find('/', second + 1) != std::string_view::npos) { + return std::nullopt; + } + auto z = path.substr(0, first); + auto x = path.substr(first + 1, second - first - 1); + auto y_with_extension = path.substr(second + 1); + auto dot = y_with_extension.rfind('.'); + auto y = dot == std::string_view::npos ? y_with_extension : y_with_extension.substr(0, dot); + auto extension = dot == std::string_view::npos ? std::string(default_extension) : std::string(y_with_extension.substr(dot)); + if (!is_unsigned_path_component(z) || !is_unsigned_path_component(x) || !is_unsigned_path_component(y) || extension != default_extension) { + return std::nullopt; + } + return Tile_Request{parse_uint32_component(z), parse_uint32_component(x), parse_uint32_component(y), extension}; +} +class Tile_Path_Resolver { +public: + explicit Tile_Path_Resolver(const Map_Tile_Config& config) + : config_(config), directories_(config.get_true_directories()) { + if (config_.path_layout != "z/x/y" && config_.path_layout != "z/y/x") { + throw std::invalid_argument("invalid tile path_layout: " + config_.path_layout); + } + if (config_.extension.empty() || config_.extension.front() != '.') { + throw std::invalid_argument("invalid tile extension: " + config_.extension); + } + } + std::optional find_file(const Tile_Request& request) const { + if (request.z < config_.minimum_level || request.z > config_.maximum_level) { + return std::nullopt; + } + auto relative = relative_path(request); + for (const auto& directory : directories_) { + auto path = std::filesystem::path(directory) / relative; + if (std::filesystem::is_regular_file(path)) { + return path; + } + } + return std::nullopt; + } + const Map_Tile_Config& config() const { + return config_; + } +private: + std::filesystem::path relative_path(const Tile_Request& request) const { + auto z = std::to_string(request.z); + auto x = std::to_string(request.x); + auto y = std::to_string(request.y) + config_.extension; + if (config_.path_layout == "z/x/y") { + return std::filesystem::path(z) / x / y; + } + return std::filesystem::path(z) / std::to_string(request.y) / (std::to_string(request.x) + config_.extension); + } + Map_Tile_Config config_; + std::vector directories_; +}; +class Tile_Source { +public: + explicit Tile_Source(Map_Tile_Config config) + : resolver_(config) {} + asio::awaitable load(const Tile_Request& request, const Tile_Cache_Request& cache_request) const { + auto path = resolver_.find_file(request); + if (!path) { + co_return Tile_Load_Result{Tile_Load_Status::Not_Found}; + } + auto cache_info = get_file_cache_info(*path); + if (!cache_info) { + co_return Tile_Load_Result{Tile_Load_Status::Not_Found}; + } + if (cache_info->size > kMaxTileFileSize || cache_info->size > std::numeric_limits::max()) { + throw std::runtime_error("tile file is too large: " + path->generic_string()); + } + auto generic_path = path->generic_string(); + auto content_type = httplib::detail::find_content_type(generic_path, {}, "application/octet-stream"); + if (is_not_modified(cache_request, *cache_info)) { + co_return Tile_Load_Result{Tile_Load_Status::Not_Modified, std::move(generic_path), {}, std::move(content_type), *cache_info}; + } + std::string content; + if (cache_request.include_body) { + auto executor = co_await asio::this_coro::executor; + asio::random_access_file file(executor, path->generic_string(), asio::file_base::read_only); + content.resize(static_cast(cache_info->size)); + if (!content.empty()) { + co_await asio::async_read_at(file, 0, asio::buffer(content.data(), content.size()), asio::use_awaitable); + } + } + co_return Tile_Load_Result{Tile_Load_Status::Ready, std::move(generic_path), std::move(content), std::move(content_type), *cache_info}; + } + const Map_Tile_Config& config() const { + return resolver_.config(); + } +private: + Tile_Path_Resolver resolver_; +}; +drogon::HttpResponsePtr make_tile_not_found_response(std::string_view request_path, bool include_body) { auto response = drogon::HttpResponse::newHttpResponse(); JSON ret = JSON::object(); ret.append({"LOG_POS", LOG_POS}); - ret.append({"not_found_path", path}); + ret.append({"not_found_path", std::string(request_path)}); response->setStatusCode(drogon::k404NotFound); response->setContentTypeCode(drogon::CT_TEXT_PLAIN); - response->setBody(ret.to_json_string()); + if (include_body) { + response->setBody(ret.to_json_string()); + } response->addHeader("Cache-Control", "no-store"); return response; } -drogon::HttpResponsePtr return_tile_file( - const drogon::HttpRequestPtr& req, - const std::string& path) { - auto info = get_file_cache_info(path); - if (!info) { - return make_tile_not_found_response(path); - } - const auto& if_none_match = req->getHeader("if-none-match"); - bool not_modified; - if (!if_none_match.empty()) { - not_modified = etag_matches(if_none_match, info->etag); - } - else { - const auto& if_modified_since = req->getHeader("if-modified-since"); - not_modified = !if_modified_since.empty() && if_modified_since == info->last_modified; - } - if (not_modified) { - auto response = drogon::HttpResponse::newHttpResponse(); - response->setStatusCode(drogon::k304NotModified); - response->setContentTypeCode(drogon::CT_NONE); - add_tile_cache_headers(response, *info); - return response; - } - auto content_type = httplib::detail::find_content_type( - path, - {}, - "application/octet-stream"); - auto response = drogon::HttpResponse::newFileResponse( - path, - "", - drogon::CT_CUSTOM, - content_type, - req); - if (response->statusCode() == drogon::k200OK || - response->statusCode() == drogon::k206PartialContent) { - add_tile_cache_headers(response, *info); +drogon::HttpResponsePtr make_tile_not_modified_response(const File_Cache_Info& info) { + auto response = drogon::HttpResponse::newHttpResponse(); + response->setStatusCode(drogon::k304NotModified); + response->setContentTypeCode(drogon::CT_NONE); + add_tile_cache_headers(response, info, false); + return response; +} +drogon::HttpResponsePtr make_tile_ok_response(Tile_Load_Result&& result, bool include_body) { + auto response = drogon::HttpResponse::newHttpResponse(); + response->setStatusCode(drogon::k200OK); + response->setContentTypeString(result.content_type); + add_tile_cache_headers(response, result.cache_info, !include_body); + if (include_body) { + response->setBody(std::move(result.content)); } return response; } +drogon::HttpResponsePtr make_tile_response(Tile_Load_Result&& result, std::string_view request_path, bool include_body) { + switch (result.status) { + case Tile_Load_Status::Ready: + return make_tile_ok_response(std::move(result), include_body); + case Tile_Load_Status::Not_Modified: + return make_tile_not_modified_response(result.cache_info); + case Tile_Load_Status::Not_Found: + return make_tile_not_found_response(request_path, include_body); + } + return make_tile_not_found_response(request_path, include_body); } -void Global::init_tiles() { - static auto& web_server = web_server_config; +drogon::HttpResponsePtr make_tile_read_failed_response(const std::exception& error, bool include_body) { + auto response = drogon::HttpResponse::newHttpResponse(); + response->setStatusCode(drogon::k500InternalServerError); + response->setContentTypeCode(drogon::CT_APPLICATION_JSON); + if (include_body) { + response->setBody(Psc::JSON::object({{"error", Psc::platform_2_utf8(error.what())}}).to_json_string()); + } + return response; +} +Tile_Cache_Request make_cache_request(const drogon::HttpRequestPtr& req) { + return Tile_Cache_Request{ + req->getHeader("if-none-match"), + req->getHeader("if-modified-since"), + req->method() != drogon::Head + }; +} +void register_tile_handler(std::string_view regex, std::string_view prefix, std::shared_ptr source) { drogon::app().registerHandlerViaRegex( - R"(^/tiles/.*$)", + std::string(regex), + [prefix = std::string(prefix), source = std::move(source)]( + const drogon::HttpRequestPtr& req, + std::function&& callback) mutable { + auto cache_request = make_cache_request(req); + auto request = parse_tile_request(req->path(), prefix, source->config().extension); + if (!request) { + callback(make_tile_not_found_response(req->path(), cache_request.include_body)); + return; + } + drogon::async_run( + [callback = std::move(callback), source, request = std::move(*request), cache_request = std::move(cache_request), path = req->path()]() mutable -> drogon::Task<> { + try { + auto result = co_await Ecap_Coro::to_drogon(source->load(request, cache_request)); + callback(make_tile_response(std::move(result), path, cache_request.include_body)); + } + catch (const std::exception& error) { + if (server_logger) { + server_logger->error("tiles", {}, LOG_POS + " " + Psc::platform_2_utf8(error.what())); + } + callback(make_tile_read_failed_response(error, cache_request.include_body)); + } + co_return; + }); + }, + {drogon::Get, drogon::Head}); +} +void register_map_resources_handler() { + drogon::app().registerHandlerViaRegex( + R"(^/map/resources$)", [](const drogon::HttpRequestPtr& req, std::function&& callback) { - auto path = web_server.get_true_tiles() + req->getPath().substr(6); - callback(return_tile_file(req, path)); + auto global = Global::instance(); + auto response = drogon::HttpResponse::newHttpResponse(); + if (req->method() == drogon::Post) { + auto parsed = Psc::try_parse_json(req->body().data()); + if (!parsed.has_value()) { + response->setStatusCode(drogon::k400BadRequest); + response->setContentTypeCode(drogon::CT_TEXT_PLAIN); + response->setBody("invalid current_imagery_key"); + callback(response); + return; + } + auto key = parsed.value().try_get_string("current_imagery_key"); + if (!key.has_value() || !global->map_resources_config.is_imagery_key(key.value())) { + response->setStatusCode(drogon::k400BadRequest); + response->setContentTypeCode(drogon::CT_TEXT_PLAIN); + response->setBody("invalid current_imagery_key"); + callback(response); + return; + } + global->map_resources_config.current_imagery_key = key.value(); + Global::save(); + } + response->setStatusCode(drogon::k200OK); + response->setContentTypeCode(drogon::CT_APPLICATION_JSON); + response->setBody(global->map_resources_config.to_public_json().to_json_string()); + response->addHeader("Cache-Control", "no-store"); + callback(response); }, - {drogon::Get}); + {drogon::Get, drogon::Post}); +} +void register_map_graphics_handler() { + drogon::app().registerHandlerViaRegex( + R"(^/map/graphics$)", + [](const drogon::HttpRequestPtr& req, + std::function&& callback) { + auto global = Global::instance(); + auto response = drogon::HttpResponse::newHttpResponse(); + if (req->method() == drogon::Post) { + auto parsed = Psc::try_parse_json(req->body().data()); + if (!parsed.has_value()) { + response->setStatusCode(drogon::k400BadRequest); + response->setContentTypeCode(drogon::CT_TEXT_PLAIN); + response->setBody("invalid cesium_graphics"); + callback(response); + return; + } + try { + global->cesium_graphics_config.from_base_json(&parsed.value()); + Global::save(); + } + catch (const std::exception& error) { + response->setStatusCode(drogon::k400BadRequest); + response->setContentTypeCode(drogon::CT_TEXT_PLAIN); + response->setBody(Psc::platform_2_utf8(error.what())); + callback(response); + return; + } + } + response->setStatusCode(drogon::k200OK); + response->setContentTypeCode(drogon::CT_APPLICATION_JSON); + response->setBody(global->cesium_graphics_config.to_base_json().to_json_string()); + response->addHeader("Cache-Control", "no-store"); + callback(response); + }, + {drogon::Get, drogon::Post}); +} +Psc::JSON make_map_view_response(Global* global) { + auto ret = global->map_view_config.to_base_json(); + ret.append({"current_imagery_key", global->map_resources_config.current_imagery_key}); + return ret; +} +void register_map_view_handler() { + drogon::app().registerHandlerViaRegex( + R"(^/map/view$)", + [](const drogon::HttpRequestPtr& req, + std::function&& callback) { + auto global = Global::instance(); + auto response = drogon::HttpResponse::newHttpResponse(); + if (req->method() == drogon::Post) { + auto parsed = Psc::try_parse_json(req->body().data()); + if (!parsed.has_value()) { + response->setStatusCode(drogon::k400BadRequest); + response->setContentTypeCode(drogon::CT_TEXT_PLAIN); + response->setBody("invalid map_view"); + callback(response); + return; + } + auto key = parsed.value().try_get_string("current_imagery_key"); + if (!key.has_value() || !global->map_resources_config.is_imagery_key(key.value())) { + response->setStatusCode(drogon::k400BadRequest); + response->setContentTypeCode(drogon::CT_TEXT_PLAIN); + response->setBody("invalid current_imagery_key"); + callback(response); + return; + } + try { + auto next_view = global->map_view_config; + next_view.from_base_json(&parsed.value()); + global->map_resources_config.current_imagery_key = key.value(); + global->map_view_config = next_view; + Global::save(); + } + catch (const std::exception& error) { + response->setStatusCode(drogon::k400BadRequest); + response->setContentTypeCode(drogon::CT_TEXT_PLAIN); + response->setBody(Psc::platform_2_utf8(error.what())); + callback(response); + return; + } + } + response->setStatusCode(drogon::k200OK); + response->setContentTypeCode(drogon::CT_APPLICATION_JSON); + response->setBody(make_map_view_response(global).to_json_string()); + response->addHeader("Cache-Control", "no-store"); + callback(response); + }, + {drogon::Get, drogon::Post}); +} +} +void Global::init_tiles() { + auto imagery = std::make_shared(map_resources_config.imagery); + auto google_imagery = std::make_shared(map_resources_config.google_imagery); + auto terrain = std::make_shared(map_resources_config.terrain); + register_map_resources_handler(); + register_map_graphics_handler(); + register_map_view_handler(); + register_tile_handler(R"(^/map/imagery/.*$)", "/map/imagery/", imagery); + register_tile_handler(R"(^/tiles/.*$)", "/tiles/", google_imagery); + register_tile_handler(R"(^/map/terrain/.*$)", "/map/terrain/", terrain); } diff --git a/module/dll_source/main.cpp b/module/dll_source/main.cpp index 3e31011..bd3e4de 100644 --- a/module/dll_source/main.cpp +++ b/module/dll_source/main.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #include "Core/Base/JSON.h" #include "Core/Base/ThreadManager.h" #include "Core/Base/global_include.h" @@ -21,6 +22,21 @@ #include "Core/Base/SM_RingBuffer.h" #include "Dll_Global.h" #include +#ifndef ECAP_ADSB_PLAYBACK_SPEED_RATE +#define ECAP_ADSB_PLAYBACK_SPEED_RATE 20.0 +#endif +#ifndef ECAP_ADSB_PLAYBACK_LOOP +#define ECAP_ADSB_PLAYBACK_LOOP 1 +#endif +#ifndef ECAP_ADSB_PLAYBACK_USE_VIRTUAL_TIME +#define ECAP_ADSB_PLAYBACK_USE_VIRTUAL_TIME 1 +#endif +#ifndef ECAP_ADSB_PLAYBACK_MAX_BATCH_LINES +#define ECAP_ADSB_PLAYBACK_MAX_BATCH_LINES 512 +#endif +#ifndef ECAP_ADSB_PLAYBACK_FALLBACK_LINE_INTERVAL_SEC +#define ECAP_ADSB_PLAYBACK_FALLBACK_LINE_INTERVAL_SEC 0.02 +#endif std::vector ddd = { "1A 33 1A 1A F1 DA 08 2A 73 60 8D 78 12 0A EA 28 88 64 25 3C 08 33 22 41", "1A 33 1A 1A F1 D9 13 EA E7 11 8D 78 1D 40 58 BF 41 E2 45 49 09 7B C8 24", @@ -48,33 +64,64 @@ std::string get_absb() { } class TextLineReader { public: + struct Playback_Line { + std::string text; + double virtual_second{}; + }; explicit TextLineReader(std::string_view file_path) { load(file_path); } - // 每调用一次,返回一行 - // 返回 true 表示成功取到一行 - // 返回 false 表示已经没有更多行 - bool get(std::string& ret_line) { - if (index_ >= lines_.size()) { - return false; + std::size_t read(char* buf, std::size_t len) { + if (lines_.empty() || len == 0) { + return 0; } - ret_line = lines_[index_]; - ++index_; - return true; + std::string ret; + std::size_t count = 0; + while (count < ECAP_ADSB_PLAYBACK_MAX_BATCH_LINES) { + if (index_ >= lines_.size()) { + if (ECAP_ADSB_PLAYBACK_LOOP) { + reset(); + } + else { + break; + } + } + if (!line_ready(lines_[index_])) { + break; + } + const auto& line = lines_[index_].text; + auto next_size = ret.size() + line.size() + 1; + if (!ret.empty() && next_size > len) { + break; + } + ret += line; + ret += '\n'; + ++index_; + ++count; + if (!ECAP_ADSB_PLAYBACK_USE_VIRTUAL_TIME) { + break; + } + } + auto copy_size = ret.size(); + if (copy_size > len) { + copy_size = len; + } + std::memcpy(buf, ret.data(), copy_size); + return copy_size; } - // 重新从第一行开始读 void reset() { index_ = 0; + true_start_time_ = clock::now(); + start_virtual_second_ = lines_.empty() ? 0 : lines_.front().virtual_second; } - // 总行数 std::size_t size() const { return lines_.size(); } - // 是否已经读完 bool empty() const { return lines_.empty(); } private: + using clock = std::chrono::steady_clock; void load(std::string_view file_path) { std::ifstream file{std::string(file_path)}; // std::cout << "抛出异常" << std::endl; @@ -86,21 +133,106 @@ private: throw std::runtime_error(err); } std::string line; + std::optional last_day_second; + std::uint64_t day_offset = 0; + std::size_t fallback_index = 0; while (std::getline(file, line)) { - lines_.push_back(line); + auto day_second = parse_day_second(line); + if (!day_second.has_value()) { + day_second = fallback_index * ECAP_ADSB_PLAYBACK_FALLBACK_LINE_INTERVAL_SEC; + } + if (last_day_second.has_value() && day_second.value() + 3600.0 < last_day_second.value()) { + ++day_offset; + } + lines_.push_back({line, day_offset * 86400.0 + day_second.value()}); + last_day_second = day_second; + ++fallback_index; } - index_ = 0; + reset(); + } + bool line_ready(const Playback_Line& line) const { + if (!ECAP_ADSB_PLAYBACK_USE_VIRTUAL_TIME) { + return true; + } + return line.virtual_second <= current_virtual_second(); + } + double current_virtual_second() const { + auto elapsed = std::chrono::duration(clock::now() - true_start_time_).count(); + return start_virtual_second_ + elapsed * ECAP_ADSB_PLAYBACK_SPEED_RATE; + } + static std::optional parse_day_second(std::string_view line) { + auto bytes = parse_hex_bytes(line); + if (!bytes.has_value()) { + return std::nullopt; + } + auto packet = unescape_packet(bytes.value()); + if (packet.size() < 8 || packet[0] != 0x1a) { + return std::nullopt; + } + std::uint64_t raw = 0; + for (std::size_t i = 2; i < 8; ++i) { + raw = (raw << 8) | packet[i]; + } + auto daysec = static_cast((raw >> 30) & 0x3ffff); + auto nanosec = static_cast(raw & 0x3fffffff); + return daysec + nanosec / 1000000000.0; + } + static std::optional> parse_hex_bytes(std::string_view line) { + std::vector ret; + int high = -1; + for (char ch : line) { + int value = hex_value(ch); + if (value < 0) { + if (ch == ' ' || ch == '\t' || ch == '\r' || ch == '\n') { + continue; + } + return std::nullopt; + } + if (high < 0) { + high = value; + } + else { + ret.push_back(static_cast((high << 4) | value)); + high = -1; + } + } + if (high >= 0) { + return std::nullopt; + } + return ret; + } + static int hex_value(char ch) { + if (ch >= '0' && ch <= '9') { + return ch - '0'; + } + if (ch >= 'a' && ch <= 'f') { + return ch - 'a' + 10; + } + if (ch >= 'A' && ch <= 'F') { + return ch - 'A' + 10; + } + return -1; + } + static std::vector unescape_packet(const std::vector& bytes) { + std::vector ret; + ret.reserve(bytes.size()); + for (std::size_t i = 0; i < bytes.size(); ++i) { + ret.push_back(bytes[i]); + if (bytes[i] == 0x1a && i + 1 < bytes.size() && bytes[i + 1] == 0x1a) { + ++i; + } + } + return ret; } private: - std::vector lines_; + std::vector lines_; std::size_t index_ = 0; + clock::time_point true_start_time_{clock::now()}; + double start_virtual_second_ = 0; }; std::size_t adsb_read(char* buf, std::size_t len) { static TextLineReader reader(get_exe_dir() + "/ADS-B_195_0205.txt"); - std::string line; - reader.get(line); - auto l = line.size(); - std::memcpy(buf, line.c_str(), l); + auto l = reader.read(buf, len); /*static Frequency_Limit fl; if (fl.test()) { std::cout << "adsb_read" << VAR_STR_2(l, len) << std::endl;