mbtiles不存在快速失败

This commit is contained in:
2026-08-04 18:26:14 +08:00
parent 75bfd25508
commit 18e64dfe22
4 changed files with 427 additions and 302 deletions
+16 -12
View File
@@ -238,6 +238,7 @@ void Map_Tile_Source_Config::from_base_json(const Psc::JSON* that_json) {
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");
request_method = read_optional_string_field(that_json, "request_method", "GET");
url = read_optional_string_field(that_json, "url");
resource.from_base_json(that_json->get("resource"));
}
@@ -246,6 +247,7 @@ Psc::JSON Map_Tile_Source_Config::to_base_json() const {
ret.append({"key", key});
ret.append({"name", name});
ret.append({"tile_type", tile_type});
ret.append({"request_method", request_method});
if (!url.empty()) {
ret.append({"url", url});
}
@@ -257,6 +259,7 @@ Psc::JSON Map_Tile_Source_Config::to_public_json(std::string_view local_url) con
ret.append({"key", key});
ret.append({"name", name});
ret.append({"tile_type", tile_type});
ret.append({"request_method", request_method});
ret.append({"resource", resource.to_public_json(local() ? local_url : url)});
return ret;
}
@@ -292,30 +295,27 @@ void Map_Resources_Config::from_base_json(const Psc::JSON* that_json) {
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"));
tile_sources = read_optional_tile_source_array(that_json, "tile_sources");
if (!is_imagery_key(current_imagery_key)) {
current_imagery_key = "imagery";
auto source = std::ranges::find_if(tile_sources, [](const Map_Tile_Source_Config& item) {
return item.tile_type == "imagery";
});
current_imagery_key = source == tile_sources.end() ? "" : source->key;
}
}
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()});
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 sources = Psc::JSON::array();
auto imagery_sources = Psc::JSON::array();
auto terrain_sources = Psc::JSON::array();
auto terrain = Psc::JSON::object();
bool has_terrain = false;
for (const auto& source : tile_sources) {
auto url = "/map/tile_sources/" + source.key + "/{z}/{x}/{y}";
auto public_source = source.to_public_json(url);
@@ -325,15 +325,19 @@ Psc::JSON Map_Resources_Config::to_public_json() const {
}
else if (source.tile_type == "terrain") {
terrain_sources.append(public_source);
if (!has_terrain) {
terrain = *public_source.get("resource");
has_terrain = true;
}
}
}
ret.append({"current_imagery_key", current_imagery_key});
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}")});
if (has_terrain) {
ret.append({"terrain", std::move(terrain)});
}
return ret;
}
bool Map_Resources_Config::is_imagery_key(std::string_view key) const {
+1 -3
View File
@@ -147,6 +147,7 @@ struct Map_Tile_Source_Config {
std::string key;
std::string name;
std::string tile_type = "imagery";
std::string request_method = "GET";
std::string url;
Map_Tile_Config resource;
[[nodiscard]] bool local() const {
@@ -158,9 +159,6 @@ struct Map_Tile_Source_Config {
};
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<Map_Tile_Source_Config> tile_sources;
void from_base_json(const Psc::JSON* that_json);
[[nodiscard]] Psc::JSON to_base_json() const;
+250 -241
View File
@@ -1,12 +1,12 @@
#include "Global.h"
#include "Performance_Monitor.h"
#include "psc_global_include/codec.h"
#include <SQLiteCpp/Database.h>
#include <SQLiteCpp/Statement.h>
#include <algorithm>
#include <asio/co_spawn.hpp>
#include <asio/thread_pool.hpp>
#include <asio/use_awaitable.hpp>
#include "Global.h"
#include "Performance_Monitor.h"
#include "psc_global_include/codec.h"
#if defined(ASIO_HAS_FILE)
#include <asio/random_access_file.hpp>
#include <asio/read_at.hpp>
@@ -29,11 +29,7 @@
#include <vector>
namespace {
constexpr std::uint64_t kMaxTileFileSize = 32ull * 1024ull * 1024ull;
enum class Tile_Load_Status {
Ready,
Not_Found,
Not_Modified
};
enum class Tile_Load_Status { Ready, Not_Found, Not_Modified };
struct Tile_Request {
std::uint32_t z;
std::uint32_t x;
@@ -92,10 +88,12 @@ std::optional<File_Cache_Info> 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) {
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<std::uint64_t>(result.modified_time), file_info.size, request.z, request.x, request.y, content_size);
result.etag = std::format("W/\"{:x}-{:x}-{}-{}-{}-{:x}\"", static_cast<std::uint64_t>(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) {
@@ -133,7 +131,8 @@ bool is_not_modified(const Tile_Cache_Request& request, const File_Cache_Info& i
}
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) {
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);
@@ -145,9 +144,7 @@ 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;
});
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;
@@ -159,14 +156,16 @@ std::uint32_t parse_uint32_component(std::string_view value) {
}
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) {
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) {
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);
@@ -174,16 +173,18 @@ std::optional<Tile_Request> parse_tile_request(std::string_view path, std::strin
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) {
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()) {
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);
}
@@ -219,46 +220,57 @@ private:
};
class Mbtiles_File {
public:
explicit Mbtiles_File(std::string path)
: path_(std::move(path)), db_(std::make_unique<SQLite::Database>(path_, SQLite::OPEN_READONLY)) {}
std::optional<Tile_Load_Result> load(const Tile_Request& request, const Tile_Cache_Request& cache_request, std::string_view content_type) {
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) {
return std::nullopt;
std::string str = "找不到地图:" + VAR_STR_1(path_);
Psc::fail_fast(str);
}
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<int>(request.z));
statement.bind(2, static_cast<int>(request.x));
statement.bind(3, static_cast<int>(request.y));
if (!statement.executeStep()) {
return std::nullopt;
}
auto column = statement.getColumn(0);
auto bytes = column.getBytes();
if (bytes < 0 || static_cast<std::uint64_t>(bytes) > kMaxTileFileSize) {
throw std::runtime_error("mbtiles tile is too large: " + path_);
}
auto cache_info = make_mbtiles_cache_info(*file_info, request, static_cast<std::uint64_t>(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<const char*>(column.getBlob());
content.assign(data, static_cast<std::size_t>(bytes));
}
return Tile_Load_Result{Tile_Load_Status::Ready, path_, std::move(content), std::string(content_type), std::move(cache_info)};
db_ = std::make_unique<SQLite::Database>(path_, SQLite::OPEN_READONLY);
}
std::optional<Tile_Load_Result> load(const Tile_Request& request, const Tile_Cache_Request& cache_request,
std::string_view content_type);
private:
std::string path_;
std::unique_ptr<SQLite::Database> db_;
std::mutex mtx_;
};
std::optional<Tile_Load_Result> 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<int>(request.z));
statement.bind(2, static_cast<int>(request.x));
statement.bind(3, static_cast<int>(request.y));
if (!statement.executeStep()) {
return std::nullopt;
}
auto column = statement.getColumn(0);
auto bytes = column.getBytes();
if (bytes < 0 || static_cast<std::uint64_t>(bytes) > kMaxTileFileSize) {
throw std::runtime_error("mbtiles tile is too large: " + path_);
}
auto cache_info = make_mbtiles_cache_info(*file_info, request, static_cast<std::uint64_t>(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<const char*>(column.getBlob());
content.assign(data, static_cast<std::size_t>(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)) {
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);
}
@@ -287,7 +299,8 @@ public:
return config_;
}
private:
asio::awaitable<Tile_Load_Result> load_directory_tile(const Tile_Request& request, const Tile_Cache_Request& cache_request) const {
asio::awaitable<Tile_Load_Result> 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};
@@ -302,7 +315,8 @@ private:
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};
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) {
@@ -311,17 +325,24 @@ private:
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_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<std::size_t>(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};
co_return Tile_Load_Result{Tile_Load_Status::Ready, std::move(generic_path), std::move(content),
std::move(content_type), *cache_info};
}
asio::awaitable<Tile_Load_Result> 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<Tile_Load_Result> {
std::cout << "加载 load_mbtiles_tile" << std::endl;
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<Tile_Load_Result> {
for (const auto& file : mbtiles_files_) {
auto result = file->load(request, cache_request, content_type);
if (result) {
@@ -329,14 +350,17 @@ private:
}
}
co_return Tile_Load_Result{Tile_Load_Status::Not_Found};
}, asio::use_awaitable);
},
asio::use_awaitable);
}
Map_Tile_Config config_;
std::unique_ptr<Tile_Path_Resolver> resolver_;
std::vector<std::unique_ptr<Mbtiles_File>> mbtiles_files_;
#if !defined(ASIO_HAS_FILE)
static asio::awaitable<std::string> 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::string> {
co_return co_await asio::co_spawn(
tile_file_pool(),
[path = std::move(path), size]() -> asio::awaitable<std::string> {
std::ifstream file(path, std::ios::binary);
if (!file) {
throw std::runtime_error("failed to open tile file: " + path.generic_string());
@@ -349,7 +373,8 @@ private:
}
}
co_return content;
}, asio::use_awaitable);
},
asio::use_awaitable);
}
#endif
};
@@ -383,7 +408,8 @@ drogon::HttpResponsePtr make_tile_ok_response(Tile_Load_Result&& result, bool in
}
return response;
}
drogon::HttpResponsePtr make_tile_response(Tile_Load_Result&& result, std::string_view request_path, bool include_body) {
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);
@@ -404,40 +430,36 @@ drogon::HttpResponsePtr make_tile_read_failed_response(const std::exception& err
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
};
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(
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;
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));
}
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});
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()) {
@@ -450,74 +472,72 @@ void register_tile_source_handler(const Map_Tile_Source_Config& config) {
}
void register_map_resources_handler() {
drogon::app().registerHandlerViaRegex(
R"(^/map/resources$)",
[](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 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();
R"(^/map/resources$)",
[](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 current_imagery_key");
callback(response);
return;
}
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::Post});
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::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;
}
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;
}
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});
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();
@@ -529,9 +549,8 @@ bool is_multipart_request(const drogon::HttpRequestPtr& req) {
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<char>(std::tolower(ch));
});
std::ranges::transform(extension, extension.begin(),
[](unsigned char ch) { return static_cast<char>(std::tolower(ch)); });
if (!extension.empty() && extension.front() != '.') {
extension.insert(extension.begin(), '.');
}
@@ -544,9 +563,8 @@ 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 == '_';
});
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") {
@@ -568,15 +586,14 @@ std::string model_file_name(std::string_view model_type, std::string_view aircra
}
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) {
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" && !global->map_model_config.aircraft_models.contains(std::string(aircraft_model_key))) {
if (model_type == "aircraft_type" &&
!global->map_model_config.aircraft_models.contains(std::string(aircraft_model_key))) {
throw std::invalid_argument("unknown aircraft_model_key");
}
auto upload_dir = std::filesystem::path(drogon::app().getUploadPath());
@@ -631,90 +648,85 @@ void handle_map_model_config_update(const drogon::HttpRequestPtr& req, Global* g
}
void register_map_models_handler() {
drogon::app().registerHandlerViaRegex(
R"(^/map/models$)",
[](const drogon::HttpRequestPtr& req,
std::function<void(const drogon::HttpResponsePtr&)>&& 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);
}
R"(^/map/models$)",
[](const drogon::HttpRequestPtr& req, std::function<void(const drogon::HttpResponsePtr&)>&& 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);
}
response->setStatusCode(drogon::k200OK);
response->setContentTypeCode(drogon::CT_APPLICATION_JSON);
response->setBody(global->map_model_config.to_base_json().to_json_string());
else {
handle_map_model_config_update(req, global);
}
}
response->setStatusCode(drogon::k200OK);
response->setContentTypeCode(drogon::CT_APPLICATION_JSON);
response->setBody(global->map_model_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<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());
if (!global->map_resources_config.is_imagery_key(next_view.map2d.current_imagery_key) ||
!global->map_resources_config.is_imagery_key(next_view.map3d.current_imagery_key)) {
throw std::invalid_argument("invalid current_imagery_key");
}
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->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<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());
if (!global->map_resources_config.is_imagery_key(next_view.map2d.current_imagery_key) ||
!global->map_resources_config.is_imagery_key(next_view.map3d.current_imagery_key)) {
throw std::invalid_argument("invalid current_imagery_key");
}
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});
}
}
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() {
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_models_handler();
@@ -722,7 +734,4 @@ void Global::init_tiles() {
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);
}