3D
This commit is contained in:
+76
-7
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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<double> altitude; // 米
|
||||
RET<std::string> call_sign;
|
||||
RET<std::string> 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);
|
||||
};
|
||||
|
||||
|
||||
@@ -129,7 +129,7 @@ struct Aircraft_List_JSON_Service_O {
|
||||
ret.src = "暂不支持";
|
||||
// ret.cla
|
||||
ret.cla = "暂不支持";
|
||||
RET<SSR::CPR::Position> pos = info->pos();
|
||||
RET<SSR::CPR::Position> pos = info->air_pos_track_list.last();
|
||||
if(pos.has_value()) {
|
||||
ret.lat = pos.value().lat;
|
||||
ret.lon = pos.value().lon;
|
||||
|
||||
@@ -6,8 +6,15 @@ struct HULC_Status_Message;
|
||||
|
||||
std::optional<SSR::CPR::Position> 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ public:
|
||||
std::string to_string();
|
||||
Psc::JSON to_Json();
|
||||
std::optional<SSR::CPR::Position> get_pos();
|
||||
bool has_valid_position();
|
||||
void set_msg(const SSR::HULC_Status_Message& msg);
|
||||
std::function<void (const SSR::HULC_Status_Message&)> handle_when_updated = nullptr;
|
||||
protected:
|
||||
|
||||
@@ -53,6 +53,7 @@ protected:
|
||||
class Data_Source_Data {
|
||||
public:
|
||||
Psc::Copyable_Atomic<bool> base_station_show{};
|
||||
Psc::Copyable_Atomic<bool> base_station_has_valid_position{};
|
||||
Psc::Copyable_Atomic<bool> aircraft_show{};
|
||||
std::string color = "#1677ff";
|
||||
int aircraft_pixel_size{};
|
||||
|
||||
@@ -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());
|
||||
});
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#include "Core/Base/Check.h"
|
||||
#include "Core/Net_Adapter/Net_Adapter.h"
|
||||
#include "Global.h"
|
||||
#include <algorithm>
|
||||
#include <filesystem>
|
||||
#include <string_view>
|
||||
#undef interface
|
||||
@@ -26,6 +27,281 @@ std::string get_config_path() {
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
static std::vector<std::string> 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<std::string> 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<std::string>& 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<std::uint32_t>();
|
||||
}
|
||||
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<std::uint32_t>();
|
||||
}
|
||||
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<double>();
|
||||
}
|
||||
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;
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include "MLAT.h"
|
||||
#include "Source_Feed_Relation.h"
|
||||
#include <fstream>
|
||||
#include <vector>
|
||||
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<std::string> 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<std::string> get_true_directories() const {
|
||||
std::vector<std::string> 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;
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -1,31 +1,52 @@
|
||||
#include "Global.h"
|
||||
#include "Performance_Monitor.h"
|
||||
#include <algorithm>
|
||||
#include <asio/random_access_file.hpp>
|
||||
#include <asio/read_at.hpp>
|
||||
#include <asio/use_awaitable.hpp>
|
||||
#include <cctype>
|
||||
#include <cstdint>
|
||||
#include <ctime>
|
||||
#include <filesystem>
|
||||
#include <format>
|
||||
#include <limits>
|
||||
#include <optional>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <sys/stat.h>
|
||||
/**
|
||||
* 瓦片文件缓存策略:
|
||||
* * 首次请求通过 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 <vector>
|
||||
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<File_Cache_Info> get_file_cache_info(const std::string& path) {
|
||||
auto native_path = std::filesystem::path(path);
|
||||
std::optional<File_Cache_Info> 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<std::uint64_t>(info.st_size);
|
||||
result.modified_time = info.st_mtime;
|
||||
result.etag = std::format(
|
||||
"W/\"{:x}-{:x}\"",
|
||||
static_cast<std::uint64_t>(result.modified_time),
|
||||
result.size);
|
||||
result.etag = std::format("W/\"{:x}-{:x}\"", static_cast<std::uint64_t>(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<std::uint64_t>(ch - '0');
|
||||
if (result > std::numeric_limits<std::uint32_t>::max()) {
|
||||
throw std::out_of_range("tile coordinate is too large");
|
||||
}
|
||||
}
|
||||
return static_cast<std::uint32_t>(result);
|
||||
}
|
||||
std::optional<Tile_Request> 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<std::filesystem::path> 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<std::string> directories_;
|
||||
};
|
||||
class Tile_Source {
|
||||
public:
|
||||
explicit Tile_Source(Map_Tile_Config config)
|
||||
: resolver_(config) {}
|
||||
asio::awaitable<Tile_Load_Result> 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<std::size_t>::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<std::size_t>(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<Tile_Source> source) {
|
||||
drogon::app().registerHandlerViaRegex(
|
||||
R"(^/tiles/.*$)",
|
||||
std::string(regex),
|
||||
[prefix = std::string(prefix), source = std::move(source)](
|
||||
const drogon::HttpRequestPtr& req,
|
||||
std::function<void(const drogon::HttpResponsePtr&)>&& 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<void(const drogon::HttpResponsePtr&)>&& 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<void(const drogon::HttpResponsePtr&)>&& 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<void(const drogon::HttpResponsePtr&)>&& 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<Tile_Source>(map_resources_config.imagery);
|
||||
auto google_imagery = std::make_shared<Tile_Source>(map_resources_config.google_imagery);
|
||||
auto terrain = std::make_shared<Tile_Source>(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);
|
||||
}
|
||||
|
||||
+151
-19
@@ -12,6 +12,7 @@
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
#include <iostream>
|
||||
#include <optional>
|
||||
#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 <string_view>
|
||||
#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<std::string> 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<double> 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<double>(clock::now() - true_start_time_).count();
|
||||
return start_virtual_second_ + elapsed * ECAP_ADSB_PLAYBACK_SPEED_RATE;
|
||||
}
|
||||
static std::optional<double> 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<std::uint32_t>((raw >> 30) & 0x3ffff);
|
||||
auto nanosec = static_cast<std::uint32_t>(raw & 0x3fffffff);
|
||||
return daysec + nanosec / 1000000000.0;
|
||||
}
|
||||
static std::optional<std::vector<std::uint8_t>> parse_hex_bytes(std::string_view line) {
|
||||
std::vector<std::uint8_t> 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<std::uint8_t>((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<std::uint8_t> unescape_packet(const std::vector<std::uint8_t>& bytes) {
|
||||
std::vector<std::uint8_t> 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<std::string> lines_;
|
||||
std::vector<Playback_Line> 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;
|
||||
|
||||
Reference in New Issue
Block a user