3D
This commit is contained in:
@@ -1,31 +1,52 @@
|
||||
#include "Global.h"
|
||||
#include "Performance_Monitor.h"
|
||||
#include <algorithm>
|
||||
#include <asio/random_access_file.hpp>
|
||||
#include <asio/read_at.hpp>
|
||||
#include <asio/use_awaitable.hpp>
|
||||
#include <cctype>
|
||||
#include <cstdint>
|
||||
#include <ctime>
|
||||
#include <filesystem>
|
||||
#include <format>
|
||||
#include <limits>
|
||||
#include <optional>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <sys/stat.h>
|
||||
/**
|
||||
* 瓦片文件缓存策略:
|
||||
* * 首次请求通过 newFileResponse() 直接发送文件,避免先把整个文件读取到内存。
|
||||
* * 根据文件修改时间和大小生成弱 ETag,同时返回 Last-Modified。
|
||||
* * 浏览器缓存未过期时直接使用本地缓存,不再请求服务器。
|
||||
* * 缓存过期或强制刷新时,浏览器自动携带 If-None-Match 或 If-Modified-Since。
|
||||
* * 服务端优先验证 If-None-Match,文件未变化时返回 304,不重复传输文件内容。
|
||||
* * 404 响应使用 no-store,避免不存在的瓦片被浏览器长期缓存。
|
||||
* * max-age=31536000, immutable 仅适用于同一 URL 对应的文件永远不被原地修改;
|
||||
* 如果瓦片可能在原路径更新,应移除 immutable 并使用较短的 max-age。
|
||||
*/
|
||||
#include <vector>
|
||||
namespace {
|
||||
constexpr std::uint64_t kMaxTileFileSize = 32ull * 1024ull * 1024ull;
|
||||
enum class Tile_Load_Status {
|
||||
Ready,
|
||||
Not_Found,
|
||||
Not_Modified
|
||||
};
|
||||
struct Tile_Request {
|
||||
std::uint32_t z;
|
||||
std::uint32_t x;
|
||||
std::uint32_t y;
|
||||
std::string extension;
|
||||
};
|
||||
struct Tile_Cache_Request {
|
||||
std::string if_none_match;
|
||||
std::string if_modified_since;
|
||||
bool include_body;
|
||||
};
|
||||
struct File_Cache_Info {
|
||||
std::uint64_t size;
|
||||
std::time_t modified_time;
|
||||
std::string etag;
|
||||
std::string last_modified;
|
||||
};
|
||||
struct Tile_Load_Result {
|
||||
Tile_Load_Status status;
|
||||
std::string path;
|
||||
std::string content;
|
||||
std::string content_type;
|
||||
File_Cache_Info cache_info;
|
||||
};
|
||||
std::string format_http_date(std::time_t time) {
|
||||
std::tm tm{};
|
||||
#ifdef _WIN32
|
||||
@@ -37,26 +58,22 @@ std::string format_http_date(std::time_t time) {
|
||||
auto size = std::strftime(buffer, sizeof(buffer), "%a, %d %b %Y %H:%M:%S GMT", &tm);
|
||||
return std::string(buffer, size);
|
||||
}
|
||||
std::optional<File_Cache_Info> get_file_cache_info(const std::string& path) {
|
||||
auto native_path = std::filesystem::path(path);
|
||||
std::optional<File_Cache_Info> get_file_cache_info(const std::filesystem::path& path) {
|
||||
#ifdef _WIN32
|
||||
struct _stati64 info{};
|
||||
if (_wstati64(native_path.c_str(), &info) != 0 || (info.st_mode & _S_IFMT) != _S_IFREG) {
|
||||
if (_wstati64(path.c_str(), &info) != 0 || (info.st_mode & _S_IFMT) != _S_IFREG || info.st_size < 0) {
|
||||
return std::nullopt;
|
||||
}
|
||||
#else
|
||||
struct stat info{};
|
||||
if (::stat(native_path.c_str(), &info) != 0 || !S_ISREG(info.st_mode)) {
|
||||
if (::stat(path.c_str(), &info) != 0 || !S_ISREG(info.st_mode) || info.st_size < 0) {
|
||||
return std::nullopt;
|
||||
}
|
||||
#endif
|
||||
File_Cache_Info result;
|
||||
result.size = static_cast<std::uint64_t>(info.st_size);
|
||||
result.modified_time = info.st_mtime;
|
||||
result.etag = std::format(
|
||||
"W/\"{:x}-{:x}\"",
|
||||
static_cast<std::uint64_t>(result.modified_time),
|
||||
result.size);
|
||||
result.etag = std::format("W/\"{:x}-{:x}\"", static_cast<std::uint64_t>(result.modified_time), result.size);
|
||||
result.last_modified = format_http_date(result.modified_time);
|
||||
return result;
|
||||
}
|
||||
@@ -89,73 +106,355 @@ bool etag_matches(std::string_view values, std::string_view current_etag) {
|
||||
}
|
||||
return false;
|
||||
}
|
||||
void add_tile_cache_headers(
|
||||
const drogon::HttpResponsePtr& response,
|
||||
const File_Cache_Info& info) {
|
||||
bool is_not_modified(const Tile_Cache_Request& request, const File_Cache_Info& info) {
|
||||
if (!request.if_none_match.empty()) {
|
||||
return etag_matches(request.if_none_match, info.etag);
|
||||
}
|
||||
return !request.if_modified_since.empty() && request.if_modified_since == info.last_modified;
|
||||
}
|
||||
void add_tile_cache_headers(const drogon::HttpResponsePtr& response, const File_Cache_Info& info, bool include_content_length) {
|
||||
response->addHeader("Cache-Control", "public, max-age=86400");
|
||||
//response->addHeader("Cache-Control", "public, max-age=31536000, immutable");
|
||||
response->addHeader("ETag", info.etag);
|
||||
response->addHeader("Last-Modified", info.last_modified);
|
||||
if (include_content_length) {
|
||||
response->addHeader("Content-Length", std::to_string(info.size));
|
||||
}
|
||||
}
|
||||
drogon::HttpResponsePtr make_tile_not_found_response(const std::string& path) {
|
||||
bool is_unsigned_path_component(std::string_view value) {
|
||||
if (value.empty()) {
|
||||
return false;
|
||||
}
|
||||
return std::ranges::all_of(value, [](unsigned char ch) {
|
||||
return std::isdigit(ch) != 0;
|
||||
});
|
||||
}
|
||||
std::uint32_t parse_uint32_component(std::string_view value) {
|
||||
std::uint64_t result = 0;
|
||||
for (char ch : value) {
|
||||
result = result * 10 + static_cast<std::uint64_t>(ch - '0');
|
||||
if (result > std::numeric_limits<std::uint32_t>::max()) {
|
||||
throw std::out_of_range("tile coordinate is too large");
|
||||
}
|
||||
}
|
||||
return static_cast<std::uint32_t>(result);
|
||||
}
|
||||
std::optional<Tile_Request> parse_tile_request(std::string_view path, std::string_view prefix, std::string_view default_extension) {
|
||||
if (!path.starts_with(prefix)) {
|
||||
return std::nullopt;
|
||||
}
|
||||
path.remove_prefix(prefix.size());
|
||||
auto first = path.find('/');
|
||||
auto second = first == std::string_view::npos ? std::string_view::npos : path.find('/', first + 1);
|
||||
if (first == std::string_view::npos || second == std::string_view::npos || path.find('/', second + 1) != std::string_view::npos) {
|
||||
return std::nullopt;
|
||||
}
|
||||
auto z = path.substr(0, first);
|
||||
auto x = path.substr(first + 1, second - first - 1);
|
||||
auto y_with_extension = path.substr(second + 1);
|
||||
auto dot = y_with_extension.rfind('.');
|
||||
auto y = dot == std::string_view::npos ? y_with_extension : y_with_extension.substr(0, dot);
|
||||
auto extension = dot == std::string_view::npos ? std::string(default_extension) : std::string(y_with_extension.substr(dot));
|
||||
if (!is_unsigned_path_component(z) || !is_unsigned_path_component(x) || !is_unsigned_path_component(y) || extension != default_extension) {
|
||||
return std::nullopt;
|
||||
}
|
||||
return Tile_Request{parse_uint32_component(z), parse_uint32_component(x), parse_uint32_component(y), extension};
|
||||
}
|
||||
class Tile_Path_Resolver {
|
||||
public:
|
||||
explicit Tile_Path_Resolver(const Map_Tile_Config& config)
|
||||
: config_(config), directories_(config.get_true_directories()) {
|
||||
if (config_.path_layout != "z/x/y" && config_.path_layout != "z/y/x") {
|
||||
throw std::invalid_argument("invalid tile path_layout: " + config_.path_layout);
|
||||
}
|
||||
if (config_.extension.empty() || config_.extension.front() != '.') {
|
||||
throw std::invalid_argument("invalid tile extension: " + config_.extension);
|
||||
}
|
||||
}
|
||||
std::optional<std::filesystem::path> find_file(const Tile_Request& request) const {
|
||||
if (request.z < config_.minimum_level || request.z > config_.maximum_level) {
|
||||
return std::nullopt;
|
||||
}
|
||||
auto relative = relative_path(request);
|
||||
for (const auto& directory : directories_) {
|
||||
auto path = std::filesystem::path(directory) / relative;
|
||||
if (std::filesystem::is_regular_file(path)) {
|
||||
return path;
|
||||
}
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
const Map_Tile_Config& config() const {
|
||||
return config_;
|
||||
}
|
||||
private:
|
||||
std::filesystem::path relative_path(const Tile_Request& request) const {
|
||||
auto z = std::to_string(request.z);
|
||||
auto x = std::to_string(request.x);
|
||||
auto y = std::to_string(request.y) + config_.extension;
|
||||
if (config_.path_layout == "z/x/y") {
|
||||
return std::filesystem::path(z) / x / y;
|
||||
}
|
||||
return std::filesystem::path(z) / std::to_string(request.y) / (std::to_string(request.x) + config_.extension);
|
||||
}
|
||||
Map_Tile_Config config_;
|
||||
std::vector<std::string> directories_;
|
||||
};
|
||||
class Tile_Source {
|
||||
public:
|
||||
explicit Tile_Source(Map_Tile_Config config)
|
||||
: resolver_(config) {}
|
||||
asio::awaitable<Tile_Load_Result> load(const Tile_Request& request, const Tile_Cache_Request& cache_request) const {
|
||||
auto path = resolver_.find_file(request);
|
||||
if (!path) {
|
||||
co_return Tile_Load_Result{Tile_Load_Status::Not_Found};
|
||||
}
|
||||
auto cache_info = get_file_cache_info(*path);
|
||||
if (!cache_info) {
|
||||
co_return Tile_Load_Result{Tile_Load_Status::Not_Found};
|
||||
}
|
||||
if (cache_info->size > kMaxTileFileSize || cache_info->size > std::numeric_limits<std::size_t>::max()) {
|
||||
throw std::runtime_error("tile file is too large: " + path->generic_string());
|
||||
}
|
||||
auto generic_path = path->generic_string();
|
||||
auto content_type = httplib::detail::find_content_type(generic_path, {}, "application/octet-stream");
|
||||
if (is_not_modified(cache_request, *cache_info)) {
|
||||
co_return Tile_Load_Result{Tile_Load_Status::Not_Modified, std::move(generic_path), {}, std::move(content_type), *cache_info};
|
||||
}
|
||||
std::string content;
|
||||
if (cache_request.include_body) {
|
||||
auto executor = co_await asio::this_coro::executor;
|
||||
asio::random_access_file file(executor, path->generic_string(), asio::file_base::read_only);
|
||||
content.resize(static_cast<std::size_t>(cache_info->size));
|
||||
if (!content.empty()) {
|
||||
co_await asio::async_read_at(file, 0, asio::buffer(content.data(), content.size()), asio::use_awaitable);
|
||||
}
|
||||
}
|
||||
co_return Tile_Load_Result{Tile_Load_Status::Ready, std::move(generic_path), std::move(content), std::move(content_type), *cache_info};
|
||||
}
|
||||
const Map_Tile_Config& config() const {
|
||||
return resolver_.config();
|
||||
}
|
||||
private:
|
||||
Tile_Path_Resolver resolver_;
|
||||
};
|
||||
drogon::HttpResponsePtr make_tile_not_found_response(std::string_view request_path, bool include_body) {
|
||||
auto response = drogon::HttpResponse::newHttpResponse();
|
||||
JSON ret = JSON::object();
|
||||
ret.append({"LOG_POS", LOG_POS});
|
||||
ret.append({"not_found_path", path});
|
||||
ret.append({"not_found_path", std::string(request_path)});
|
||||
response->setStatusCode(drogon::k404NotFound);
|
||||
response->setContentTypeCode(drogon::CT_TEXT_PLAIN);
|
||||
response->setBody(ret.to_json_string());
|
||||
if (include_body) {
|
||||
response->setBody(ret.to_json_string());
|
||||
}
|
||||
response->addHeader("Cache-Control", "no-store");
|
||||
return response;
|
||||
}
|
||||
drogon::HttpResponsePtr return_tile_file(
|
||||
const drogon::HttpRequestPtr& req,
|
||||
const std::string& path) {
|
||||
auto info = get_file_cache_info(path);
|
||||
if (!info) {
|
||||
return make_tile_not_found_response(path);
|
||||
}
|
||||
const auto& if_none_match = req->getHeader("if-none-match");
|
||||
bool not_modified;
|
||||
if (!if_none_match.empty()) {
|
||||
not_modified = etag_matches(if_none_match, info->etag);
|
||||
}
|
||||
else {
|
||||
const auto& if_modified_since = req->getHeader("if-modified-since");
|
||||
not_modified = !if_modified_since.empty() && if_modified_since == info->last_modified;
|
||||
}
|
||||
if (not_modified) {
|
||||
auto response = drogon::HttpResponse::newHttpResponse();
|
||||
response->setStatusCode(drogon::k304NotModified);
|
||||
response->setContentTypeCode(drogon::CT_NONE);
|
||||
add_tile_cache_headers(response, *info);
|
||||
return response;
|
||||
}
|
||||
auto content_type = httplib::detail::find_content_type(
|
||||
path,
|
||||
{},
|
||||
"application/octet-stream");
|
||||
auto response = drogon::HttpResponse::newFileResponse(
|
||||
path,
|
||||
"",
|
||||
drogon::CT_CUSTOM,
|
||||
content_type,
|
||||
req);
|
||||
if (response->statusCode() == drogon::k200OK ||
|
||||
response->statusCode() == drogon::k206PartialContent) {
|
||||
add_tile_cache_headers(response, *info);
|
||||
drogon::HttpResponsePtr make_tile_not_modified_response(const File_Cache_Info& info) {
|
||||
auto response = drogon::HttpResponse::newHttpResponse();
|
||||
response->setStatusCode(drogon::k304NotModified);
|
||||
response->setContentTypeCode(drogon::CT_NONE);
|
||||
add_tile_cache_headers(response, info, false);
|
||||
return response;
|
||||
}
|
||||
drogon::HttpResponsePtr make_tile_ok_response(Tile_Load_Result&& result, bool include_body) {
|
||||
auto response = drogon::HttpResponse::newHttpResponse();
|
||||
response->setStatusCode(drogon::k200OK);
|
||||
response->setContentTypeString(result.content_type);
|
||||
add_tile_cache_headers(response, result.cache_info, !include_body);
|
||||
if (include_body) {
|
||||
response->setBody(std::move(result.content));
|
||||
}
|
||||
return response;
|
||||
}
|
||||
drogon::HttpResponsePtr make_tile_response(Tile_Load_Result&& result, std::string_view request_path, bool include_body) {
|
||||
switch (result.status) {
|
||||
case Tile_Load_Status::Ready:
|
||||
return make_tile_ok_response(std::move(result), include_body);
|
||||
case Tile_Load_Status::Not_Modified:
|
||||
return make_tile_not_modified_response(result.cache_info);
|
||||
case Tile_Load_Status::Not_Found:
|
||||
return make_tile_not_found_response(request_path, include_body);
|
||||
}
|
||||
return make_tile_not_found_response(request_path, include_body);
|
||||
}
|
||||
void Global::init_tiles() {
|
||||
static auto& web_server = web_server_config;
|
||||
drogon::HttpResponsePtr make_tile_read_failed_response(const std::exception& error, bool include_body) {
|
||||
auto response = drogon::HttpResponse::newHttpResponse();
|
||||
response->setStatusCode(drogon::k500InternalServerError);
|
||||
response->setContentTypeCode(drogon::CT_APPLICATION_JSON);
|
||||
if (include_body) {
|
||||
response->setBody(Psc::JSON::object({{"error", Psc::platform_2_utf8(error.what())}}).to_json_string());
|
||||
}
|
||||
return response;
|
||||
}
|
||||
Tile_Cache_Request make_cache_request(const drogon::HttpRequestPtr& req) {
|
||||
return Tile_Cache_Request{
|
||||
req->getHeader("if-none-match"),
|
||||
req->getHeader("if-modified-since"),
|
||||
req->method() != drogon::Head
|
||||
};
|
||||
}
|
||||
void register_tile_handler(std::string_view regex, std::string_view prefix, std::shared_ptr<Tile_Source> source) {
|
||||
drogon::app().registerHandlerViaRegex(
|
||||
R"(^/tiles/.*$)",
|
||||
std::string(regex),
|
||||
[prefix = std::string(prefix), source = std::move(source)](
|
||||
const drogon::HttpRequestPtr& req,
|
||||
std::function<void(const drogon::HttpResponsePtr&)>&& callback) mutable {
|
||||
auto cache_request = make_cache_request(req);
|
||||
auto request = parse_tile_request(req->path(), prefix, source->config().extension);
|
||||
if (!request) {
|
||||
callback(make_tile_not_found_response(req->path(), cache_request.include_body));
|
||||
return;
|
||||
}
|
||||
drogon::async_run(
|
||||
[callback = std::move(callback), source, request = std::move(*request), cache_request = std::move(cache_request), path = req->path()]() mutable -> drogon::Task<> {
|
||||
try {
|
||||
auto result = co_await Ecap_Coro::to_drogon(source->load(request, cache_request));
|
||||
callback(make_tile_response(std::move(result), path, cache_request.include_body));
|
||||
}
|
||||
catch (const std::exception& error) {
|
||||
if (server_logger) {
|
||||
server_logger->error("tiles", {}, LOG_POS + " " + Psc::platform_2_utf8(error.what()));
|
||||
}
|
||||
callback(make_tile_read_failed_response(error, cache_request.include_body));
|
||||
}
|
||||
co_return;
|
||||
});
|
||||
},
|
||||
{drogon::Get, drogon::Head});
|
||||
}
|
||||
void register_map_resources_handler() {
|
||||
drogon::app().registerHandlerViaRegex(
|
||||
R"(^/map/resources$)",
|
||||
[](const drogon::HttpRequestPtr& req,
|
||||
std::function<void(const drogon::HttpResponsePtr&)>&& callback) {
|
||||
auto path = web_server.get_true_tiles() + req->getPath().substr(6);
|
||||
callback(return_tile_file(req, path));
|
||||
auto global = Global::instance();
|
||||
auto response = drogon::HttpResponse::newHttpResponse();
|
||||
if (req->method() == drogon::Post) {
|
||||
auto parsed = Psc::try_parse_json(req->body().data());
|
||||
if (!parsed.has_value()) {
|
||||
response->setStatusCode(drogon::k400BadRequest);
|
||||
response->setContentTypeCode(drogon::CT_TEXT_PLAIN);
|
||||
response->setBody("invalid current_imagery_key");
|
||||
callback(response);
|
||||
return;
|
||||
}
|
||||
auto key = parsed.value().try_get_string("current_imagery_key");
|
||||
if (!key.has_value() || !global->map_resources_config.is_imagery_key(key.value())) {
|
||||
response->setStatusCode(drogon::k400BadRequest);
|
||||
response->setContentTypeCode(drogon::CT_TEXT_PLAIN);
|
||||
response->setBody("invalid current_imagery_key");
|
||||
callback(response);
|
||||
return;
|
||||
}
|
||||
global->map_resources_config.current_imagery_key = key.value();
|
||||
Global::save();
|
||||
}
|
||||
response->setStatusCode(drogon::k200OK);
|
||||
response->setContentTypeCode(drogon::CT_APPLICATION_JSON);
|
||||
response->setBody(global->map_resources_config.to_public_json().to_json_string());
|
||||
response->addHeader("Cache-Control", "no-store");
|
||||
callback(response);
|
||||
},
|
||||
{drogon::Get});
|
||||
{drogon::Get, drogon::Post});
|
||||
}
|
||||
void register_map_graphics_handler() {
|
||||
drogon::app().registerHandlerViaRegex(
|
||||
R"(^/map/graphics$)",
|
||||
[](const drogon::HttpRequestPtr& req,
|
||||
std::function<void(const drogon::HttpResponsePtr&)>&& callback) {
|
||||
auto global = Global::instance();
|
||||
auto response = drogon::HttpResponse::newHttpResponse();
|
||||
if (req->method() == drogon::Post) {
|
||||
auto parsed = Psc::try_parse_json(req->body().data());
|
||||
if (!parsed.has_value()) {
|
||||
response->setStatusCode(drogon::k400BadRequest);
|
||||
response->setContentTypeCode(drogon::CT_TEXT_PLAIN);
|
||||
response->setBody("invalid cesium_graphics");
|
||||
callback(response);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
global->cesium_graphics_config.from_base_json(&parsed.value());
|
||||
Global::save();
|
||||
}
|
||||
catch (const std::exception& error) {
|
||||
response->setStatusCode(drogon::k400BadRequest);
|
||||
response->setContentTypeCode(drogon::CT_TEXT_PLAIN);
|
||||
response->setBody(Psc::platform_2_utf8(error.what()));
|
||||
callback(response);
|
||||
return;
|
||||
}
|
||||
}
|
||||
response->setStatusCode(drogon::k200OK);
|
||||
response->setContentTypeCode(drogon::CT_APPLICATION_JSON);
|
||||
response->setBody(global->cesium_graphics_config.to_base_json().to_json_string());
|
||||
response->addHeader("Cache-Control", "no-store");
|
||||
callback(response);
|
||||
},
|
||||
{drogon::Get, drogon::Post});
|
||||
}
|
||||
Psc::JSON make_map_view_response(Global* global) {
|
||||
auto ret = global->map_view_config.to_base_json();
|
||||
ret.append({"current_imagery_key", global->map_resources_config.current_imagery_key});
|
||||
return ret;
|
||||
}
|
||||
void register_map_view_handler() {
|
||||
drogon::app().registerHandlerViaRegex(
|
||||
R"(^/map/view$)",
|
||||
[](const drogon::HttpRequestPtr& req,
|
||||
std::function<void(const drogon::HttpResponsePtr&)>&& callback) {
|
||||
auto global = Global::instance();
|
||||
auto response = drogon::HttpResponse::newHttpResponse();
|
||||
if (req->method() == drogon::Post) {
|
||||
auto parsed = Psc::try_parse_json(req->body().data());
|
||||
if (!parsed.has_value()) {
|
||||
response->setStatusCode(drogon::k400BadRequest);
|
||||
response->setContentTypeCode(drogon::CT_TEXT_PLAIN);
|
||||
response->setBody("invalid map_view");
|
||||
callback(response);
|
||||
return;
|
||||
}
|
||||
auto key = parsed.value().try_get_string("current_imagery_key");
|
||||
if (!key.has_value() || !global->map_resources_config.is_imagery_key(key.value())) {
|
||||
response->setStatusCode(drogon::k400BadRequest);
|
||||
response->setContentTypeCode(drogon::CT_TEXT_PLAIN);
|
||||
response->setBody("invalid current_imagery_key");
|
||||
callback(response);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
auto next_view = global->map_view_config;
|
||||
next_view.from_base_json(&parsed.value());
|
||||
global->map_resources_config.current_imagery_key = key.value();
|
||||
global->map_view_config = next_view;
|
||||
Global::save();
|
||||
}
|
||||
catch (const std::exception& error) {
|
||||
response->setStatusCode(drogon::k400BadRequest);
|
||||
response->setContentTypeCode(drogon::CT_TEXT_PLAIN);
|
||||
response->setBody(Psc::platform_2_utf8(error.what()));
|
||||
callback(response);
|
||||
return;
|
||||
}
|
||||
}
|
||||
response->setStatusCode(drogon::k200OK);
|
||||
response->setContentTypeCode(drogon::CT_APPLICATION_JSON);
|
||||
response->setBody(make_map_view_response(global).to_json_string());
|
||||
response->addHeader("Cache-Control", "no-store");
|
||||
callback(response);
|
||||
},
|
||||
{drogon::Get, drogon::Post});
|
||||
}
|
||||
}
|
||||
void Global::init_tiles() {
|
||||
auto imagery = std::make_shared<Tile_Source>(map_resources_config.imagery);
|
||||
auto google_imagery = std::make_shared<Tile_Source>(map_resources_config.google_imagery);
|
||||
auto terrain = std::make_shared<Tile_Source>(map_resources_config.terrain);
|
||||
register_map_resources_handler();
|
||||
register_map_graphics_handler();
|
||||
register_map_view_handler();
|
||||
register_tile_handler(R"(^/map/imagery/.*$)", "/map/imagery/", imagery);
|
||||
register_tile_handler(R"(^/tiles/.*$)", "/tiles/", google_imagery);
|
||||
register_tile_handler(R"(^/map/terrain/.*$)", "/map/terrain/", terrain);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user