From 75bfd2550885b379409f025641c2069ed9052c45 Mon Sep 17 00:00:00 2001 From: wyc <1104749580@qq.com> Date: Tue, 4 Aug 2026 17:21:15 +0800 Subject: [PATCH] =?UTF-8?q?=E6=94=AF=E6=8C=81mbtiles?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/config.json | 654 +++++++++++++++----------- module/Local_Server/server/Config.cpp | 122 ++++- module/Local_Server/server/Config.h | 24 + module/Local_Server/server/tiles.cpp | 128 ++++- 4 files changed, 614 insertions(+), 314 deletions(-) diff --git a/config/config.json b/config/config.json index 139b948..e7ab86f 100644 --- a/config/config.json +++ b/config/config.json @@ -41,11 +41,11 @@ "webapp": "@/../third_party/eacp_webapp/wwwroot" }, "map_resources": { - "current_imagery_key": "imagery", + "current_imagery_key": "mixed_imagery", "imagery": { "directories": [ - "D:/ae/tiles/Esri_World_Imagery_0-9", - "D:/ae/tiles/Esri_World_Imagery_10-12" + "D:/ae/tiles/Esri_World_Imagery_0-9", + "D:/ae/tiles/Esri_World_Imagery_10-12" ], "path_layout": "z/x/y", "y_axis": "xyz", @@ -57,7 +57,7 @@ }, "google_imagery": { "directories": [ - "D:/tiles" + "D:/tiles" ], "path_layout": "z/x/y", "y_axis": "xyz", @@ -68,28 +68,136 @@ "maximum_level": 10 }, "terrain": { + "file_format": "mbtiles", "directories": [ - "D:/ae/tiles/AWS_Terrarium_0-15" + ], + "files": [ + "C:/tiles/高程 server.arcgisonline.com.mbtiles" ], "path_layout": "z/x/y", - "y_axis": "xyz", + "y_axis": "tms", "projection": "web_mercator", "extension": ".png", "tile_size": 256, - "minimum_level": 0, - "maximum_level": 10, + "minimum_level": 11, + "maximum_level": 11, "encoding": "terrarium" - } + }, + "tile_sources": [ + { + "key": "mixed_imagery", + "name": "本地混合图", + "tile_type": "imagery", + "resource": { + "file_format": "mbtiles", + "directories": [ + ], + "files": [ + "C:/tiles/混合图.mbtiles" + ], + "path_layout": "z/x/y", + "y_axis": "tms", + "projection": "web_mercator", + "extension": ".png", + "tile_size": 256, + "minimum_level": 11, + "maximum_level": 11 + } + }, + { + "key": "height_terrain", + "name": "本地高程", + "tile_type": "terrain", + "resource": { + "file_format": "mbtiles", + "directories": [ + ], + "files": [ + "C:/tiles/高程 server.arcgisonline.com.mbtiles", + "C:/tiles/中国高程9-10.mbtiles" + ], + "path_layout": "z/x/y", + "y_axis": "tms", + "projection": "web_mercator", + "extension": ".png", + "tile_size": 256, + "minimum_level": 11, + "maximum_level": 11, + "encoding": "terrarium" + } + }, + { + "key": "imagery", + "name": "本地卫星影像", + "tile_type": "imagery", + "resource": { + "file_format": "directory", + "directories": [ + "D:/ae/tiles/Esri_World_Imagery_0-9", + "D:/ae/tiles/Esri_World_Imagery_10-12" + ], + "files": [ + ], + "path_layout": "z/x/y", + "y_axis": "xyz", + "projection": "web_mercator", + "extension": ".png", + "tile_size": 256, + "minimum_level": 0, + "maximum_level": 11 + } + }, + { + "key": "google_imagery", + "name": "本地Google瓦片", + "tile_type": "imagery", + "resource": { + "file_format": "directory", + "directories": [ + "D:/tiles" + ], + "files": [ + ], + "path_layout": "z/x/y", + "y_axis": "xyz", + "projection": "web_mercator", + "extension": ".png", + "tile_size": 256, + "minimum_level": 1, + "maximum_level": 10 + } + }, + { + "key": "osm_online", + "name": "OpenStreetMap在线", + "tile_type": "imagery", + "url": "https://tile.openstreetmap.org/{z}/{x}/{y}.png", + "resource": { + "file_format": "online", + "directories": [ + ], + "files": [ + ], + "path_layout": "z/x/y", + "y_axis": "xyz", + "projection": "web_mercator", + "extension": ".png", + "tile_size": 256, + "minimum_level": 0, + "maximum_level": 19 + } + } + ] }, "map_view": { "scene_mode": "3d", "map2d": { - "current_imagery_key": "imagery", + "current_imagery_key": "mixed_imagery", "tile_zoom_mode": "native", "tile_display_maximum_level": 8 }, "map3d": { - "current_imagery_key": "imagery", + "current_imagery_key": "mixed_imagery", "tile_zoom_mode": "native", "tile_display_maximum_level": 19 }, @@ -277,20 +385,20 @@ }, "device": { "net": [ - { - "key": "device", - "ip": "192.168.1.75", - "port": 80, - "netmask": "255.255.255.0", - "gateway": "192.168.1.1" - }, - { - "key": "wifi", - "ip": "192.168.10.193", - "port": 80, - "netmask": "255.255.255.0", - "gateway": "192.168.10.1" - } + { + "key": "device", + "ip": "192.168.1.75", + "port": 80, + "netmask": "255.255.255.0", + "gateway": "192.168.1.1" + }, + { + "key": "wifi", + "ip": "192.168.10.193", + "port": 80, + "netmask": "255.255.255.0", + "gateway": "192.168.10.1" + } ] }, "mode_acs": { @@ -319,185 +427,185 @@ "tcp_server_default_connect_user_buffer_size": 409600, "tcp_server_default_connect_system_buffer_size": 409600, "list": [ - { - "type": "Data_Feed_TCP_Server", - "key": "Port_10003", - "enable": true, - "output_format": { - "type": "BIN", - "use_status": true, - "mode_s_output_type": "ALL_Mode_S", - "use_mode_ac": true, - "sbs_only_pos": true - }, - "port": 10003, - "connect_user_buffer_size": 409600, - "connect_system_buffer_size": 409600 - }, - { - "type": "Data_Feed_UDP_Server", - "key": "22333", - "enable": false, - "output_format": { - "type": "BIN", - "use_status": false, - "mode_s_output_type": "ALL_Mode_S", - "use_mode_ac": false, - "sbs_only_pos": false - }, - "port": 50001 - }, - { - "type": "Data_Feed_UDP_Client", - "key": "Port_10004", - "enable": false, - "output_format": { - "type": "AVR", - "use_status": false, - "mode_s_output_type": "DF_11_17_18", - "use_mode_ac": false, - "sbs_only_pos": true - }, - "port": 30004, - "url": "192.168.1.75" - }, - { - "type": "Data_Feed_TCP_Client", - "key": "Port_10005", - "enable": false, - "output_format": { - "type": "BIN", - "use_status": true, - "mode_s_output_type": "NO_POS_Mode_S", - "use_mode_ac": true, - "sbs_only_pos": true - }, - "url": "192.168.1.167", - "port": 10005 - }, - { - "type": "Data_Feed_TCP_Server", - "key": "Port_30003", - "enable": false, - "output_format": { - "type": "SBS", - "use_status": false, - "mode_s_output_type": "ALL_Mode_S", - "use_mode_ac": true, - "sbs_only_pos": true - }, - "port": 30003, - "connect_user_buffer_size": 0, - "connect_system_buffer_size": 0 - } + { + "type": "Data_Feed_TCP_Server", + "key": "Port_10003", + "enable": true, + "output_format": { + "type": "BIN", + "use_status": true, + "mode_s_output_type": "ALL_Mode_S", + "use_mode_ac": true, + "sbs_only_pos": true + }, + "port": 10003, + "connect_user_buffer_size": 409600, + "connect_system_buffer_size": 409600 + }, + { + "type": "Data_Feed_UDP_Server", + "key": "22333", + "enable": false, + "output_format": { + "type": "BIN", + "use_status": false, + "mode_s_output_type": "ALL_Mode_S", + "use_mode_ac": false, + "sbs_only_pos": false + }, + "port": 50001 + }, + { + "type": "Data_Feed_UDP_Client", + "key": "Port_10004", + "enable": false, + "output_format": { + "type": "AVR", + "use_status": false, + "mode_s_output_type": "DF_11_17_18", + "use_mode_ac": false, + "sbs_only_pos": true + }, + "port": 30004, + "url": "192.168.1.75" + }, + { + "type": "Data_Feed_TCP_Client", + "key": "Port_10005", + "enable": false, + "output_format": { + "type": "BIN", + "use_status": true, + "mode_s_output_type": "NO_POS_Mode_S", + "use_mode_ac": true, + "sbs_only_pos": true + }, + "url": "192.168.1.167", + "port": 10005 + }, + { + "type": "Data_Feed_TCP_Server", + "key": "Port_30003", + "enable": false, + "output_format": { + "type": "SBS", + "use_status": false, + "mode_s_output_type": "ALL_Mode_S", + "use_mode_ac": true, + "sbs_only_pos": true + }, + "port": 30003, + "connect_user_buffer_size": 0, + "connect_system_buffer_size": 0 + } ] }, "data_source": { "list": [ - { - "type": "Dll_Data_Source", - "key": "lzy_dll", - "enable": true, - "base_station_has_valid_position": true, - "map_display": { - "map2d": { - "base_station_show": true, - "aircraft_show": true, - "constant_screen_size": true, - "color": "#454641", - "track_point_color": "#ffff00", - "base_station_color": "#1677ff", - "aircraft_pixel_size": 34, - "base_station_pixel_size": 30, - "aircraft_scale": 2.000000, - "base_station_scale": 0.440000, - "show_icao": true, - "show_call_sign": false, - "show_fly_status": false - }, - "map3d": { - "base_station_show": true, - "aircraft_show": true, - "constant_screen_size": true, - "color": "#454641", - "track_point_color": "#ffff00", - "base_station_color": "#1677ff", - "aircraft_pixel_size": 50, - "base_station_pixel_size": 100, - "aircraft_scale": 1.000000, - "base_station_scale": 0.440000, - "show_icao": true, - "show_call_sign": false, - "show_fly_status": false - } - }, - "lat": 37.433547, - "lon": 121.408730, - "alt": 2300.000000, - "ignore_msg_time": true, - "update_form_gps": false, - "keep_mode": true, - "library_path": "@/dll_source.dll", - "function_name": "adsb_read", - "data_type": "BIN_Blank_Text", - "buffer_size": 200000 - }, - { - "type": "File_Data_Source", - "key": "aaaa", - "enable": false, - "base_station_has_valid_position": false, - "map_display": { - "map2d": { - "base_station_show": true, - "aircraft_show": true, - "constant_screen_size": true, - "color": "#d1a54c", - "track_point_color": "#ffff00", - "base_station_color": "#1677ff", - "aircraft_pixel_size": 50, - "base_station_pixel_size": 100, - "aircraft_scale": 2.000000, - "base_station_scale": 0.440000, - "show_icao": true, - "show_call_sign": true, - "show_fly_status": true - }, - "map3d": { - "base_station_show": true, - "aircraft_show": true, - "constant_screen_size": true, - "color": "#d1a54c", - "track_point_color": "#ffff00", - "base_station_color": "#1677ff", - "aircraft_pixel_size": 50, - "base_station_pixel_size": 100, - "aircraft_scale": 2.000000, - "base_station_scale": 0.440000, - "show_icao": true, - "show_call_sign": true, - "show_fly_status": true - } - }, - "lat": 37.000000, - "lon": 121.000000, - "alt": 1000.000000, - "ignore_msg_time": true, - "update_form_gps": false, - "keep_mode": true, - "file_path": "D:\\ae\\proj\\projects\\ECAP_Server\\data\\BIN_Blank_Text\\ADS-B_195_0205.txt", - "data_type": "BIN_Blank_Text", - "play_mode": "loop" - } + { + "type": "Dll_Data_Source", + "key": "lzy_dll", + "enable": true, + "base_station_has_valid_position": true, + "map_display": { + "map2d": { + "base_station_show": true, + "aircraft_show": true, + "constant_screen_size": true, + "color": "#454641", + "track_point_color": "#ffff00", + "base_station_color": "#1677ff", + "aircraft_pixel_size": 34, + "base_station_pixel_size": 30, + "aircraft_scale": 2.000000, + "base_station_scale": 0.440000, + "show_icao": true, + "show_call_sign": false, + "show_fly_status": false + }, + "map3d": { + "base_station_show": true, + "aircraft_show": true, + "constant_screen_size": true, + "color": "#454641", + "track_point_color": "#ffff00", + "base_station_color": "#1677ff", + "aircraft_pixel_size": 50, + "base_station_pixel_size": 100, + "aircraft_scale": 1.000000, + "base_station_scale": 0.440000, + "show_icao": true, + "show_call_sign": false, + "show_fly_status": false + } + }, + "lat": 37.433547, + "lon": 121.408730, + "alt": 2300.000000, + "ignore_msg_time": true, + "update_form_gps": false, + "keep_mode": true, + "library_path": "@/dll_source.dll", + "function_name": "adsb_read", + "data_type": "BIN_Blank_Text", + "buffer_size": 200000 + }, + { + "type": "File_Data_Source", + "key": "aaaa", + "enable": false, + "base_station_has_valid_position": false, + "map_display": { + "map2d": { + "base_station_show": true, + "aircraft_show": true, + "constant_screen_size": true, + "color": "#d1a54c", + "track_point_color": "#ffff00", + "base_station_color": "#1677ff", + "aircraft_pixel_size": 50, + "base_station_pixel_size": 100, + "aircraft_scale": 2.000000, + "base_station_scale": 0.440000, + "show_icao": true, + "show_call_sign": true, + "show_fly_status": true + }, + "map3d": { + "base_station_show": true, + "aircraft_show": true, + "constant_screen_size": true, + "color": "#d1a54c", + "track_point_color": "#ffff00", + "base_station_color": "#1677ff", + "aircraft_pixel_size": 50, + "base_station_pixel_size": 100, + "aircraft_scale": 2.000000, + "base_station_scale": 0.440000, + "show_icao": true, + "show_call_sign": true, + "show_fly_status": true + } + }, + "lat": 37.000000, + "lon": 121.000000, + "alt": 1000.000000, + "ignore_msg_time": true, + "update_form_gps": false, + "keep_mode": true, + "file_path": "D:\\ae\\proj\\projects\\ECAP_Server\\data\\BIN_Blank_Text\\ADS-B_195_0205.txt", + "data_type": "BIN_Blank_Text", + "play_mode": "loop" + } ] }, "source_feed_relation": { "list": [ - { - "key": "key_10003", - "enable": true, - "type": "First_Source_To_All_Feed_Relation" - } + { + "key": "key_10003", + "enable": true, + "type": "First_Source_To_All_Feed_Relation" + } ] } }, @@ -515,83 +623,83 @@ "external_database": { "ecap_sqlite_path": "@/../data/sqlite/ecap.sqlite", "list": [ - { - "name": "tar1090_db_aircraft", - "download_url": "https://raw.githubusercontent.com/wiedehopf/tar1090-db/refs/heads/csv/aircraft.csv.gz", - "description": "wiedehopf/tar1090-db 发布的轻量 ICAO24 飞机静态库,提供注册号、机型、年份和运营方备注。", - "update_interval_seconds": 604800, - "last_updated_at": 1780388281 - }, - { - "name": "wiedehopf_tar1090_db_aircraft", - "download_url": "https://raw.githubusercontent.com/wiedehopf/tar1090-db/refs/heads/csv/aircraft.csv.gz", - "description": "wiedehopf/tar1090-db 项目发布的飞机静态数据库镜像,供 tar1090 本地识别飞机。", - "update_interval_seconds": 604800, - "last_updated_at": 1780381804 - }, - { - "name": "mictronics_aircraft_database", - "download_url": "https://raw.githubusercontent.com/wiedehopf/tar1090-db/refs/heads/csv/aircraft.csv.gz", - "description": "Mictronics 社区飞机数据库的 tar1090 汇总镜像,可按 ICAO24 查询注册号、机型和运营方。", - "update_interval_seconds": 604800, - "last_updated_at": 1780381804 - }, - { - "name": "opensky_aircraft_database", - "download_url": "https://opensky-network.org/datasets/metadata/aircraftDatabase.csv", - "description": "OpenSky Network 飞机元数据 CSV,包含制造商、型号、注册号和运营方等字段。", - "update_interval_seconds": 604800, - "last_updated_at": 1780381804 - }, - { - "name": "faa_aircraft_registry", - "download_url": "https://registry.faa.gov/database/ReleasableAircraft.zip", - "description": "FAA 官方可公开下载的美国飞机注册库,MASTER.txt 包含 Mode-S Hex 注册信息。", - "update_interval_seconds": 86400, - "last_updated_at": 1780381804 - }, - { - "name": "icao_doc_8643_aircraft_type_designators", - "download_url": "", - "description": "ICAO Doc 8643 机型代码表,用于将 A320、B738 等 designator 映射为制造商、型号和类别。", - "update_interval_seconds": 2419200, - "last_updated_at": 1780381804 - }, - { - "name": "vrs_routes", - "download_url": "https://vrs-standing-data.adsb.lol/routes.csv", - "description": "VRS 航线表,以 callsign 查询机场代码序列,用于推断起飞机场、经停机场和目的机场。", - "update_interval_seconds": 3600, - "last_updated_at": 1780381804 - }, - { - "name": "vrs_airports", - "download_url": "https://vrs-standing-data.adsb.lol/airports.csv", - "description": "VRS 机场字典,提供机场代码、名称、国家、经纬度和高度。", - "update_interval_seconds": 3600, - "last_updated_at": 1780381804 - }, - { - "name": "adsblol_vrs_standing_data_routes", - "download_url": "https://vrs-standing-data.adsb.lol/routes.csv", - "description": "ADSB.lol 发布的 VRS standing-data 航线镜像,以 callsign 查询机场代码序列。", - "update_interval_seconds": 3600, - "last_updated_at": 1780381804 - }, - { - "name": "vradarserver_standing_data_routes", - "download_url": "https://vrs-standing-data.adsb.lol/routes.csv", - "description": "Virtual Radar Server standing-data 的航线适配表;当前在线更新使用 ADSB.lol 公共镜像。", - "update_interval_seconds": 3600, - "last_updated_at": 1780381804 - }, - { - "name": "opensky_flightdata_api_cache", - "download_url": "", - "description": "OpenSky FlightData API 查询缓存,按 ICAO24 和时间范围保存历史航班起降机场结果。", - "update_interval_seconds": 0, - "last_updated_at": 1780381804 - } + { + "name": "tar1090_db_aircraft", + "download_url": "https://raw.githubusercontent.com/wiedehopf/tar1090-db/refs/heads/csv/aircraft.csv.gz", + "description": "wiedehopf/tar1090-db 发布的轻量 ICAO24 飞机静态库,提供注册号、机型、年份和运营方备注。", + "update_interval_seconds": 604800, + "last_updated_at": 1780388281 + }, + { + "name": "wiedehopf_tar1090_db_aircraft", + "download_url": "https://raw.githubusercontent.com/wiedehopf/tar1090-db/refs/heads/csv/aircraft.csv.gz", + "description": "wiedehopf/tar1090-db 项目发布的飞机静态数据库镜像,供 tar1090 本地识别飞机。", + "update_interval_seconds": 604800, + "last_updated_at": 1780381804 + }, + { + "name": "mictronics_aircraft_database", + "download_url": "https://raw.githubusercontent.com/wiedehopf/tar1090-db/refs/heads/csv/aircraft.csv.gz", + "description": "Mictronics 社区飞机数据库的 tar1090 汇总镜像,可按 ICAO24 查询注册号、机型和运营方。", + "update_interval_seconds": 604800, + "last_updated_at": 1780381804 + }, + { + "name": "opensky_aircraft_database", + "download_url": "https://opensky-network.org/datasets/metadata/aircraftDatabase.csv", + "description": "OpenSky Network 飞机元数据 CSV,包含制造商、型号、注册号和运营方等字段。", + "update_interval_seconds": 604800, + "last_updated_at": 1780381804 + }, + { + "name": "faa_aircraft_registry", + "download_url": "https://registry.faa.gov/database/ReleasableAircraft.zip", + "description": "FAA 官方可公开下载的美国飞机注册库,MASTER.txt 包含 Mode-S Hex 注册信息。", + "update_interval_seconds": 86400, + "last_updated_at": 1780381804 + }, + { + "name": "icao_doc_8643_aircraft_type_designators", + "download_url": "", + "description": "ICAO Doc 8643 机型代码表,用于将 A320、B738 等 designator 映射为制造商、型号和类别。", + "update_interval_seconds": 2419200, + "last_updated_at": 1780381804 + }, + { + "name": "vrs_routes", + "download_url": "https://vrs-standing-data.adsb.lol/routes.csv", + "description": "VRS 航线表,以 callsign 查询机场代码序列,用于推断起飞机场、经停机场和目的机场。", + "update_interval_seconds": 3600, + "last_updated_at": 1780381804 + }, + { + "name": "vrs_airports", + "download_url": "https://vrs-standing-data.adsb.lol/airports.csv", + "description": "VRS 机场字典,提供机场代码、名称、国家、经纬度和高度。", + "update_interval_seconds": 3600, + "last_updated_at": 1780381804 + }, + { + "name": "adsblol_vrs_standing_data_routes", + "download_url": "https://vrs-standing-data.adsb.lol/routes.csv", + "description": "ADSB.lol 发布的 VRS standing-data 航线镜像,以 callsign 查询机场代码序列。", + "update_interval_seconds": 3600, + "last_updated_at": 1780381804 + }, + { + "name": "vradarserver_standing_data_routes", + "download_url": "https://vrs-standing-data.adsb.lol/routes.csv", + "description": "Virtual Radar Server standing-data 的航线适配表;当前在线更新使用 ADSB.lol 公共镜像。", + "update_interval_seconds": 3600, + "last_updated_at": 1780381804 + }, + { + "name": "opensky_flightdata_api_cache", + "download_url": "", + "description": "OpenSky FlightData API 查询缓存,按 ICAO24 和时间范围保存历史航班起降机场结果。", + "update_interval_seconds": 0, + "last_updated_at": 1780381804 + } ] }, "cesium_graphics": { diff --git a/module/Local_Server/server/Config.cpp b/module/Local_Server/server/Config.cpp index 9956453..04728ce 100644 --- a/module/Local_Server/server/Config.cpp +++ b/module/Local_Server/server/Config.cpp @@ -43,6 +43,24 @@ static std::vector read_string_array(const Psc::JSON* json, std::st } return ret; } +static std::vector read_optional_string_array(const Psc::JSON* json, std::string_view key) { + auto field = json->get(key); + if (field == nullptr) { + return {}; + } + if (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) { @@ -171,7 +189,9 @@ 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"); + file_format = read_optional_string_field(that_json, "file_format", "directory"); + directories = read_optional_string_array(that_json, "directories"); + files = read_optional_string_array(that_json, "files"); 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"); @@ -183,7 +203,9 @@ void Map_Tile_Config::from_base_json(const Psc::JSON* that_json) { } Psc::JSON Map_Tile_Config::to_base_json() const { auto ret = Psc::JSON::object(); + ret.append({"file_format", file_format}); ret.append(write_string_array("directories", directories)); + ret.append(write_string_array("files", files)); ret.append({"path_layout", path_layout}); ret.append({"y_axis", y_axis}); ret.append({"projection", projection}); @@ -209,6 +231,59 @@ Psc::JSON Map_Tile_Config::to_public_json(std::string_view url) const { } return ret; } +void Map_Tile_Source_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 source config"); + } + key = read_string_field(that_json, "key"); + name = read_string_field(that_json, "name"); + tile_type = read_optional_string_field(that_json, "tile_type", "imagery"); + url = read_optional_string_field(that_json, "url"); + resource.from_base_json(that_json->get("resource")); +} +Psc::JSON Map_Tile_Source_Config::to_base_json() const { + auto ret = Psc::JSON::object(); + ret.append({"key", key}); + ret.append({"name", name}); + ret.append({"tile_type", tile_type}); + if (!url.empty()) { + ret.append({"url", url}); + } + ret.append({"resource", resource.to_base_json()}); + return ret; +} +Psc::JSON Map_Tile_Source_Config::to_public_json(std::string_view local_url) const { + auto ret = Psc::JSON::object(); + ret.append({"key", key}); + ret.append({"name", name}); + ret.append({"tile_type", tile_type}); + ret.append({"resource", resource.to_public_json(local() ? local_url : url)}); + return ret; +} +static std::vector read_optional_tile_source_array(const Psc::JSON* json, std::string_view key) { + auto field = json->get(key); + if (field == nullptr) { + return {}; + } + if (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) { + Map_Tile_Source_Config item; + item.from_base_json(&child); + ret.push_back(std::move(item)); + } + return ret; +} +static Psc::JSON write_tile_source_array(std::string_view key, const std::vector& list) { + auto array = Psc::JSON::array(); + for (const auto& source : list) { + array.append(source.to_base_json()); + } + return Psc::JSON(key, array); +} 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"); @@ -220,6 +295,7 @@ void Map_Resources_Config::from_base_json(const Psc::JSON* that_json) { 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")); + tile_sources = read_optional_tile_source_array(that_json, "tile_sources"); if (!is_imagery_key(current_imagery_key)) { current_imagery_key = "imagery"; } @@ -230,44 +306,40 @@ Psc::JSON Map_Resources_Config::to_base_json() const { 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"}); + ret.append(write_tile_source_array("tile_sources", tile_sources)); 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)); + auto imagery_sources = Psc::JSON::array(); + auto terrain_sources = Psc::JSON::array(); + for (const auto& source : tile_sources) { + auto url = "/map/tile_sources/" + source.key + "/{z}/{x}/{y}"; + auto public_source = source.to_public_json(url); + sources.append(public_source); + if (source.tile_type == "imagery") { + imagery_sources.append(public_source); + } + else if (source.tile_type == "terrain") { + terrain_sources.append(public_source); + } + } ret.append({"current_imagery_key", current_imagery_key}); - ret.append({"imagery_sources", sources}); + ret.append({"tile_sources", sources}); + ret.append({"imagery_sources", imagery_sources}); + ret.append({"terrain_sources", terrain_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"; + return std::ranges::any_of(tile_sources, [key](const Map_Tile_Source_Config& source) { + return source.tile_type == "imagery" && source.key == key; + }); } void Map_Camera_Config::from_base_json(const Psc::JSON* that_json) { if (that_json == nullptr) { diff --git a/module/Local_Server/server/Config.h b/module/Local_Server/server/Config.h index b27f768..fc8ea72 100644 --- a/module/Local_Server/server/Config.h +++ b/module/Local_Server/server/Config.h @@ -112,7 +112,9 @@ struct Web_Server_Config { std::string webapp; }; struct Map_Tile_Config { + std::string file_format = "directory"; std::vector directories; + std::vector files; std::string path_layout; std::string y_axis; std::string projection; @@ -129,15 +131,37 @@ struct Map_Tile_Config { } return ret; } + [[nodiscard]] std::vector get_true_files() const { + std::vector ret; + ret.reserve(files.size()); + for (const auto& path : files) { + 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_Tile_Source_Config { + std::string key; + std::string name; + std::string tile_type = "imagery"; + std::string url; + Map_Tile_Config resource; + [[nodiscard]] bool local() const { + return resource.file_format != "online"; + } + 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 local_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; + std::vector tile_sources; void from_base_json(const Psc::JSON* that_json); [[nodiscard]] Psc::JSON to_base_json() const; [[nodiscard]] Psc::JSON to_public_json() const; diff --git a/module/Local_Server/server/tiles.cpp b/module/Local_Server/server/tiles.cpp index bdffb83..24a540f 100644 --- a/module/Local_Server/server/tiles.cpp +++ b/module/Local_Server/server/tiles.cpp @@ -1,13 +1,16 @@ #include "Global.h" #include "Performance_Monitor.h" +#include "psc_global_include/codec.h" +#include +#include #include +#include +#include #include #if defined(ASIO_HAS_FILE) #include #include #else -#include -#include #include #endif #include @@ -16,6 +19,8 @@ #include #include #include +#include +#include #include #include #include @@ -53,6 +58,10 @@ struct Tile_Load_Result { std::string content_type; File_Cache_Info cache_info; }; +asio::thread_pool& tile_file_pool() { + static asio::thread_pool pool(4); + return pool; +} std::string format_http_date(std::time_t time) { std::tm tm{}; #ifdef _WIN32 @@ -83,6 +92,12 @@ std::optional get_file_cache_info(const std::filesystem::path& result.last_modified = format_http_date(result.modified_time); return result; } +File_Cache_Info make_mbtiles_cache_info(const File_Cache_Info& file_info, const Tile_Request& request, std::uint64_t content_size) { + auto result = file_info; + result.size = content_size; + result.etag = std::format("W/\"{:x}-{:x}-{}-{}-{}-{:x}\"", static_cast(result.modified_time), file_info.size, request.z, request.x, request.y, content_size); + return result; +} std::string_view trim_http_ows(std::string_view value) { while (!value.empty() && (value.front() == ' ' || value.front() == '\t')) { value.remove_prefix(1); @@ -177,12 +192,9 @@ public: } } 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; + auto path = Psc::path_from_utf8(directory) / relative; if (std::filesystem::is_regular_file(path)) { return path; } @@ -205,12 +217,78 @@ private: Map_Tile_Config config_; std::vector directories_; }; +class Mbtiles_File { +public: + explicit Mbtiles_File(std::string path) + : path_(std::move(path)), db_(std::make_unique(path_, SQLite::OPEN_READONLY)) {} + std::optional load(const Tile_Request& request, const Tile_Cache_Request& cache_request, std::string_view content_type) { + auto file_info = get_file_cache_info(Psc::path_from_utf8(path_)); + if (!file_info) { + return std::nullopt; + } + std::lock_guard lock(mtx_); + SQLite::Statement statement(*db_, "SELECT tile_data FROM tiles WHERE zoom_level = ? AND tile_column = ? AND tile_row = ? LIMIT 1"); + statement.bind(1, static_cast(request.z)); + statement.bind(2, static_cast(request.x)); + statement.bind(3, static_cast(request.y)); + if (!statement.executeStep()) { + return std::nullopt; + } + auto column = statement.getColumn(0); + auto bytes = column.getBytes(); + if (bytes < 0 || static_cast(bytes) > kMaxTileFileSize) { + throw std::runtime_error("mbtiles tile is too large: " + path_); + } + auto cache_info = make_mbtiles_cache_info(*file_info, request, static_cast(bytes)); + if (is_not_modified(cache_request, cache_info)) { + return Tile_Load_Result{Tile_Load_Status::Not_Modified, path_, {}, std::string(content_type), std::move(cache_info)}; + } + std::string content; + if (cache_request.include_body) { + auto data = static_cast(column.getBlob()); + content.assign(data, static_cast(bytes)); + } + return Tile_Load_Result{Tile_Load_Status::Ready, path_, std::move(content), std::string(content_type), std::move(cache_info)}; + } +private: + std::string path_; + std::unique_ptr db_; + std::mutex mtx_; +}; class Tile_Source { public: explicit Tile_Source(Map_Tile_Config config) - : resolver_(config) {} + : config_(std::move(config)) { + if (config_.extension.empty() || config_.extension.front() != '.') { + throw std::invalid_argument("invalid tile extension: " + config_.extension); + } + if (config_.file_format == "directory") { + resolver_ = std::make_unique(config_); + } + else if (config_.file_format == "mbtiles") { + for (auto& file : config_.get_true_files()) { + mbtiles_files_.push_back(std::make_unique(std::move(file))); + } + } + else { + throw std::invalid_argument("invalid tile file_format: " + config_.file_format); + } + } asio::awaitable load(const Tile_Request& request, const Tile_Cache_Request& cache_request) const { - auto path = resolver_.find_file(request); + if (request.z < config_.minimum_level || request.z > config_.maximum_level) { + co_return Tile_Load_Result{Tile_Load_Status::Not_Found}; + } + if (config_.file_format == "mbtiles") { + co_return co_await load_mbtiles_tile(request, cache_request); + } + co_return co_await load_directory_tile(request, cache_request); + } + const Map_Tile_Config& config() const { + return config_; + } +private: + asio::awaitable load_directory_tile(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}; } @@ -241,16 +319,22 @@ public: } 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(); + asio::awaitable load_mbtiles_tile(Tile_Request request, Tile_Cache_Request cache_request) const { + auto content_type = httplib::detail::find_content_type("tile" + config_.extension, {}, "application/octet-stream"); + co_return co_await asio::co_spawn(tile_file_pool(), [this, request, cache_request = std::move(cache_request), content_type = std::move(content_type)]() -> asio::awaitable { + for (const auto& file : mbtiles_files_) { + auto result = file->load(request, cache_request, content_type); + if (result) { + co_return std::move(*result); + } + } + co_return Tile_Load_Result{Tile_Load_Status::Not_Found}; + }, asio::use_awaitable); } -private: - Tile_Path_Resolver resolver_; + Map_Tile_Config config_; + std::unique_ptr resolver_; + std::vector> mbtiles_files_; #if !defined(ASIO_HAS_FILE) - static asio::thread_pool& tile_file_pool() { - static asio::thread_pool pool(4); - return pool; - } static asio::awaitable read_file_fallback(std::filesystem::path path, std::size_t size) { co_return co_await asio::co_spawn(tile_file_pool(), [path = std::move(path), size]() -> asio::awaitable { std::ifstream file(path, std::ios::binary); @@ -355,6 +439,15 @@ void register_tile_handler(std::string_view regex, std::string_view prefix, std: }, {drogon::Get, drogon::Head}); } +void register_tile_source_handler(const Map_Tile_Source_Config& config) { + if (!config.local()) { + return; + } + auto source = std::make_shared(config.resource); + auto prefix = "/map/tile_sources/" + config.key + "/"; + auto regex = "^" + prefix + ".*$"; + register_tile_handler(regex, prefix, source); +} void register_map_resources_handler() { drogon::app().registerHandlerViaRegex( R"(^/map/resources$)", @@ -626,6 +719,9 @@ void Global::init_tiles() { register_map_graphics_handler(); register_map_models_handler(); register_map_view_handler(); + for (const auto& source : map_resources_config.tile_sources) { + register_tile_source_handler(source); + } 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);