支持mbtiles

This commit is contained in:
2026-08-04 17:21:15 +08:00
parent fd28aecc66
commit 75bfd25508
4 changed files with 614 additions and 314 deletions
+97 -25
View File
@@ -43,6 +43,24 @@ static std::vector<std::string> read_string_array(const Psc::JSON* json, std::st
}
return ret;
}
static std::vector<std::string> 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<std::string> ret;
ret.reserve(field->children.size());
for (const auto& child : field->children) {
if (child.valueType != Psc::String) {
throw Psc::json_assign_error(std::make_error_code(std::errc::invalid_argument), std::string(key));
}
ret.emplace_back(child.val);
}
return ret;
}
static Psc::JSON write_string_array(std::string_view key, const std::vector<std::string>& list) {
auto array = Psc::JSON::array();
for (const auto& path : list) {
@@ -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<Map_Tile_Source_Config> 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<Map_Tile_Source_Config> 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<Map_Tile_Source_Config>& 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) {
+24
View File
@@ -112,7 +112,9 @@ struct Web_Server_Config {
std::string webapp;
};
struct Map_Tile_Config {
std::string file_format = "directory";
std::vector<std::string> directories;
std::vector<std::string> 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<std::string> get_true_files() const {
std::vector<std::string> 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<Map_Tile_Source_Config> 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;
+112 -16
View File
@@ -1,13 +1,16 @@
#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>
#if defined(ASIO_HAS_FILE)
#include <asio/random_access_file.hpp>
#include <asio/read_at.hpp>
#else
#include <asio/co_spawn.hpp>
#include <asio/thread_pool.hpp>
#include <fstream>
#endif
#include <cctype>
@@ -16,6 +19,8 @@
#include <filesystem>
#include <format>
#include <limits>
#include <memory>
#include <mutex>
#include <optional>
#include <stdexcept>
#include <string>
@@ -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<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) {
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);
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<std::filesystem::path> find_file(const Tile_Request& request) const {
if (request.z < config_.minimum_level || request.z > config_.maximum_level) {
return std::nullopt;
}
auto relative = relative_path(request);
for (const auto& directory : directories_) {
auto path = std::filesystem::path(directory) / relative;
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<std::string> directories_;
};
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) {
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)};
}
private:
std::string path_;
std::unique_ptr<SQLite::Database> 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<Tile_Path_Resolver>(config_);
}
else if (config_.file_format == "mbtiles") {
for (auto& file : config_.get_true_files()) {
mbtiles_files_.push_back(std::make_unique<Mbtiles_File>(std::move(file)));
}
}
else {
throw std::invalid_argument("invalid tile file_format: " + config_.file_format);
}
}
asio::awaitable<Tile_Load_Result> 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<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};
}
@@ -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<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> {
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<Tile_Path_Resolver> resolver_;
std::vector<std::unique_ptr<Mbtiles_File>> 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<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> {
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<Tile_Source>(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);