#include #include #include "../../Radarcape_Core/cpp-httplib/httplib.h" #include #include #include #include #include "Global.h" #include "Performance_Monitor.h" #if defined(ASIO_HAS_FILE) #include #include #else #include #endif #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace { constexpr std::uint64_t kMaxTileFileSize = 32ull * 1024ull * 1024ull; enum class Tile_Load_Status { Ready, Not_Found, Not_Modified }; struct Tile_Request { std::uint32_t z; std::uint32_t x; std::uint32_t y; std::string extension; }; struct Tile_Cache_Request { std::string if_none_match; std::string if_modified_since; bool include_body; }; struct File_Cache_Info { std::uint64_t size; std::time_t modified_time; std::string etag; std::string last_modified; }; struct Tile_Load_Result { Tile_Load_Status status; std::string path; std::string content; std::string content_type; File_Cache_Info cache_info; }; 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 gmtime_s(&tm, &time); #else gmtime_r(&time, &tm); #endif char buffer[64]; auto size = std::strftime(buffer, sizeof(buffer), "%a, %d %b %Y %H:%M:%S GMT", &tm); return std::string(buffer, size); } std::optional get_file_cache_info(const std::filesystem::path& path) { #ifdef _WIN32 struct _stati64 info{}; 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(path.c_str(), &info) != 0 || !S_ISREG(info.st_mode) || info.st_size < 0) { return std::nullopt; } #endif File_Cache_Info result; result.size = static_cast(info.st_size); result.modified_time = info.st_mtime; result.etag = std::format("W/\"{:x}-{:x}\"", static_cast(result.modified_time), result.size); result.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); } while (!value.empty() && (value.back() == ' ' || value.back() == '\t')) { value.remove_suffix(1); } return value; } std::string_view remove_weak_prefix(std::string_view etag) { if (etag.starts_with("W/")) { etag.remove_prefix(2); } return etag; } bool etag_matches(std::string_view values, std::string_view current_etag) { while (!values.empty()) { auto pos = values.find(','); auto value = trim_http_ows(values.substr(0, pos)); if (value == "*" || remove_weak_prefix(value) == remove_weak_prefix(current_etag)) { return true; } if (pos == std::string_view::npos) { break; } values.remove_prefix(pos + 1); } return false; } 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("ETag", info.etag); response->addHeader("Last-Modified", info.last_modified); if (include_content_length) { response->addHeader("Content-Length", std::to_string(info.size)); } } bool is_unsigned_path_component(std::string_view value) { if (value.empty()) { return false; } return std::ranges::all_of(value, [](unsigned char ch) { return std::isdigit(ch) != 0; }); } std::uint32_t parse_uint32_component(std::string_view value) { std::uint64_t result = 0; for (char ch : value) { result = result * 10 + static_cast(ch - '0'); if (result > std::numeric_limits::max()) { throw std::out_of_range("tile coordinate is too large"); } } return static_cast(result); } std::optional parse_tile_request(std::string_view path, std::string_view prefix, std::string_view default_extension) { if (!path.starts_with(prefix)) { return std::nullopt; } path.remove_prefix(prefix.size()); auto first = path.find('/'); auto second = first == std::string_view::npos ? std::string_view::npos : path.find('/', first + 1); if (first == std::string_view::npos || second == std::string_view::npos || path.find('/', second + 1) != std::string_view::npos) { return std::nullopt; } auto z = path.substr(0, first); auto x = path.substr(first + 1, second - first - 1); auto y_with_extension = path.substr(second + 1); auto dot = y_with_extension.rfind('.'); auto y = dot == std::string_view::npos ? y_with_extension : y_with_extension.substr(0, dot); auto extension = dot == std::string_view::npos ? std::string(default_extension) : std::string(y_with_extension.substr(dot)); if (!is_unsigned_path_component(z) || !is_unsigned_path_component(x) || !is_unsigned_path_component(y) || extension != default_extension) { return std::nullopt; } return Tile_Request{parse_uint32_component(z), parse_uint32_component(x), parse_uint32_component(y), extension}; } class Tile_Path_Resolver { public: explicit Tile_Path_Resolver(const Map_Tile_Config& config) : config_(config), directories_(config.get_true_directories()) { if (config_.extension.empty() || config_.extension.front() != '.') { throw std::invalid_argument("invalid tile extension: " + config_.extension); } } std::optional find_file(const Tile_Request& request) const { auto relative = relative_path(request); for (const auto& directory : directories_) { auto path = Psc::path_from_utf8(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 == Map_Tile_Path_Layout::z_x_y) { return std::filesystem::path(z) / x / y; } return std::filesystem::path(z) / std::to_string(request.y) / (std::to_string(request.x) + config_.extension); } Map_Tile_Config config_; std::vector directories_; }; class Mbtiles_File { public: explicit Mbtiles_File(std::string path) : path_(std::move(path)) { auto file_info = get_file_cache_info(Psc::path_from_utf8(path_)); if (!file_info) { std::string str = "找不到地图:" + VAR_STR_1(path_); Psc::fail_fast(str); } 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); private: std::string path_; std::unique_ptr db_; std::mutex mtx_; }; std::optional Mbtiles_File::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)}; } class Tile_Source { public: explicit Tile_Source(Map_Tile_Config 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 == Map_Tile_File_Format::directory) { resolver_ = std::make_unique(config_); } else if (config_.file_format == Map_Tile_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: " + Psc::to_string(config_.file_format)); } } asio::awaitable load(const Tile_Request& request, const Tile_Cache_Request& cache_request) const { 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 == Map_Tile_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}; } auto cache_info = get_file_cache_info(*path); if (!cache_info) { co_return Tile_Load_Result{Tile_Load_Status::Not_Found}; } if (cache_info->size > kMaxTileFileSize || cache_info->size > std::numeric_limits::max()) { throw std::runtime_error("tile file is too large: " + path->generic_string()); } auto generic_path = path->generic_string(); auto content_type = httplib::detail::find_content_type(generic_path, {}, "application/octet-stream"); if (is_not_modified(cache_request, *cache_info)) { co_return Tile_Load_Result{ Tile_Load_Status::Not_Modified, std::move(generic_path), {}, std::move(content_type), *cache_info}; } std::string content; if (cache_request.include_body) { #if defined(ASIO_HAS_FILE) auto executor = co_await asio::this_coro::executor; asio::random_access_file file(executor, path->generic_string(), asio::file_base::read_only); content.resize(static_cast(cache_info->size)); if (!content.empty()) { co_await asio::async_read_at(file, 0, asio::buffer(content.data(), content.size()), asio::use_awaitable); } #else content = co_await read_file_fallback(*path, static_cast(cache_info->size)); #endif } co_return Tile_Load_Result{Tile_Load_Status::Ready, std::move(generic_path), std::move(content), std::move(content_type), *cache_info}; } 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); } Map_Tile_Config config_; std::unique_ptr resolver_; std::vector> mbtiles_files_; #if !defined(ASIO_HAS_FILE) 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); if (!file) { throw std::runtime_error("failed to open tile file: " + path.generic_string()); } std::string content(size, '\0'); if (!content.empty()) { file.read(content.data(), static_cast(content.size())); if (file.gcount() != static_cast(content.size())) { throw std::runtime_error("failed to read complete tile file: " + path.generic_string()); } } co_return content; }, asio::use_awaitable); } #endif }; 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", std::string(request_path)}); response->setStatusCode(drogon::k404NotFound); response->setContentTypeCode(drogon::CT_TEXT_PLAIN); if (include_body) { response->setBody(ret.to_json_string()); } response->addHeader("Cache-Control", "no-store"); return response; } 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); } drogon::HttpResponsePtr make_tile_read_failed_response(const std::exception& error, bool include_body) { auto response = drogon::HttpResponse::newHttpResponse(); response->setStatusCode(drogon::k500InternalServerError); response->setContentTypeCode(drogon::CT_APPLICATION_JSON); if (include_body) { response->setBody(Psc::JSON::object({{"error", Psc::platform_2_utf8(error.what())}}).to_json_string()); } return response; } Tile_Cache_Request make_cache_request(const drogon::HttpRequestPtr& req) { return Tile_Cache_Request{req->getHeader("if-none-match"), req->getHeader("if-modified-since"), req->method() != drogon::Head}; } void register_tile_handler(std::string_view regex, std::string_view prefix, std::shared_ptr source) { drogon::app().registerHandlerViaRegex( std::string(regex), [prefix = std::string(prefix), source = std::move(source)]( const drogon::HttpRequestPtr& req, std::function&& callback) mutable { auto cache_request = make_cache_request(req); auto request = parse_tile_request(req->path(), prefix, source->config().extension); if (!request) { callback(make_tile_not_found_response(req->path(), cache_request.include_body)); return; } drogon::async_run([callback = std::move(callback), source, request = std::move(*request), cache_request = std::move(cache_request), path = req->path()]() mutable -> drogon::Task<> { try { auto result = co_await Ecap_Coro::to_drogon(source->load(request, cache_request)); callback(make_tile_response(std::move(result), path, cache_request.include_body)); } catch (const std::exception& error) { if (server_logger) { server_logger->error("tiles", {}, LOG_POS + " " + Psc::platform_2_utf8(error.what())); } callback(make_tile_read_failed_response(error, cache_request.include_body)); } co_return; }); }, {drogon::Get, drogon::Head}); } void register_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$)", [](const drogon::HttpRequestPtr& req, std::function&& callback) { auto global = Global::instance(); auto response = drogon::HttpResponse::newHttpResponse(); if (req->method() == drogon::Post) { auto parsed = Psc::try_parse_json(req->body().data()); if (!parsed.has_value()) { response->setStatusCode(drogon::k400BadRequest); response->setContentTypeCode(drogon::CT_TEXT_PLAIN); response->setBody("invalid current_imagery_key"); callback(response); return; } auto key = parsed.value().try_get_string("current_imagery_key"); const bool valid_key = key.has_value() && global->map_resources_config.read([&](const auto& config) { return config.is_imagery_key(key.value()); }); if (!valid_key) { response->setStatusCode(drogon::k400BadRequest); response->setContentTypeCode(drogon::CT_TEXT_PLAIN); response->setBody("invalid current_imagery_key"); callback(response); return; } global->map_resources_config.write([&](auto& config) { config.current_imagery_key = key.value(); }); Global::save(Config_Section::map_resources); } response->setStatusCode(drogon::k200OK); response->setContentTypeCode(drogon::CT_APPLICATION_JSON); response->setBody(global->map_resources_config.read([](const auto& config) { return config.to_public_json().to_json_string(); })); response->addHeader("Cache-Control", "no-store"); callback(response); }, {drogon::Get, drogon::Post}); } void register_map_graphics_handler() { drogon::app().registerHandlerViaRegex( R"(^/map/graphics$)", [](const drogon::HttpRequestPtr& req, std::function&& callback) { auto global = Global::instance(); auto response = drogon::HttpResponse::newHttpResponse(); if (req->method() == drogon::Post) { auto parsed = Psc::try_parse_json(req->body().data()); if (!parsed.has_value()) { response->setStatusCode(drogon::k400BadRequest); response->setContentTypeCode(drogon::CT_TEXT_PLAIN); response->setBody("invalid cesium_graphics"); callback(response); return; } try { global->cesium_graphics_config.from_base_json(&parsed.value()); Global::save(Config_Section::cesium_graphics); } 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 view = global->map_view_config.read([](const auto& config) { return config; }); global->map_resources_config.read([&](const auto& config) { const auto first_key = [&](Map_Tile_Type type) { const auto item = std::ranges::find_if(config.tile_sources, [type](const auto& source) { return source.tile_type == type; }); return item == config.tile_sources.end() ? std::string{} : item->key; }; if(!config.is_imagery_key(view.map2d.current_imagery_key)) view.map2d.current_imagery_key = config.current_imagery_key; if(!config.is_imagery_key(view.map3d.current_imagery_key)) view.map3d.current_imagery_key = config.current_imagery_key; if(!view.map3d.current_terrain_key.empty() && !config.is_terrain_key(view.map3d.current_terrain_key)) view.map3d.current_terrain_key = first_key(Map_Tile_Type::terrain); }); auto ret = view.to_base_json(); global->map_resources_config.read([&](const auto& config) { ret.append({"current_imagery_key", config.current_imagery_key}); }); return ret; } bool is_multipart_request(const drogon::HttpRequestPtr& req) { auto content_type = req->getHeader("content-type"); return content_type.find("multipart/form-data") != std::string::npos; } std::string normalize_upload_extension(std::string extension) { std::ranges::transform(extension, extension.begin(), [](unsigned char ch) { return static_cast(std::tolower(ch)); }); if (!extension.empty() && extension.front() != '.') { extension.insert(extension.begin(), '.'); } return extension; } std::filesystem::path web_model_directory(Global* global) { return std::filesystem::path(global->web_server_config.get_true_webapp()) / "model"; } std::filesystem::path source_model_directory(Global* global) { return std::filesystem::path(global->web_server_config.get_true_webapp()).parent_path() / "model"; } bool is_safe_aircraft_model_key(std::string_view key) { return !key.empty() && std::ranges::all_of(key, [](unsigned char ch) { return std::isalnum(ch) != 0 || ch == '_'; }); } std::string model_file_name(std::string_view model_type, std::string_view aircraft_model_key) { if (model_type == "aircraft") { return "aircraft-custom.glb"; } if (model_type == "aircraft_type") { if (!is_safe_aircraft_model_key(aircraft_model_key)) { throw std::invalid_argument("invalid aircraft_model_key"); } auto normalized_key = std::string(aircraft_model_key); std::ranges::replace(normalized_key, '_', '-'); return "aircraft-" + normalized_key + ".glb"; } if (model_type == "base_station") { return "base-station-custom.glb"; } if (model_type == "device") { return "device-custom.glb"; } throw std::invalid_argument("invalid model_type"); } void save_uploaded_model_file(const drogon::HttpFile& upload, std::string_view model_type, std::string_view aircraft_model_key, Global* global) { auto extension = normalize_upload_extension(std::string(upload.getFileExtension())); if (extension != ".glb") { throw std::invalid_argument("model file must be .glb"); } if (model_type == "aircraft_type") { const bool exists = global->map_model_config.read([&](const auto& config) { return config.aircraft_models.contains(std::string(aircraft_model_key)); }); if (!exists) { throw std::invalid_argument("unknown aircraft_model_key"); } } auto upload_dir = std::filesystem::path(drogon::app().getUploadPath()); std::filesystem::create_directories(upload_dir); auto temp_name = std::string(model_type) + "_" + std::string(aircraft_model_key) + "_model_upload.tmp"; if (upload.saveAs(temp_name) != 0) { throw std::runtime_error("cannot save uploaded model"); } auto temp_path = upload_dir / temp_name; auto file_name = model_file_name(model_type, aircraft_model_key); auto web_dir = web_model_directory(global); auto source_dir = source_model_directory(global); std::filesystem::create_directories(web_dir); std::filesystem::create_directories(source_dir); std::filesystem::copy_file(temp_path, web_dir / file_name, std::filesystem::copy_options::overwrite_existing); std::filesystem::copy_file(temp_path, source_dir / file_name, std::filesystem::copy_options::overwrite_existing); std::filesystem::remove(temp_path); auto url = "/ui/model/" + file_name; global->map_model_config.write([&](auto& config) { if (model_type == "aircraft") { config.aircraft_model.url = url; } else if (model_type == "aircraft_type") { config.aircraft_models[std::string(aircraft_model_key)].url = url; } else if (model_type == "base_station") { config.base_station_model.url = url; } else { config.device_model.url = url; } }); } void handle_map_model_upload(const drogon::HttpRequestPtr& req, Global* global) { drogon::MultiPartParser parser; if (parser.parse(req) != 0) { throw std::invalid_argument("invalid multipart model upload"); } auto model_type = parser.getOptionalParameter("model_type"); if (!model_type || parser.getFiles().size() != 1) { throw std::invalid_argument("model_type or file"); } auto aircraft_model_key = parser.getOptionalParameter("aircraft_model_key").value_or(""); save_uploaded_model_file(parser.getFiles().front(), *model_type, aircraft_model_key, global); Global::save(Config_Section::map_models); } void handle_map_model_config_update(const drogon::HttpRequestPtr& req, Global* global) { auto parsed = Psc::try_parse_json(req->body().data()); if (!parsed.has_value()) { throw std::invalid_argument("invalid map_models"); } global->map_model_config.write([&](auto& config) { config.from_base_json(&parsed.value()); }); Global::save(Config_Section::map_models); } void register_map_models_handler() { drogon::app().registerHandlerViaRegex( R"(^/map/models$)", [](const drogon::HttpRequestPtr& req, std::function&& callback) { auto global = Global::instance(); auto response = drogon::HttpResponse::newHttpResponse(); try { if (req->method() == drogon::Post) { if (is_multipart_request(req)) { handle_map_model_upload(req, global); } else { handle_map_model_config_update(req, global); } } response->setStatusCode(drogon::k200OK); response->setContentTypeCode(drogon::CT_APPLICATION_JSON); response->setBody(global->map_model_config.read([](const auto& config) { return config.to_base_json().to_json_string(); })); } catch (const std::exception& error) { response->setStatusCode(drogon::k400BadRequest); response->setContentTypeCode(drogon::CT_TEXT_PLAIN); response->setBody(Psc::platform_2_utf8(error.what())); } response->addHeader("Cache-Control", "no-store"); callback(response); }, {drogon::Get, drogon::Post}); } void register_map_view_handler() { drogon::app().registerHandlerViaRegex( R"(^/map/view$)", [](const drogon::HttpRequestPtr& req, std::function&& callback) { auto global = Global::instance(); auto response = drogon::HttpResponse::newHttpResponse(); if (req->method() == drogon::Post) { auto parsed = Psc::try_parse_json(req->body().data()); if (!parsed.has_value()) { response->setStatusCode(drogon::k400BadRequest); response->setContentTypeCode(drogon::CT_TEXT_PLAIN); response->setBody("invalid map_view"); callback(response); return; } auto key = parsed.value().try_get_string("current_imagery_key"); const bool valid_key = key.has_value() && global->map_resources_config.read([&](const auto& config) { return config.is_imagery_key(key.value()); }); if (!valid_key) { 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.read([](const auto& config) { return config; }); next_view.from_base_json(&parsed.value()); global->map_resources_config.read([&](const auto& config) { if (!config.is_imagery_key(next_view.map2d.current_imagery_key) || !config.is_imagery_key(next_view.map3d.current_imagery_key)) { throw std::invalid_argument("invalid current_imagery_key"); } if (!next_view.map3d.current_terrain_key.empty() && !config.is_terrain_key(next_view.map3d.current_terrain_key)) { throw std::invalid_argument("invalid current_terrain_key"); } }); global->map_resources_config.write([&](auto& config) { config.current_imagery_key = key.value(); }); global->map_view_config.write([&](auto& config) { config = std::move(next_view); }); Global::save(Config_Section::map_resources); Global::save(Config_Section::map_view); } 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}); } } // namespace void Global::init_tiles() { register_map_resources_handler(); register_map_graphics_handler(); register_map_models_handler(); register_map_view_handler(); map_resources_config.read([](const auto& config) { for (const auto& source : config.tile_sources) { register_tile_source_handler(source); } }); }